use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use serde::Deserialize; use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest}; use sgclaw::generated_scene::ir::SceneIr; #[derive(Debug, Deserialize)] struct P1FamilyManifest { families: Vec, } #[derive(Debug, Deserialize)] struct P1FamilySpec { id: String, group: String, #[serde(rename = "familyName")] family_name: String, #[serde(rename = "representativeFixtureDir")] representative_fixture_dir: String, #[serde(rename = "representativeSceneId")] representative_scene_id: String, #[serde(rename = "representativeSceneName")] representative_scene_name: String, #[serde(rename = "expectedArchetype")] expected_archetype: String, #[serde(rename = "requiredGateNames")] required_gate_names: Vec, #[serde(rename = "requiredEvidenceTypes")] required_evidence_types: Vec, #[serde(rename = "expansionFixtureDir", default)] expansion_fixture_dir: Option, #[serde(rename = "expansionSceneId", default)] expansion_scene_id: Option, #[serde(rename = "expansionSceneName", default)] expansion_scene_name: Option, #[serde(rename = "expansionAssertions", default)] expansion_assertions: Option, #[serde(rename = "batchCandidateAsset", default)] batch_candidate_asset: Option, #[serde(rename = "batchExpansionFixtures", default)] batch_expansion_fixtures: Vec, #[serde(rename = "successRateSummary")] success_rate_summary: String, #[serde(rename = "failureTaxonomy")] failure_taxonomy: Vec, } #[derive(Debug, Deserialize, Default)] struct ExpansionAssertions { #[serde(rename = "requiredDefaultMode", default)] required_default_mode: Option, #[serde(rename = "expectedPaginationField", default)] expected_pagination_field: Option, #[serde(rename = "requiredJoinKey", default)] required_join_key: Option, #[serde(rename = "requiredAggregateRule", default)] required_aggregate_rule: Option, #[serde(rename = "requiredMainRequest", default)] required_main_request: Option, #[serde(rename = "requiredEnrichmentRequest", default)] required_enrichment_request: Option, #[serde(rename = "requiredMergeJoinKey", default)] required_merge_join_key: Option, #[serde(rename = "requiredMergeAggregateRule", default)] required_merge_aggregate_rule: Option, #[serde(rename = "requiredOutputColumn", default)] required_output_column: Option, } #[derive(Debug, Deserialize)] struct BatchExpansionFixture { #[serde(rename = "fixtureDir")] fixture_dir: String, #[serde(rename = "sceneId")] scene_id: String, #[serde(rename = "sceneName")] scene_name: String, assertions: ExpansionAssertions, } #[test] fn p1_family_manifest_is_actionable() { let manifest = load_manifest(); assert_eq!(manifest.families.len(), 7); for family in manifest.families { assert!(matches!( family.group.as_str(), "G1" | "G2" | "G3" | "G6" | "G7" | "G8" )); assert!(!family.family_name.trim().is_empty()); assert!(Path::new(&family.representative_fixture_dir).exists()); assert!(!family.expected_archetype.trim().is_empty()); assert!(!family.required_gate_names.is_empty()); assert!(!family.required_evidence_types.is_empty()); assert!(!family.success_rate_summary.trim().is_empty()); assert!(!family.failure_taxonomy.is_empty()); if let Some(expansion_fixture_dir) = &family.expansion_fixture_dir { assert!(Path::new(expansion_fixture_dir).exists()); assert!(!family .expansion_scene_id .as_deref() .unwrap_or_default() .is_empty()); assert!(!family .expansion_scene_name .as_deref() .unwrap_or_default() .is_empty()); } if let Some(batch_candidate_asset) = &family.batch_candidate_asset { assert!(Path::new(batch_candidate_asset).exists()); } for fixture in &family.batch_expansion_fixtures { assert!(Path::new(&fixture.fixture_dir).exists()); assert!(!fixture.scene_id.is_empty()); assert!(!fixture.scene_name.is_empty()); } } } #[test] fn representative_p1_family_migrations_are_reusable() { let manifest = load_manifest(); for family in manifest.families { let output_root = temp_workspace(&format!("sgclaw-p1-family-{}", family.id)); generate_scene_package(GenerateSceneRequest { source_dir: PathBuf::from(&family.representative_fixture_dir), scene_id: family.representative_scene_id.clone(), scene_name: family.representative_scene_name.clone(), 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 representative migration: {}", family.id, err)); let generated_dir = output_root .join("skills") .join(&family.representative_scene_id); let generated_report: SceneIr = serde_json::from_str( &fs::read_to_string(generated_dir.join("references/generation-report.json")).unwrap(), ) .unwrap(); assert_eq!( generated_report.workflow_archetype().as_str(), family.expected_archetype, "expected archetype mismatch for {}", family.id ); for gate_name in &family.required_gate_names { assert!( generated_report .readiness .gates .iter() .any(|gate| gate.name == *gate_name), "missing gate {} for {}", gate_name, family.id ); } for evidence_type in &family.required_evidence_types { assert!( generated_report .evidence .iter() .any(|item| item.evidence_type == *evidence_type), "missing evidence type {} for {}", evidence_type, family.id ); } assert!( generated_report.readiness.level == "A" || generated_report.readiness.level == "B", "representative migration should be reusable for {}", family.id ); if let (Some(expansion_fixture_dir), Some(expansion_scene_id), Some(expansion_scene_name)) = ( &family.expansion_fixture_dir, &family.expansion_scene_id, &family.expansion_scene_name, ) { let expansion_output_root = temp_workspace(&format!("sgclaw-p1-family-expansion-{}", family.id)); generate_scene_package(GenerateSceneRequest { source_dir: PathBuf::from(expansion_fixture_dir), scene_id: expansion_scene_id.clone(), scene_name: expansion_scene_name.clone(), scene_kind: None, target_url: None, output_root: expansion_output_root.clone(), lessons_path: None, scene_info_json: None, scene_ir_json: None, }) .unwrap_or_else(|err| panic!("{} failed expansion migration: {}", family.id, err)); let expansion_dir = expansion_output_root .join("skills") .join(expansion_scene_id); let expansion_report: SceneIr = serde_json::from_str( &fs::read_to_string(expansion_dir.join("references/generation-report.json")) .unwrap(), ) .unwrap(); assert_eq!( expansion_report.workflow_archetype().as_str(), family.expected_archetype, "expected expansion archetype mismatch for {}", family.id ); assert!( expansion_report.readiness.level == "A" || expansion_report.readiness.level == "B", "expansion migration should be reusable for {}", family.id ); if let Some(assertions) = &family.expansion_assertions { if let Some(required_default_mode) = &assertions.required_default_mode { assert_eq!( expansion_report.default_mode.as_deref(), Some(required_default_mode.as_str()), "missing expansion default mode {} for {}", required_default_mode, family.id ); } if let Some(expected_pagination_field) = &assertions.expected_pagination_field { assert_eq!( expansion_report .pagination_plan .as_ref() .map(|plan| plan.page_field.as_str()), Some(expected_pagination_field.as_str()), "expansion pagination field mismatch for {}", family.id ); } if let Some(required_join_key) = &assertions.required_join_key { assert!( expansion_report .join_keys .iter() .any(|key| key == required_join_key), "missing expansion join key {} for {}", required_join_key, family.id ); } if let Some(required_aggregate_rule) = &assertions.required_aggregate_rule { assert!( expansion_report .merge_or_dedupe_rules .iter() .any(|rule| rule == required_aggregate_rule), "missing expansion aggregate rule {} for {}", required_aggregate_rule, family.id ); } if let Some(required_main_request) = &assertions.required_main_request { assert!( expansion_report .main_request .as_ref() .and_then(|request| request.api_endpoint.as_ref()) .map(|endpoint| endpoint.name.contains(required_main_request)) .unwrap_or(false), "missing expansion main request {} for {}", required_main_request, family.id ); } if let Some(required_enrichment_request) = &assertions.required_enrichment_request { assert!( expansion_report .enrichment_requests .iter() .any(|request| request.name.contains(required_enrichment_request)), "missing expansion enrichment request {} for {}", required_enrichment_request, family.id ); } if let Some(required_merge_join_key) = &assertions.required_merge_join_key { assert!( expansion_report .merge_plan .as_ref() .map(|plan| { plan.join_keys .iter() .any(|key| key == required_merge_join_key) }) .unwrap_or(false), "missing expansion merge join key {} for {}", required_merge_join_key, family.id ); } if let Some(required_merge_aggregate_rule) = &assertions.required_merge_aggregate_rule { assert!( expansion_report .merge_plan .as_ref() .map(|plan| { plan.aggregate_rules .iter() .any(|rule| rule == required_merge_aggregate_rule) }) .unwrap_or(false), "missing expansion merge aggregate rule {} for {}", required_merge_aggregate_rule, family.id ); } if let Some(required_output_column) = &assertions.required_output_column { assert!( expansion_report .merge_plan .as_ref() .map(|plan| { plan.output_columns .iter() .any(|(field, _)| field == required_output_column) }) .unwrap_or(false), "missing expansion output column {} for {}", required_output_column, family.id ); } } } for batch_fixture in &family.batch_expansion_fixtures { let batch_output_root = temp_workspace(&format!( "sgclaw-p1-family-batch-{}-{}", family.id, batch_fixture.scene_id )); generate_scene_package(GenerateSceneRequest { source_dir: PathBuf::from(&batch_fixture.fixture_dir), scene_id: batch_fixture.scene_id.clone(), scene_name: batch_fixture.scene_name.clone(), scene_kind: None, target_url: None, output_root: batch_output_root.clone(), lessons_path: None, scene_info_json: None, scene_ir_json: None, }) .unwrap_or_else(|err| { panic!("{} failed batch expansion migration: {}", family.id, err) }); let batch_dir = batch_output_root .join("skills") .join(&batch_fixture.scene_id); let batch_report: SceneIr = serde_json::from_str( &fs::read_to_string(batch_dir.join("references/generation-report.json")).unwrap(), ) .unwrap(); assert_eq!( batch_report.workflow_archetype().as_str(), family.expected_archetype, "expected batch expansion archetype mismatch for {}", family.id ); assert!( batch_report.readiness.level == "A" || batch_report.readiness.level == "B", "batch expansion migration should be reusable for {}", family.id ); if let Some(required_default_mode) = &batch_fixture.assertions.required_default_mode { assert_eq!( batch_report.default_mode.as_deref(), Some(required_default_mode.as_str()), "missing batch expansion default mode {} for {}", required_default_mode, family.id ); } if let Some(expected_pagination_field) = &batch_fixture.assertions.expected_pagination_field { assert_eq!( batch_report .pagination_plan .as_ref() .map(|plan| plan.page_field.as_str()), Some(expected_pagination_field.as_str()), "batch expansion pagination field mismatch for {}", family.id ); } if let Some(required_join_key) = &batch_fixture.assertions.required_join_key { assert!( batch_report .join_keys .iter() .any(|key| key == required_join_key), "missing batch expansion join key {} for {}", required_join_key, family.id ); } if let Some(required_aggregate_rule) = &batch_fixture.assertions.required_aggregate_rule { assert!( batch_report .merge_or_dedupe_rules .iter() .any(|rule| rule == required_aggregate_rule), "missing batch expansion aggregate rule {} for {}", required_aggregate_rule, family.id ); } } } } fn load_manifest() -> P1FamilyManifest { serde_json::from_str( &fs::read_to_string("tests/fixtures/generated_scene/p1_family_manifest.json").unwrap(), ) .unwrap() } 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 }