286 lines
9.3 KiB
Rust
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
|
|
}
|