456 lines
18 KiB
Rust
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
|
|
}
|