feat: add generated scene skill platform hardening
This commit is contained in:
455
tests/scene_generator_p1_family_test.rs
Normal file
455
tests/scene_generator_p1_family_test.rs
Normal file
@@ -0,0 +1,455 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user