Files
claw/tests/scene_generator_canonical_test.rs

286 lines
9.3 KiB
Rust

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
}