feat: add generated scene skill platform hardening
This commit is contained in:
285
tests/scene_generator_canonical_test.rs
Normal file
285
tests/scene_generator_canonical_test.rs
Normal file
@@ -0,0 +1,285 @@
|
||||
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<CanonicalTarget>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
#[serde(rename = "requiredWorkflowStepTypes")]
|
||||
required_workflow_step_types: Vec<String>,
|
||||
#[serde(rename = "requiredGateNames")]
|
||||
required_gate_names: Vec<String>,
|
||||
#[serde(rename = "acceptanceChecklist")]
|
||||
acceptance_checklist: Vec<String>,
|
||||
#[serde(rename = "failureTaxonomy")]
|
||||
failure_taxonomy: Vec<String>,
|
||||
}
|
||||
|
||||
#[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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user