Files
claw/tests/scene_generator_p1_family_test.rs

456 lines
18 KiB
Rust

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<P1FamilySpec>,
}
#[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<String>,
#[serde(rename = "requiredEvidenceTypes")]
required_evidence_types: Vec<String>,
#[serde(rename = "expansionFixtureDir", default)]
expansion_fixture_dir: Option<String>,
#[serde(rename = "expansionSceneId", default)]
expansion_scene_id: Option<String>,
#[serde(rename = "expansionSceneName", default)]
expansion_scene_name: Option<String>,
#[serde(rename = "expansionAssertions", default)]
expansion_assertions: Option<ExpansionAssertions>,
#[serde(rename = "batchCandidateAsset", default)]
batch_candidate_asset: Option<String>,
#[serde(rename = "batchExpansionFixtures", default)]
batch_expansion_fixtures: Vec<BatchExpansionFixture>,
#[serde(rename = "successRateSummary")]
success_rate_summary: String,
#[serde(rename = "failureTaxonomy")]
failure_taxonomy: Vec<String>,
}
#[derive(Debug, Deserialize, Default)]
struct ExpansionAssertions {
#[serde(rename = "requiredDefaultMode", default)]
required_default_mode: Option<String>,
#[serde(rename = "expectedPaginationField", default)]
expected_pagination_field: Option<String>,
#[serde(rename = "requiredJoinKey", default)]
required_join_key: Option<String>,
#[serde(rename = "requiredAggregateRule", default)]
required_aggregate_rule: Option<String>,
#[serde(rename = "requiredMainRequest", default)]
required_main_request: Option<String>,
#[serde(rename = "requiredEnrichmentRequest", default)]
required_enrichment_request: Option<String>,
#[serde(rename = "requiredMergeJoinKey", default)]
required_merge_join_key: Option<String>,
#[serde(rename = "requiredMergeAggregateRule", default)]
required_merge_aggregate_rule: Option<String>,
#[serde(rename = "requiredOutputColumn", default)]
required_output_column: Option<String>,
}
#[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
}