use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use serde::Deserialize; use serde_json::Value; use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest}; use sgclaw::generated_scene::ir::SceneIr; #[derive(Debug, Deserialize)] struct CanonicalManifest { targets: Vec, } #[derive(Debug, Deserialize)] struct CanonicalTarget { id: String, #[serde(rename = "fixtureDir")] fixture_dir: String, #[serde(rename = "canonicalSceneIr")] canonical_scene_ir: String, #[serde(rename = "requiredEvidenceTypes")] required_evidence_types: Vec, #[serde(rename = "requiredWorkflowStepTypes")] required_workflow_step_types: Vec, #[serde(rename = "requiredGateNames")] required_gate_names: Vec, #[serde(rename = "acceptanceChecklist")] acceptance_checklist: Vec, #[serde(rename = "failureTaxonomy")] failure_taxonomy: Vec, } #[test] fn p0_canonical_manifest_is_actionable() { let manifest = load_manifest(); assert_eq!(manifest.targets.len(), 3); for target in manifest.targets { assert!( Path::new(&target.fixture_dir).exists(), "fixture dir missing: {}", target.fixture_dir ); assert!( Path::new(&target.canonical_scene_ir).exists(), "canonical ir missing: {}", target.canonical_scene_ir ); assert!( !target.required_evidence_types.is_empty(), "required_evidence_types should not be empty for {}", target.id ); assert!( !target.required_workflow_step_types.is_empty(), "required_workflow_step_types should not be empty for {}", target.id ); assert!( !target.required_gate_names.is_empty(), "required_gate_names should not be empty for {}", target.id ); assert!( !target.acceptance_checklist.is_empty(), "acceptance_checklist should not be empty for {}", target.id ); assert!( !target.failure_taxonomy.is_empty(), "failure_taxonomy should not be empty for {}", target.id ); } } #[test] fn generated_p0_fixtures_align_with_canonical_answers() { let manifest = load_manifest(); for target in manifest.targets { let output_root = temp_workspace(&format!("sgclaw-canonical-{}", target.id)); let scene_id = scene_id_from_target(&target.id); let scene_name = scene_name_from_target(&target.id); generate_scene_package(GenerateSceneRequest { source_dir: PathBuf::from(&target.fixture_dir), scene_id, scene_name, scene_kind: None, target_url: None, output_root: output_root.clone(), lessons_path: None, scene_info_json: None, scene_ir_json: None, }) .unwrap_or_else(|err| panic!("{} failed to generate: {}", target.id, err)); let generated_dir = output_root .join("skills") .join(scene_id_from_target(&target.id)); let generated_report: SceneIr = serde_json::from_str( &fs::read_to_string(generated_dir.join("references/generation-report.json")).unwrap(), ) .unwrap(); let canonical: SceneIr = serde_json::from_str(&fs::read_to_string(&target.canonical_scene_ir).unwrap()).unwrap(); assert_eq!( generated_report.workflow_archetype().as_str(), canonical.workflow_archetype().as_str(), "archetype mismatch for {}", target.id ); assert_eq!( generated_report.bootstrap.expected_domain, canonical.bootstrap.expected_domain, "expectedDomain mismatch for {}", target.id ); assert!( generated_report .bootstrap .target_url .starts_with(&canonical.bootstrap.target_url), "targetUrl mismatch for {}: {} vs {}", target.id, generated_report.bootstrap.target_url, canonical.bootstrap.target_url ); let generated_step_types = generated_report .workflow_steps .iter() .map(|step| step.step_type.clone()) .collect::>(); for required in &target.required_workflow_step_types { assert!( generated_step_types.iter().any(|step| step == required), "missing workflow step {} for {}", required, target.id ); } let generated_gate_names = generated_report .readiness .gates .iter() .map(|gate| gate.name.clone()) .collect::>(); for required in &target.required_gate_names { assert!( generated_gate_names.iter().any(|gate| gate == required), "missing readiness gate {} for {}", required, target.id ); } let generated_evidence_types = generated_report .evidence .iter() .map(|item| item.evidence_type.clone()) .collect::>(); for required in &target.required_evidence_types { assert!( generated_evidence_types.iter().any(|kind| kind == required), "missing evidence type {} for {}", required, target.id ); } let generated_json: Value = serde_json::from_str( &fs::read_to_string(generated_dir.join("references/generation-report.json")).unwrap(), ) .unwrap(); assert!( generated_json.get("readiness").is_some(), "generation-report.json should include readiness for {}", target.id ); if target.id == "p0-3-paginated-enrichment" { assert_eq!( generated_report .main_request .as_ref() .map(|request| request.response_path.as_str()), canonical .main_request .as_ref() .map(|request| request.response_path.as_str()), "g3 main request response path mismatch for {}", target.id ); assert_eq!( generated_report .pagination_plan .as_ref() .map(|plan| plan.page_field.as_str()), canonical .pagination_plan .as_ref() .map(|plan| plan.page_field.as_str()), "g3 page field mismatch for {}", target.id ); assert_eq!( generated_report .pagination_plan .as_ref() .map(|plan| plan.termination_rule.as_str()), canonical .pagination_plan .as_ref() .map(|plan| plan.termination_rule.as_str()), "g3 termination rule mismatch for {}", target.id ); assert_eq!( generated_report.join_keys, canonical.join_keys, "g3 join keys mismatch for {}", target.id ); assert_eq!( generated_report.merge_or_dedupe_rules, canonical.merge_or_dedupe_rules, "g3 merge/dedupe rules mismatch for {}", target.id ); assert_eq!( generated_report .export_plan .as_ref() .and_then(|plan| plan.entry.as_deref()), canonical .export_plan .as_ref() .and_then(|plan| plan.entry.as_deref()), "g3 export entry mismatch for {}", target.id ); } } } fn load_manifest() -> CanonicalManifest { serde_json::from_str( &fs::read_to_string( "tests/fixtures/generated_scene/p0_canonical_answers/p0-canonical-manifest.json", ) .unwrap(), ) .unwrap() } fn scene_id_from_target(target_id: &str) -> String { match target_id { "p0-1-tq-lineloss-report" => "tq-lineloss-report".to_string(), "p0-2-single-request-table" => "single-request-report".to_string(), "p0-3-paginated-enrichment" => "paginated-enrichment-report".to_string(), other => other.to_string(), } } fn scene_name_from_target(target_id: &str) -> String { match target_id { "p0-1-tq-lineloss-report" => "台区线损月周累计统计分析".to_string(), "p0-2-single-request-table" => "单请求通用报表".to_string(), "p0-3-paginated-enrichment" => "分页补数明细报表".to_string(), other => other.to_string(), } } fn temp_workspace(prefix: &str) -> PathBuf { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); let path = std::env::temp_dir().join(format!("{prefix}-{nanos}")); fs::create_dir_all(&path).unwrap(); path }