refactor: keep generated scene runtime only in sgclaw
This commit is contained in:
@@ -1,119 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use sgclaw::generated_scene::analyzer::SceneKind;
|
|
||||||
use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest};
|
|
||||||
use sgclaw::generated_scene::ir::{LegacySceneInfoJson, SceneIr};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
if let Err(err) = run() {
|
|
||||||
eprintln!("sg_scene_generate: {err}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run() -> Result<(), String> {
|
|
||||||
let args = parse_args(env::args().skip(1))?;
|
|
||||||
let scene_info: Option<LegacySceneInfoJson> = args
|
|
||||||
.scene_info_json
|
|
||||||
.map(|json| serde_json::from_str(&json))
|
|
||||||
.transpose()
|
|
||||||
.map_err(|e| format!("Invalid scene-info-json: {}", e))?;
|
|
||||||
let scene_ir: Option<SceneIr> = args
|
|
||||||
.scene_ir_json
|
|
||||||
.map(|json| serde_json::from_str(&json))
|
|
||||||
.transpose()
|
|
||||||
.map_err(|e| format!("Invalid scene-ir-json: {}", e))?;
|
|
||||||
let skill_root = generate_scene_package(GenerateSceneRequest {
|
|
||||||
source_dir: args.source_dir,
|
|
||||||
scene_id: args.scene_id,
|
|
||||||
scene_name: args.scene_name,
|
|
||||||
scene_kind: args.scene_kind,
|
|
||||||
target_url: args.target_url,
|
|
||||||
output_root: args.output_root,
|
|
||||||
lessons_path: args.lessons_path,
|
|
||||||
scene_info_json: scene_info,
|
|
||||||
scene_ir_json: scene_ir,
|
|
||||||
})
|
|
||||||
.map_err(|err| err.to_string())?;
|
|
||||||
|
|
||||||
println!("generated scene package: {}", skill_root.display());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CliArgs {
|
|
||||||
source_dir: PathBuf,
|
|
||||||
scene_id: String,
|
|
||||||
scene_name: String,
|
|
||||||
scene_kind: Option<SceneKind>,
|
|
||||||
target_url: Option<String>,
|
|
||||||
output_root: PathBuf,
|
|
||||||
lessons_path: Option<PathBuf>,
|
|
||||||
scene_info_json: Option<String>,
|
|
||||||
scene_ir_json: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
|
||||||
let mut source_dir = None;
|
|
||||||
let mut scene_id = None;
|
|
||||||
let mut scene_name = None;
|
|
||||||
let mut scene_kind = None;
|
|
||||||
let mut target_url = None;
|
|
||||||
let mut output_root = None;
|
|
||||||
let mut lessons_path = None;
|
|
||||||
let mut scene_info_json = None;
|
|
||||||
let mut scene_ir_json = None;
|
|
||||||
let mut pending_flag: Option<String> = None;
|
|
||||||
|
|
||||||
for arg in args {
|
|
||||||
if let Some(flag) = pending_flag.take() {
|
|
||||||
match flag.as_str() {
|
|
||||||
"--source-dir" => source_dir = Some(PathBuf::from(arg)),
|
|
||||||
"--scene-id" => scene_id = Some(arg),
|
|
||||||
"--scene-name" => scene_name = Some(arg),
|
|
||||||
"--scene-kind" => {
|
|
||||||
scene_kind = Some(
|
|
||||||
SceneKind::from_str(&arg)
|
|
||||||
.ok_or_else(|| format!("invalid scene kind: {}", arg))?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
"--target-url" => target_url = Some(arg),
|
|
||||||
"--output-root" => output_root = Some(PathBuf::from(arg)),
|
|
||||||
"--lessons" => lessons_path = Some(PathBuf::from(arg)),
|
|
||||||
"--scene-info-json" => scene_info_json = Some(arg),
|
|
||||||
"--scene-ir-json" => scene_ir_json = Some(arg),
|
|
||||||
_ => return Err(format!("unsupported argument {flag}")),
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match arg.as_str() {
|
|
||||||
"--source-dir" | "--scene-id" | "--scene-name" | "--scene-kind" | "--target-url"
|
|
||||||
| "--output-root" | "--lessons" | "--scene-info-json" | "--scene-ir-json" => {
|
|
||||||
pending_flag = Some(arg);
|
|
||||||
}
|
|
||||||
"--help" | "-h" => return Err(usage()),
|
|
||||||
_ => return Err(format!("unsupported argument {arg}\n{}", usage())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(flag) = pending_flag {
|
|
||||||
return Err(format!("missing value for {flag}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(CliArgs {
|
|
||||||
source_dir: source_dir.ok_or_else(usage)?,
|
|
||||||
scene_id: scene_id.ok_or_else(usage)?,
|
|
||||||
scene_name: scene_name.ok_or_else(usage)?,
|
|
||||||
scene_kind,
|
|
||||||
target_url,
|
|
||||||
output_root: output_root.ok_or_else(usage)?,
|
|
||||||
lessons_path,
|
|
||||||
scene_info_json,
|
|
||||||
scene_ir_json,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn usage() -> String {
|
|
||||||
"usage: sg_scene_generate --source-dir <scenario-dir> --scene-id <scene-id> --scene-name <display-name> [--scene-kind <report_collection|monitoring>] [--target-url <url>] --output-root <skill-staging-root> [--lessons <lessons-toml>] [--scene-info-json '<json>'] [--scene-ir-json '<json>']".to_string()
|
|
||||||
}
|
|
||||||
@@ -1,5 +1 @@
|
|||||||
pub mod analyzer;
|
|
||||||
pub mod generator;
|
|
||||||
pub mod ir;
|
|
||||||
pub mod lessons;
|
|
||||||
pub mod scheduled_monitoring_runtime;
|
pub mod scheduled_monitoring_runtime;
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
use sgclaw::generated_scene::lessons::{
|
|
||||||
load_generation_lessons, GenerationLessons, BUILTIN_REPORT_COLLECTION_LESSONS,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn builtin_report_collection_lessons_match_required_generator_rules() {
|
|
||||||
let lessons = GenerationLessons::default_report_collection();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
BUILTIN_REPORT_COLLECTION_LESSONS,
|
|
||||||
"builtin:report_collection_v1"
|
|
||||||
);
|
|
||||||
assert!(lessons.routing.require_exact_suffix);
|
|
||||||
assert!(lessons.routing.unsupported_scene_fail_closed);
|
|
||||||
assert!(lessons.canonical_params.require_explicit_period);
|
|
||||||
assert!(lessons.bootstrap.require_expected_domain);
|
|
||||||
assert!(lessons.bootstrap.require_target_url);
|
|
||||||
assert!(lessons.artifact.require_report_artifact);
|
|
||||||
assert!(lessons.validation.require_pipe_and_ws_checks);
|
|
||||||
assert!(lessons.validation.require_manual_service_console_smoke);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lineloss_lessons_toml_declares_required_generator_rules() {
|
|
||||||
let lessons =
|
|
||||||
load_generation_lessons("docs/superpowers/references/tq-lineloss-lessons-learned.toml")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(lessons.routing.require_exact_suffix);
|
|
||||||
assert!(lessons.routing.unsupported_scene_fail_closed);
|
|
||||||
assert!(lessons.canonical_params.require_explicit_period);
|
|
||||||
assert!(lessons.bootstrap.require_expected_domain);
|
|
||||||
assert!(lessons.bootstrap.require_target_url);
|
|
||||||
assert!(lessons.artifact.require_report_artifact);
|
|
||||||
assert!(lessons.validation.require_pipe_and_ws_checks);
|
|
||||||
assert!(lessons.validation.require_manual_service_console_smoke);
|
|
||||||
}
|
|
||||||
42
tests/generated_scene_runtime_boundary_test.rs
Normal file
42
tests/generated_scene_runtime_boundary_test.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn repo_root() -> &'static Path {
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgclaw_no_longer_ships_generated_scene_binary_entrypoint() {
|
||||||
|
let binary_entrypoint = repo_root().join("src/bin/sg_scene_generate.rs");
|
||||||
|
assert!(
|
||||||
|
!binary_entrypoint.exists(),
|
||||||
|
"sgclaw should not keep sg_scene_generate.rs once generated_scene_core owns generation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_scene_module_only_exports_runtime_adapter() {
|
||||||
|
let module_source =
|
||||||
|
fs::read_to_string(repo_root().join("src/generated_scene/mod.rs")).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
module_source.contains("pub mod scheduled_monitoring_runtime;"),
|
||||||
|
"generated_scene runtime adapter must remain exported"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!module_source.contains("pub mod analyzer;"),
|
||||||
|
"generated_scene analyzer export must move out of sgclaw"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!module_source.contains("pub mod generator;"),
|
||||||
|
"generated_scene generator export must move out of sgclaw"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!module_source.contains("pub mod ir;"),
|
||||||
|
"generated_scene ir export must move out of sgclaw"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!module_source.contains("pub mod lessons;"),
|
||||||
|
"generated_scene lessons export must move out of sgclaw"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
const assert = require("assert");
|
|
||||||
const fs = require("fs");
|
|
||||||
const os = require("os");
|
|
||||||
const path = require("path");
|
|
||||||
const {
|
|
||||||
buildAnalyzePrompt,
|
|
||||||
extractJsonFromResponse,
|
|
||||||
isRetryableLlmError,
|
|
||||||
repairCommonJsonIssues,
|
|
||||||
} = require("../frontend/scene-generator/llm-client");
|
|
||||||
const {
|
|
||||||
buildDeterministicSceneIr,
|
|
||||||
readDirectory,
|
|
||||||
validateSceneIdCandidate,
|
|
||||||
} = require("../frontend/scene-generator/generator-runner");
|
|
||||||
const {
|
|
||||||
getGenerationBlockers,
|
|
||||||
mergeSceneIr,
|
|
||||||
sanitizeSceneIr,
|
|
||||||
} = require("../frontend/scene-generator/server");
|
|
||||||
|
|
||||||
function testBuildAnalyzePromptIncludesFileContents() {
|
|
||||||
const dirContents = {
|
|
||||||
"scene.toml": '[scene]\nid = "test-scene"',
|
|
||||||
scripts: { "collect_test.js": "async function main() {}" },
|
|
||||||
tree: "├── scene.toml\n└── collect_test.js",
|
|
||||||
};
|
|
||||||
|
|
||||||
const prompt = buildAnalyzePrompt("D:/test/scenario", dirContents);
|
|
||||||
|
|
||||||
assert.ok(prompt.includes("scene.toml"), "should include scene.toml");
|
|
||||||
assert.ok(prompt.includes("collect_test.js"), "should include script name");
|
|
||||||
assert.ok(prompt.includes("D:/test/scenario"), "should include sourceDir");
|
|
||||||
console.log("PASS: testBuildAnalyzePromptIncludesFileContents");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testExtractJsonFromResponse() {
|
|
||||||
const withMarkdown =
|
|
||||||
'```json\n{"sceneId": "test", "sceneName": "测试"}\n```';
|
|
||||||
const plain = '{"sceneId": "test", "sceneName": "测试"}';
|
|
||||||
const withPrefix =
|
|
||||||
'Here is the result:\n{"sceneId": "test", "sceneName": "测试"}';
|
|
||||||
|
|
||||||
assert.deepStrictEqual(extractJsonFromResponse(withMarkdown), {
|
|
||||||
sceneId: "test",
|
|
||||||
sceneName: "测试",
|
|
||||||
});
|
|
||||||
assert.deepStrictEqual(extractJsonFromResponse(plain), {
|
|
||||||
sceneId: "test",
|
|
||||||
sceneName: "测试",
|
|
||||||
});
|
|
||||||
assert.deepStrictEqual(extractJsonFromResponse(withPrefix), {
|
|
||||||
sceneId: "test",
|
|
||||||
sceneName: "测试",
|
|
||||||
});
|
|
||||||
console.log("PASS: testExtractJsonFromResponse");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testExtractJsonFromResponseRepairsMissingArrayComma() {
|
|
||||||
const malformed =
|
|
||||||
'{"sceneId":"marketing-zero-consumer-report","evidence":[{"kind":"a"} {"kind":"b"}],"sceneName":"营销"}';
|
|
||||||
|
|
||||||
const result = extractJsonFromResponse(malformed);
|
|
||||||
|
|
||||||
assert.strictEqual(result.sceneId, "marketing-zero-consumer-report");
|
|
||||||
assert.strictEqual(Array.isArray(result.evidence), true);
|
|
||||||
assert.strictEqual(result.evidence.length, 2);
|
|
||||||
console.log("PASS: testExtractJsonFromResponseRepairsMissingArrayComma");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testRepairCommonJsonIssuesRemovesTrailingCommas() {
|
|
||||||
const malformed =
|
|
||||||
'{\n "sceneId": "marketing-zero-consumer-report",\n "evidence": [{"kind":"a",},],\n}';
|
|
||||||
const repaired = repairCommonJsonIssues(malformed);
|
|
||||||
const parsed = JSON.parse(repaired);
|
|
||||||
|
|
||||||
assert.strictEqual(parsed.sceneId, "marketing-zero-consumer-report");
|
|
||||||
assert.strictEqual(parsed.evidence.length, 1);
|
|
||||||
console.log("PASS: testRepairCommonJsonIssuesRemovesTrailingCommas");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testIsRetryableLlmErrorRecognizesTimeouts() {
|
|
||||||
assert.strictEqual(isRetryableLlmError(new Error("LLM API request timed out")), true);
|
|
||||||
assert.strictEqual(isRetryableLlmError(new Error("LLM API error 503: upstream unavailable")), true);
|
|
||||||
assert.strictEqual(isRetryableLlmError(new Error("LLM response missing sceneId")), false);
|
|
||||||
console.log("PASS: testIsRetryableLlmErrorRecognizesTimeouts");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testDeterministicNamingAvoidsDegenerateSlugFallback() {
|
|
||||||
const sceneIr = buildDeterministicSceneIr(
|
|
||||||
{ deterministicSignals: {} },
|
|
||||||
"D:/tmp/营销2.0零度户报表数据生成"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(sceneIr.sceneId, "marketing-zero-consumer-report");
|
|
||||||
assert.strictEqual(sceneIr.sceneIdDiagnostics.valid, true);
|
|
||||||
assert.strictEqual(sceneIr.sceneIdDiagnostics.candidateSource, "deterministic_keywords");
|
|
||||||
console.log("PASS: testDeterministicNamingAvoidsDegenerateSlugFallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testValidateSceneIdCandidateRejectsLowEntropyIds() {
|
|
||||||
const invalid = validateSceneIdCandidate("2-0", {
|
|
||||||
sceneName: "营销2.0零度户报表数据生成",
|
|
||||||
sourceDir: "D:/tmp/营销2.0零度户报表数据生成",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.strictEqual(invalid.valid, false);
|
|
||||||
assert.ok(
|
|
||||||
["numeric_only_scene_id", "numeric_dominant_scene_id", "scene_id_too_short"].includes(invalid.reason),
|
|
||||||
`unexpected invalid reason: ${invalid.reason}`
|
|
||||||
);
|
|
||||||
console.log("PASS: testValidateSceneIdCandidateRejectsLowEntropyIds");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testMergeSceneIrPrefersValidSceneIdOverInvalidLlmValue() {
|
|
||||||
const deterministic = sanitizeSceneIr({
|
|
||||||
sceneId: "marketing-zero-consumer-report",
|
|
||||||
sceneIdDiagnostics: {
|
|
||||||
candidateSource: "deterministic_keywords",
|
|
||||||
valid: true,
|
|
||||||
candidates: [{ value: "marketing-zero-consumer-report", source: "deterministic_keywords", valid: true }],
|
|
||||||
},
|
|
||||||
sceneName: "营销2.0零度户报表数据生成",
|
|
||||||
bootstrap: { expectedDomain: "yx.gs.sgcc.com.cn", targetUrl: "http://yx.gs.sgcc.com.cn" },
|
|
||||||
workflowSteps: [{ type: "request" }],
|
|
||||||
apiEndpoints: [{ name: "userList", url: "http://yx.gs.sgcc.com.cn/list", method: "POST" }],
|
|
||||||
validationHints: { runtimeCompatible: true },
|
|
||||||
readiness: { level: "B" },
|
|
||||||
});
|
|
||||||
const llm = sanitizeSceneIr({
|
|
||||||
sceneId: "2-0",
|
|
||||||
sceneIdDiagnostics: {
|
|
||||||
candidateSource: "llm_semantic",
|
|
||||||
valid: false,
|
|
||||||
invalidReason: "numeric_dominant_scene_id",
|
|
||||||
candidates: [{ value: "2-0", source: "llm_semantic", valid: false, reason: "numeric_dominant_scene_id" }],
|
|
||||||
},
|
|
||||||
sceneName: "营销2.0零度户报表数据生成",
|
|
||||||
bootstrap: { expectedDomain: "yx.gs.sgcc.com.cn", targetUrl: "http://yx.gs.sgcc.com.cn" },
|
|
||||||
workflowSteps: [{ type: "request" }],
|
|
||||||
apiEndpoints: [{ name: "userList", url: "http://yx.gs.sgcc.com.cn/list", method: "POST" }],
|
|
||||||
validationHints: { runtimeCompatible: true },
|
|
||||||
readiness: { level: "B" },
|
|
||||||
});
|
|
||||||
const warnings = [];
|
|
||||||
|
|
||||||
const merged = mergeSceneIr(deterministic, llm, warnings);
|
|
||||||
|
|
||||||
assert.strictEqual(merged.sceneId, "marketing-zero-consumer-report");
|
|
||||||
assert.strictEqual(merged.sceneIdDiagnostics.valid, true);
|
|
||||||
assert.ok(warnings.some((item) => item.includes("SceneId conflict")));
|
|
||||||
console.log("PASS: testMergeSceneIrPrefersValidSceneIdOverInvalidLlmValue");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testGetGenerationBlockersRejectsInvalidSceneId() {
|
|
||||||
const blockers = getGenerationBlockers({
|
|
||||||
sceneIr: {
|
|
||||||
sceneIdDiagnostics: {
|
|
||||||
valid: false,
|
|
||||||
invalidReason: "numeric_dominant_scene_id",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sceneId: "2-0",
|
|
||||||
sceneName: "营销2.0零度户报表数据生成",
|
|
||||||
sourceDir: "D:/tmp/营销2.0零度户报表数据生成",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
blockers.some((item) => item.startsWith("invalid_scene_id:")),
|
|
||||||
`expected invalid_scene_id blocker, got ${JSON.stringify(blockers)}`
|
|
||||||
);
|
|
||||||
assert.ok(blockers.includes("analysis_invalid_scene_id:numeric_dominant_scene_id"));
|
|
||||||
console.log("PASS: testGetGenerationBlockersRejectsInvalidSceneId");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testBootstrapPrefersBusinessEntryOverLocalhostExport() {
|
|
||||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sgclaw-bootstrap-"));
|
|
||||||
const sceneDir = path.join(tempRoot, "bootstrap");
|
|
||||||
fs.mkdirSync(sceneDir);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(sceneDir, "index.html"),
|
|
||||||
`<!doctype html><html><body><script>
|
|
||||||
const sourceUrl = "http://yx.gs.sgcc.com.cn";
|
|
||||||
const apiUrl = "http://yxgateway.gs.sgcc.com.cn/api";
|
|
||||||
function getRows() {
|
|
||||||
return $.ajax({ url: "http://yxgateway.gs.sgcc.com.cn/marketing/userList", type: "POST" });
|
|
||||||
}
|
|
||||||
function exportExcel() {
|
|
||||||
return $.ajax({ url: "http://localhost:13313/SurfaceServices/personalBread/export/faultDetailsExportXLSX", type: "POST" });
|
|
||||||
}
|
|
||||||
</script></body></html>`,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
const sceneIr = readDirectory(sceneDir).deterministic;
|
|
||||||
|
|
||||||
assert.strictEqual(sceneIr.bootstrap.expectedDomain, "yx.gs.sgcc.com.cn");
|
|
||||||
assert.strictEqual(sceneIr.bootstrap.targetUrl, "http://yx.gs.sgcc.com.cn/");
|
|
||||||
console.log("PASS: testBootstrapPrefersBusinessEntryOverLocalhostExport");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testBootstrapBecomesUnresolvedWhenOnlyLocalhostExists() {
|
|
||||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sgclaw-bootstrap-local-"));
|
|
||||||
const sceneDir = path.join(tempRoot, "bootstrap-local");
|
|
||||||
fs.mkdirSync(sceneDir);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(sceneDir, "index.html"),
|
|
||||||
`<!doctype html><html><body><script>
|
|
||||||
function exportExcel() {
|
|
||||||
return $.ajax({ url: "http://localhost:13313/SurfaceServices/personalBread/export/faultDetailsExportXLSX", type: "POST" });
|
|
||||||
}
|
|
||||||
</script></body></html>`,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
const sceneIr = readDirectory(sceneDir).deterministic;
|
|
||||||
|
|
||||||
assert.strictEqual(sceneIr.bootstrap.expectedDomain, "");
|
|
||||||
assert.strictEqual(sceneIr.bootstrap.targetUrl, "");
|
|
||||||
assert.ok(sceneIr.readiness.missingPieces.includes("bootstrap_target"));
|
|
||||||
console.log("PASS: testBootstrapBecomesUnresolvedWhenOnlyLocalhostExists");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testWorkflowClassificationPrefersPaginatedOverGenericModeNoise() {
|
|
||||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sgclaw-workflow-"));
|
|
||||||
const sceneDir = path.join(tempRoot, "workflow");
|
|
||||||
fs.mkdirSync(sceneDir);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(sceneDir, "index.html"),
|
|
||||||
`<!doctype html><html><body><script>
|
|
||||||
const type = "list";
|
|
||||||
const status = "ready";
|
|
||||||
async function loadData(page, pageSize) {
|
|
||||||
return $.ajax({ url: "http://yx.gs.sgcc.com.cn/marketing/userList", type: "POST", data: JSON.stringify({ page, pageSize }) });
|
|
||||||
}
|
|
||||||
async function getChargeInfo(custNo) {
|
|
||||||
return $.ajax({ url: "http://yx.gs.sgcc.com.cn/marketing/userCharges", type: "POST", data: JSON.stringify({ custNo }) });
|
|
||||||
}
|
|
||||||
function exportExcel(rows) { return rows.length; }
|
|
||||||
function run(rows) {
|
|
||||||
return rows.filter((row) => row.charge !== 0);
|
|
||||||
}
|
|
||||||
</script></body></html>`,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
const sceneIr = readDirectory(sceneDir).deterministic;
|
|
||||||
|
|
||||||
assert.strictEqual(sceneIr.workflowArchetype, "paginated_enrichment");
|
|
||||||
assert.ok(sceneIr.workflowEvidence.paginationFields.length > 0);
|
|
||||||
assert.ok(sceneIr.workflowEvidence.secondaryRequestEntries.length > 0);
|
|
||||||
assert.ok(sceneIr.workflowEvidence.postProcessSteps.length > 0);
|
|
||||||
console.log("PASS: testWorkflowClassificationPrefersPaginatedOverGenericModeNoise");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testWorkflowClassificationDoesNotEmitPaginatedWithoutPostProcess() {
|
|
||||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sgclaw-workflow-no-post-"));
|
|
||||||
const sceneDir = path.join(tempRoot, "workflow-no-post");
|
|
||||||
fs.mkdirSync(sceneDir);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(sceneDir, "index.html"),
|
|
||||||
`<!doctype html><html><body><script>
|
|
||||||
async function loadData(page, pageSize) {
|
|
||||||
return $.ajax({ url: "http://yx.gs.sgcc.com.cn/marketing/userList", type: "POST", data: JSON.stringify({ page, pageSize }) });
|
|
||||||
}
|
|
||||||
async function getChargeInfo(custNo) {
|
|
||||||
return $.ajax({ url: "http://yx.gs.sgcc.com.cn/marketing/userCharges", type: "POST", data: JSON.stringify({ custNo }) });
|
|
||||||
}
|
|
||||||
</script></body></html>`,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
const sceneIr = readDirectory(sceneDir).deterministic;
|
|
||||||
|
|
||||||
assert.notStrictEqual(sceneIr.workflowArchetype, "paginated_enrichment");
|
|
||||||
console.log("PASS: testWorkflowClassificationDoesNotEmitPaginatedWithoutPostProcess");
|
|
||||||
}
|
|
||||||
|
|
||||||
function testGenerationBlockersIncludeFailedReadinessGates() {
|
|
||||||
const blockers = getGenerationBlockers({
|
|
||||||
sceneIr: {
|
|
||||||
readiness: {
|
|
||||||
gates: [
|
|
||||||
{ name: "bootstrap_resolved", passed: false, reason: "bootstrap_target" },
|
|
||||||
{ name: "request_contract_complete", passed: false, reason: "request_endpoint" },
|
|
||||||
{ name: "response_contract_complete", passed: false, reason: "response_path" },
|
|
||||||
{ name: "workflow_contract_complete", passed: false, reason: "post_process" },
|
|
||||||
{ name: "workflow_complete_for_archetype", passed: false, reason: "post_process" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sceneId: "marketing-zero-consumer-report",
|
|
||||||
sceneName: "营销2.0零度户报表数据生成",
|
|
||||||
sourceDir: "D:/tmp/营销2.0零度户报表数据生成",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.ok(blockers.includes("gate_failed:bootstrap_resolved:bootstrap_target"));
|
|
||||||
assert.ok(blockers.includes("gate_failed:request_contract_complete:request_endpoint"));
|
|
||||||
assert.ok(blockers.includes("gate_failed:response_contract_complete:response_path"));
|
|
||||||
assert.ok(blockers.includes("gate_failed:workflow_contract_complete:post_process"));
|
|
||||||
assert.ok(blockers.includes("gate_failed:workflow_complete_for_archetype:post_process"));
|
|
||||||
console.log("PASS: testGenerationBlockersIncludeFailedReadinessGates");
|
|
||||||
}
|
|
||||||
|
|
||||||
testBuildAnalyzePromptIncludesFileContents();
|
|
||||||
testExtractJsonFromResponse();
|
|
||||||
testExtractJsonFromResponseRepairsMissingArrayComma();
|
|
||||||
testRepairCommonJsonIssuesRemovesTrailingCommas();
|
|
||||||
testIsRetryableLlmErrorRecognizesTimeouts();
|
|
||||||
testDeterministicNamingAvoidsDegenerateSlugFallback();
|
|
||||||
testValidateSceneIdCandidateRejectsLowEntropyIds();
|
|
||||||
testMergeSceneIrPrefersValidSceneIdOverInvalidLlmValue();
|
|
||||||
testGetGenerationBlockersRejectsInvalidSceneId();
|
|
||||||
testBootstrapPrefersBusinessEntryOverLocalhostExport();
|
|
||||||
testBootstrapBecomesUnresolvedWhenOnlyLocalhostExists();
|
|
||||||
testWorkflowClassificationPrefersPaginatedOverGenericModeNoise();
|
|
||||||
testWorkflowClassificationDoesNotEmitPaginatedWithoutPostProcess();
|
|
||||||
testGenerationBlockersIncludeFailedReadinessGates();
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use sgclaw::generated_scene::analyzer::SceneKind;
|
|
||||||
use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest};
|
|
||||||
use sgclaw::generated_scene::ir::{
|
|
||||||
ApiEndpointIr, ModeConditionIr, ModeIr, NormalizeRulesIr, SceneIdDiagnosticsIr, SceneIr,
|
|
||||||
WorkflowArchetype,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn make_test_mode(
|
|
||||||
name: &str,
|
|
||||||
url: &str,
|
|
||||||
content_type: Option<&str>,
|
|
||||||
response_path: &str,
|
|
||||||
) -> ModeIr {
|
|
||||||
ModeIr {
|
|
||||||
name: name.to_string(),
|
|
||||||
label: Some(name.to_string()),
|
|
||||||
condition: Some(ModeConditionIr {
|
|
||||||
field: "period_mode".to_string(),
|
|
||||||
operator: "equals".to_string(),
|
|
||||||
value: serde_json::Value::String(name.to_string()),
|
|
||||||
}),
|
|
||||||
api_endpoint: Some(ApiEndpointIr {
|
|
||||||
name: format!("{}_endpoint", name),
|
|
||||||
url: url.to_string(),
|
|
||||||
method: "POST".to_string(),
|
|
||||||
content_type: content_type.map(|s| s.to_string()),
|
|
||||||
description: None,
|
|
||||||
}),
|
|
||||||
column_defs: vec![("id".to_string(), "ID".to_string())],
|
|
||||||
request_template: serde_json::json!({ "mode": name }),
|
|
||||||
request_field_mappings: Vec::new(),
|
|
||||||
normalize_rules: Some(NormalizeRulesIr {
|
|
||||||
rules_type: "validate_required".to_string(),
|
|
||||||
required_fields: vec!["id".to_string()],
|
|
||||||
filter_null: true,
|
|
||||||
}),
|
|
||||||
response_path: response_path.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_test_scene_ir(modes: Vec<ModeIr>) -> SceneIr {
|
|
||||||
let is_multi = modes.len() > 1;
|
|
||||||
let api_endpoints = modes
|
|
||||||
.iter()
|
|
||||||
.filter_map(|mode| mode.api_endpoint.clone())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
SceneIr {
|
|
||||||
scene_id: "test-scene".to_string(),
|
|
||||||
scene_id_diagnostics: SceneIdDiagnosticsIr::default(),
|
|
||||||
scene_name: "Test Scene".to_string(),
|
|
||||||
scene_kind: "report_collection".to_string(),
|
|
||||||
workflow_archetype: Some(if is_multi {
|
|
||||||
WorkflowArchetype::MultiModeRequest
|
|
||||||
} else {
|
|
||||||
WorkflowArchetype::SingleRequestTable
|
|
||||||
}),
|
|
||||||
bootstrap: Default::default(),
|
|
||||||
params: Vec::new(),
|
|
||||||
modes,
|
|
||||||
default_mode: Some("month".to_string()),
|
|
||||||
mode_switch_field: Some("period_mode".to_string()),
|
|
||||||
workflow_steps: vec![
|
|
||||||
sgclaw::generated_scene::ir::WorkflowStepIr {
|
|
||||||
step_type: "request".to_string(),
|
|
||||||
description: Some("select mode and query corresponding endpoint".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
sgclaw::generated_scene::ir::WorkflowStepIr {
|
|
||||||
step_type: "transform".to_string(),
|
|
||||||
description: Some("normalize mode-specific table rows".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
],
|
|
||||||
workflow_evidence: Default::default(),
|
|
||||||
main_request: None,
|
|
||||||
pagination_plan: None,
|
|
||||||
enrichment_requests: Vec::new(),
|
|
||||||
join_keys: Vec::new(),
|
|
||||||
merge_or_dedupe_rules: Vec::new(),
|
|
||||||
export_plan: None,
|
|
||||||
merge_plan: None,
|
|
||||||
request_template: serde_json::Value::Null,
|
|
||||||
response_path: "".to_string(),
|
|
||||||
normalize_rules: None,
|
|
||||||
artifact_contract: Default::default(),
|
|
||||||
validation_hints: Default::default(),
|
|
||||||
evidence: Vec::new(),
|
|
||||||
readiness: Default::default(),
|
|
||||||
api_endpoints,
|
|
||||||
runtime_dependencies: Vec::new(),
|
|
||||||
static_params: Default::default(),
|
|
||||||
column_defs: Vec::new(),
|
|
||||||
confidence: 0.0,
|
|
||||||
uncertainties: Vec::new(),
|
|
||||||
monitoring_action_workflow: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test 1: Single request table uses dedicated simple-request path instead of MODES fallback.
|
|
||||||
#[test]
|
|
||||||
fn test_single_request_table_uses_dedicated_path() {
|
|
||||||
let output_root = temp_workspace("sgclaw-single-mode-test");
|
|
||||||
let modes = vec![make_test_mode(
|
|
||||||
"month",
|
|
||||||
"http://example.com/api/month",
|
|
||||||
None,
|
|
||||||
"data",
|
|
||||||
)];
|
|
||||||
let scene_ir = make_test_scene_ir(modes);
|
|
||||||
|
|
||||||
// Use SingleRequestTable archetype - the compile path should stay on the dedicated single-request route.
|
|
||||||
let mut scene_ir = scene_ir;
|
|
||||||
scene_ir.workflow_archetype = Some(WorkflowArchetype::SingleRequestTable);
|
|
||||||
scene_ir.api_endpoints = vec![ApiEndpointIr {
|
|
||||||
name: "default_endpoint".to_string(),
|
|
||||||
url: "http://example.com/api/data".to_string(),
|
|
||||||
method: "POST".to_string(),
|
|
||||||
content_type: None,
|
|
||||||
description: None,
|
|
||||||
}];
|
|
||||||
|
|
||||||
generate_scene_package(GenerateSceneRequest {
|
|
||||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
|
||||||
scene_id: "single-mode-scene".to_string(),
|
|
||||||
scene_name: "Single Mode Scene".to_string(),
|
|
||||||
scene_kind: Some(SceneKind::ReportCollection),
|
|
||||||
target_url: None,
|
|
||||||
output_root: output_root.clone(),
|
|
||||||
lessons_path: None,
|
|
||||||
scene_info_json: None,
|
|
||||||
scene_ir_json: Some(scene_ir),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let skill_root = output_root.join("skills/single-mode-scene");
|
|
||||||
let generated_script =
|
|
||||||
fs::read_to_string(skill_root.join("scripts/collect_single_mode_scene.js")).unwrap();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
generated_script.contains("const REQUEST_TEMPLATE ="),
|
|
||||||
"Generated JS should contain REQUEST_TEMPLATE on the dedicated single-request path"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!generated_script.contains("const MODES ="),
|
|
||||||
"Generated JS should no longer route SingleRequestTable through MODES fallback"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test 2: Multi-mode generates mode routing (detectMode and MODES.find)
|
|
||||||
#[test]
|
|
||||||
fn test_multi_mode_generates_mode_routing() {
|
|
||||||
let output_root = temp_workspace("sgclaw-multi-mode-test");
|
|
||||||
let modes = vec![
|
|
||||||
make_test_mode("month", "http://example.com/api/month", None, "data"),
|
|
||||||
make_test_mode("week", "http://example.com/api/week", None, "data"),
|
|
||||||
];
|
|
||||||
let scene_ir = make_test_scene_ir(modes);
|
|
||||||
|
|
||||||
generate_scene_package(GenerateSceneRequest {
|
|
||||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
|
||||||
scene_id: "multi-mode-scene".to_string(),
|
|
||||||
scene_name: "Multi Mode Scene".to_string(),
|
|
||||||
scene_kind: Some(SceneKind::ReportCollection),
|
|
||||||
target_url: None,
|
|
||||||
output_root: output_root.clone(),
|
|
||||||
lessons_path: None,
|
|
||||||
scene_info_json: None,
|
|
||||||
scene_ir_json: Some(scene_ir),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let skill_root = output_root.join("skills/multi-mode-scene");
|
|
||||||
let generated_script =
|
|
||||||
fs::read_to_string(skill_root.join("scripts/collect_multi_mode_scene.js")).unwrap();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
generated_script.contains("function detectMode"),
|
|
||||||
"Generated JS should contain 'detectMode' function for multi-mode routing"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
generated_script.contains("MODES.find"),
|
|
||||||
"Generated JS should contain 'MODES.find' for mode selection"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test 3: Form-urlencoded request body uses Object.entries().join('&') not JSON.stringify
|
|
||||||
#[test]
|
|
||||||
fn test_form_urlencoded_request_body() {
|
|
||||||
let output_root = temp_workspace("sgclaw-form-urlencoded-test");
|
|
||||||
let modes = vec![make_test_mode(
|
|
||||||
"month",
|
|
||||||
"http://example.com/api/month",
|
|
||||||
Some("application/x-www-form-urlencoded"),
|
|
||||||
"data",
|
|
||||||
)];
|
|
||||||
let mut scene_ir = make_test_scene_ir(modes);
|
|
||||||
scene_ir.workflow_archetype = Some(WorkflowArchetype::MultiModeRequest);
|
|
||||||
|
|
||||||
generate_scene_package(GenerateSceneRequest {
|
|
||||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
|
||||||
scene_id: "form-urlencoded-scene".to_string(),
|
|
||||||
scene_name: "Form URL Encoded Scene".to_string(),
|
|
||||||
scene_kind: Some(SceneKind::ReportCollection),
|
|
||||||
target_url: None,
|
|
||||||
output_root: output_root.clone(),
|
|
||||||
lessons_path: None,
|
|
||||||
scene_info_json: None,
|
|
||||||
scene_ir_json: Some(scene_ir),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let skill_root = output_root.join("skills/form-urlencoded-scene");
|
|
||||||
let generated_script =
|
|
||||||
fs::read_to_string(skill_root.join("scripts/collect_form_urlencoded_scene.js")).unwrap();
|
|
||||||
|
|
||||||
// The buildModeRequest function should use Object.entries for form-urlencoded
|
|
||||||
assert!(
|
|
||||||
generated_script.contains("Object.entries(requestBody)"),
|
|
||||||
"Generated JS should use Object.entries for form-urlencoded body encoding"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
generated_script.contains(".join('&')"),
|
|
||||||
"Generated JS should join form-urlencoded entries with '&'"
|
|
||||||
);
|
|
||||||
// Verify the conditional exists in buildModeRequest
|
|
||||||
assert!(
|
|
||||||
generated_script.contains("application/x-www-form-urlencoded"),
|
|
||||||
"Generated JS should reference form-urlencoded content type"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test 4: Response path extraction uses mode.responsePath in the template
|
|
||||||
#[test]
|
|
||||||
fn test_response_path_extraction_in_template() {
|
|
||||||
let output_root = temp_workspace("sgclaw-response-path-test");
|
|
||||||
let modes = vec![make_test_mode(
|
|
||||||
"month",
|
|
||||||
"http://example.com/api/month",
|
|
||||||
None,
|
|
||||||
"data.list",
|
|
||||||
)];
|
|
||||||
let mut scene_ir = make_test_scene_ir(modes);
|
|
||||||
scene_ir.workflow_archetype = Some(WorkflowArchetype::MultiModeRequest);
|
|
||||||
|
|
||||||
generate_scene_package(GenerateSceneRequest {
|
|
||||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
|
||||||
scene_id: "response-path-scene".to_string(),
|
|
||||||
scene_name: "Response Path Scene".to_string(),
|
|
||||||
scene_kind: Some(SceneKind::ReportCollection),
|
|
||||||
target_url: None,
|
|
||||||
output_root: output_root.clone(),
|
|
||||||
lessons_path: None,
|
|
||||||
scene_info_json: None,
|
|
||||||
scene_ir_json: Some(scene_ir),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let skill_root = output_root.join("skills/response-path-scene");
|
|
||||||
let generated_script =
|
|
||||||
fs::read_to_string(skill_root.join("scripts/collect_response_path_scene.js")).unwrap();
|
|
||||||
|
|
||||||
// The multi-mode template uses mode.responsePath for response extraction
|
|
||||||
assert!(
|
|
||||||
generated_script.contains("mode.responsePath"),
|
|
||||||
"Generated JS should use 'mode.responsePath' for per-mode response extraction"
|
|
||||||
);
|
|
||||||
// The safeGet call should reference the mode's responsePath
|
|
||||||
assert!(
|
|
||||||
generated_script.contains("safeGet(raw, mode.responsePath"),
|
|
||||||
"Generated JS should call safeGet with mode.responsePath"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test 5: processData flag in $.ajax call with correct conditional
|
|
||||||
#[test]
|
|
||||||
fn test_process_data_flag_in_ajax() {
|
|
||||||
let output_root = temp_workspace("sgclaw-process-data-test");
|
|
||||||
let modes = vec![make_test_mode(
|
|
||||||
"month",
|
|
||||||
"http://example.com/api/month",
|
|
||||||
Some("application/x-www-form-urlencoded"),
|
|
||||||
"data",
|
|
||||||
)];
|
|
||||||
let scene_ir = make_test_scene_ir(modes);
|
|
||||||
|
|
||||||
generate_scene_package(GenerateSceneRequest {
|
|
||||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
|
||||||
scene_id: "process-data-scene".to_string(),
|
|
||||||
scene_name: "Process Data Scene".to_string(),
|
|
||||||
scene_kind: Some(SceneKind::ReportCollection),
|
|
||||||
target_url: None,
|
|
||||||
output_root: output_root.clone(),
|
|
||||||
lessons_path: None,
|
|
||||||
scene_info_json: None,
|
|
||||||
scene_ir_json: Some(scene_ir),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let skill_root = output_root.join("skills/process-data-scene");
|
|
||||||
let generated_script =
|
|
||||||
fs::read_to_string(skill_root.join("scripts/collect_process_data_scene.js")).unwrap();
|
|
||||||
|
|
||||||
// The $.ajax call should contain processData flag
|
|
||||||
assert!(
|
|
||||||
generated_script.contains("processData:"),
|
|
||||||
"Generated JS $.ajax call should contain 'processData:' flag"
|
|
||||||
);
|
|
||||||
// processData should be false for form-urlencoded (negated condition)
|
|
||||||
assert!(
|
|
||||||
generated_script.contains(
|
|
||||||
"processData: request.headers['Content-Type'] !== 'application/x-www-form-urlencoded'"
|
|
||||||
),
|
|
||||||
"Generated JS should set processData to false for form-urlencoded content type"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,455 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use sgclaw::generated_scene::generator::{
|
|
||||||
generate_scheduled_monitoring_action_skill_package, GenerateScheduledMonitoringActionSkillRequest,
|
|
||||||
};
|
|
||||||
use tungstenite::{accept, Message};
|
use tungstenite::{accept, Message};
|
||||||
|
|
||||||
fn bin_path() -> PathBuf {
|
fn bin_path() -> PathBuf {
|
||||||
@@ -36,6 +33,12 @@ fn temp_workspace(prefix: &str) -> PathBuf {
|
|||||||
root
|
root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validation_bundle_skills_dir() -> PathBuf {
|
||||||
|
std::env::current_dir()
|
||||||
|
.unwrap()
|
||||||
|
.join("dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills")
|
||||||
|
}
|
||||||
|
|
||||||
fn scheduled_trigger(mode: &str) -> Value {
|
fn scheduled_trigger(mode: &str) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"trigger_type": "scheduled",
|
"trigger_type": "scheduled",
|
||||||
@@ -529,8 +532,6 @@ fn binary_wiring_loads_registry_backed_scheduled_skill() {
|
|||||||
let output_path = workspace.join("run-record.json");
|
let output_path = workspace.join("run-record.json");
|
||||||
let config_path = workspace.join("sgclaw_config.json");
|
let config_path = workspace.join("sgclaw_config.json");
|
||||||
let rules_path = workspace.join("resources").join("rules.json");
|
let rules_path = workspace.join("resources").join("rules.json");
|
||||||
let materialization_root = workspace.join("materialized");
|
|
||||||
fs::create_dir_all(&materialization_root).unwrap();
|
|
||||||
write_json(&trigger_path, &scheduled_trigger("monitor_only"));
|
write_json(&trigger_path, &scheduled_trigger("monitor_only"));
|
||||||
write_runtime_rules(&rules_path);
|
write_runtime_rules(&rules_path);
|
||||||
let detect_payload = json!({
|
let detect_payload = json!({
|
||||||
@@ -579,25 +580,9 @@ fn binary_wiring_loads_registry_backed_scheduled_skill() {
|
|||||||
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
||||||
write_browser_config(&config_path, &browser_ws_url);
|
write_browser_config(&config_path, &browser_ws_url);
|
||||||
|
|
||||||
generate_scheduled_monitoring_action_skill_package(GenerateScheduledMonitoringActionSkillRequest {
|
|
||||||
scene_id: "command-center-fee-control-monitor".to_string(),
|
|
||||||
scene_name: "指挥中心费控异常监测".to_string(),
|
|
||||||
output_root: materialization_root.clone(),
|
|
||||||
source_evidence_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
|
||||||
),
|
|
||||||
ir_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
trigger_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let output = run_binary_with_skills_dir_and_config(
|
let output = run_binary_with_skills_dir_and_config(
|
||||||
&trigger_path,
|
&trigger_path,
|
||||||
&materialization_root.join("skills"),
|
&validation_bundle_skills_dir(),
|
||||||
&config_path,
|
&config_path,
|
||||||
&workspace,
|
&workspace,
|
||||||
&output_path,
|
&output_path,
|
||||||
@@ -632,29 +617,11 @@ fn binary_wiring_registry_backed_skill_executes_read_only_scripts_with_runtime_i
|
|||||||
let output_path = workspace.join("run-record.json");
|
let output_path = workspace.join("run-record.json");
|
||||||
let config_path = workspace.join("sgclaw_config.json");
|
let config_path = workspace.join("sgclaw_config.json");
|
||||||
let rules_path = workspace.join("resources").join("rules.json");
|
let rules_path = workspace.join("resources").join("rules.json");
|
||||||
let materialization_root = workspace.join("materialized");
|
|
||||||
fs::create_dir_all(&materialization_root).unwrap();
|
|
||||||
write_json(
|
write_json(
|
||||||
&trigger_path,
|
&trigger_path,
|
||||||
&scheduled_trigger_with_runtime_inputs("monitor_only"),
|
&scheduled_trigger_with_runtime_inputs("monitor_only"),
|
||||||
);
|
);
|
||||||
write_runtime_rules(&rules_path);
|
write_runtime_rules(&rules_path);
|
||||||
|
|
||||||
generate_scheduled_monitoring_action_skill_package(GenerateScheduledMonitoringActionSkillRequest {
|
|
||||||
scene_id: "command-center-fee-control-monitor".to_string(),
|
|
||||||
scene_name: "鎸囨尌涓績璐规帶寮傚父鐩戞祴".to_string(),
|
|
||||||
output_root: materialization_root.clone(),
|
|
||||||
source_evidence_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
|
||||||
),
|
|
||||||
ir_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
trigger_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
let detect_payload = json!({
|
let detect_payload = json!({
|
||||||
"type": "scheduled-monitoring-detect-snapshot",
|
"type": "scheduled-monitoring-detect-snapshot",
|
||||||
"report_name": "指挥中心费控异常监测",
|
"report_name": "指挥中心费控异常监测",
|
||||||
@@ -708,7 +675,7 @@ fn binary_wiring_registry_backed_skill_executes_read_only_scripts_with_runtime_i
|
|||||||
|
|
||||||
let output = run_binary_with_skills_dir_and_config(
|
let output = run_binary_with_skills_dir_and_config(
|
||||||
&trigger_path,
|
&trigger_path,
|
||||||
&materialization_root.join("skills"),
|
&validation_bundle_skills_dir(),
|
||||||
&config_path,
|
&config_path,
|
||||||
&workspace,
|
&workspace,
|
||||||
&output_path,
|
&output_path,
|
||||||
@@ -744,32 +711,12 @@ fn command_center_preview_reflects_automation_semantics() {
|
|||||||
let output_path = workspace.join("run-record.json");
|
let output_path = workspace.join("run-record.json");
|
||||||
let config_path = workspace.join("sgclaw_config.json");
|
let config_path = workspace.join("sgclaw_config.json");
|
||||||
let rules_path = workspace.join("resources").join("rules.json");
|
let rules_path = workspace.join("resources").join("rules.json");
|
||||||
let materialization_root = workspace.join("materialized");
|
|
||||||
fs::create_dir_all(&materialization_root).unwrap();
|
|
||||||
write_json(
|
write_json(
|
||||||
&trigger_path,
|
&trigger_path,
|
||||||
&scheduled_trigger_with_runtime_inputs("monitor_only"),
|
&scheduled_trigger_with_runtime_inputs("monitor_only"),
|
||||||
);
|
);
|
||||||
write_runtime_rules(&rules_path);
|
write_runtime_rules(&rules_path);
|
||||||
|
|
||||||
generate_scheduled_monitoring_action_skill_package(
|
|
||||||
GenerateScheduledMonitoringActionSkillRequest {
|
|
||||||
scene_id: "command-center-fee-control-monitor".to_string(),
|
|
||||||
scene_name: "command-center-fee-control-monitor".to_string(),
|
|
||||||
output_root: materialization_root.clone(),
|
|
||||||
source_evidence_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
|
||||||
),
|
|
||||||
ir_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
trigger_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let detect_payload = json!({
|
let detect_payload = json!({
|
||||||
"type": "scheduled-monitoring-detect-snapshot",
|
"type": "scheduled-monitoring-detect-snapshot",
|
||||||
"report_name": "command-center-fee-control-monitor",
|
"report_name": "command-center-fee-control-monitor",
|
||||||
@@ -823,7 +770,7 @@ fn command_center_preview_reflects_automation_semantics() {
|
|||||||
|
|
||||||
let output = run_binary_with_skills_dir_and_config(
|
let output = run_binary_with_skills_dir_and_config(
|
||||||
&trigger_path,
|
&trigger_path,
|
||||||
&materialization_root.join("skills"),
|
&validation_bundle_skills_dir(),
|
||||||
&config_path,
|
&config_path,
|
||||||
&workspace,
|
&workspace,
|
||||||
&output_path,
|
&output_path,
|
||||||
@@ -861,32 +808,12 @@ fn command_center_empty_pending_list_does_not_emit_log_write_preview() {
|
|||||||
let output_path = workspace.join("run-record.json");
|
let output_path = workspace.join("run-record.json");
|
||||||
let config_path = workspace.join("sgclaw_config.json");
|
let config_path = workspace.join("sgclaw_config.json");
|
||||||
let rules_path = workspace.join("resources").join("rules.json");
|
let rules_path = workspace.join("resources").join("rules.json");
|
||||||
let materialization_root = workspace.join("materialized");
|
|
||||||
fs::create_dir_all(&materialization_root).unwrap();
|
|
||||||
write_json(
|
write_json(
|
||||||
&trigger_path,
|
&trigger_path,
|
||||||
&scheduled_trigger_with_runtime_inputs("monitor_only"),
|
&scheduled_trigger_with_runtime_inputs("monitor_only"),
|
||||||
);
|
);
|
||||||
write_runtime_rules(&rules_path);
|
write_runtime_rules(&rules_path);
|
||||||
|
|
||||||
generate_scheduled_monitoring_action_skill_package(
|
|
||||||
GenerateScheduledMonitoringActionSkillRequest {
|
|
||||||
scene_id: "command-center-fee-control-monitor".to_string(),
|
|
||||||
scene_name: "command-center-fee-control-monitor".to_string(),
|
|
||||||
output_root: materialization_root.clone(),
|
|
||||||
source_evidence_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
|
||||||
),
|
|
||||||
ir_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
trigger_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let detect_payload = json!({
|
let detect_payload = json!({
|
||||||
"type": "scheduled-monitoring-detect-snapshot",
|
"type": "scheduled-monitoring-detect-snapshot",
|
||||||
"report_name": "command-center-fee-control-monitor",
|
"report_name": "command-center-fee-control-monitor",
|
||||||
@@ -936,7 +863,7 @@ fn command_center_empty_pending_list_does_not_emit_log_write_preview() {
|
|||||||
|
|
||||||
let output = run_binary_with_skills_dir_and_config(
|
let output = run_binary_with_skills_dir_and_config(
|
||||||
&trigger_path,
|
&trigger_path,
|
||||||
&materialization_root.join("skills"),
|
&validation_bundle_skills_dir(),
|
||||||
&config_path,
|
&config_path,
|
||||||
&workspace,
|
&workspace,
|
||||||
&output_path,
|
&output_path,
|
||||||
@@ -971,27 +898,9 @@ fn binary_wiring_browser_attached_passes_platform_service_base_from_config() {
|
|||||||
let output_path = workspace.join("run-record.json");
|
let output_path = workspace.join("run-record.json");
|
||||||
let config_path = workspace.join("sgclaw_config.json");
|
let config_path = workspace.join("sgclaw_config.json");
|
||||||
let rules_path = workspace.join("resources").join("rules.json");
|
let rules_path = workspace.join("resources").join("rules.json");
|
||||||
let materialization_root = workspace.join("materialized");
|
|
||||||
fs::create_dir_all(&materialization_root).unwrap();
|
|
||||||
write_json(&trigger_path, &scheduled_trigger("monitor_only"));
|
write_json(&trigger_path, &scheduled_trigger("monitor_only"));
|
||||||
write_runtime_rules(&rules_path);
|
write_runtime_rules(&rules_path);
|
||||||
|
|
||||||
generate_scheduled_monitoring_action_skill_package(GenerateScheduledMonitoringActionSkillRequest {
|
|
||||||
scene_id: "command-center-fee-control-monitor".to_string(),
|
|
||||||
scene_name: "鎸囨尌涓績璐规帶寮傚父鐩戞祴".to_string(),
|
|
||||||
output_root: materialization_root.clone(),
|
|
||||||
source_evidence_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
|
||||||
),
|
|
||||||
ir_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
trigger_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let detect_payload = json!({
|
let detect_payload = json!({
|
||||||
"type": "scheduled-monitoring-detect-snapshot",
|
"type": "scheduled-monitoring-detect-snapshot",
|
||||||
"report_name": "鎸囨尌涓績璐规帶寮傚父鐩戞祴",
|
"report_name": "鎸囨尌涓績璐规帶寮傚父鐩戞祴",
|
||||||
@@ -1046,7 +955,7 @@ fn binary_wiring_browser_attached_passes_platform_service_base_from_config() {
|
|||||||
|
|
||||||
let output = run_binary_with_skills_dir_and_config(
|
let output = run_binary_with_skills_dir_and_config(
|
||||||
&trigger_path,
|
&trigger_path,
|
||||||
&materialization_root.join("skills"),
|
&validation_bundle_skills_dir(),
|
||||||
&config_path,
|
&config_path,
|
||||||
&workspace,
|
&workspace,
|
||||||
&output_path,
|
&output_path,
|
||||||
@@ -1164,7 +1073,7 @@ fn binary_wiring_loads_archive_workorder_skill_from_bundle() {
|
|||||||
);
|
);
|
||||||
assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 1);
|
assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 1);
|
||||||
assert_eq!(record["previewArtifact"]["summary"]["notify_count"], 0);
|
assert_eq!(record["previewArtifact"]["summary"]["notify_count"], 0);
|
||||||
assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 1);
|
assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
record["auditPreview"]["detectReadDiagnostics"]["businessType"],
|
record["auditPreview"]["detectReadDiagnostics"]["businessType"],
|
||||||
"归档工单配网推送"
|
"归档工单配网推送"
|
||||||
@@ -1311,7 +1220,7 @@ fn binary_wiring_loads_available_balance_below_zero_skill_from_bundle() {
|
|||||||
);
|
);
|
||||||
assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 1);
|
assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 1);
|
||||||
assert_eq!(record["previewArtifact"]["summary"]["notify_count"], 0);
|
assert_eq!(record["previewArtifact"]["summary"]["notify_count"], 0);
|
||||||
assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 1);
|
assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
record["auditPreview"]["detectReadDiagnostics"]["businessType"],
|
record["auditPreview"]["detectReadDiagnostics"]["businessType"],
|
||||||
"可用电费小于零监测提醒"
|
"可用电费小于零监测提醒"
|
||||||
|
|||||||
@@ -1,60 +1,32 @@
|
|||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use sgclaw::compat::scene_platform::scheduled_registry::load_scheduled_monitoring_registry;
|
use sgclaw::compat::scene_platform::scheduled_registry::load_scheduled_monitoring_registry;
|
||||||
use sgclaw::generated_scene::generator::{
|
|
||||||
generate_scheduled_monitoring_action_skill_package, GenerateScheduledMonitoringActionSkillRequest,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn temp_workspace(prefix: &str) -> PathBuf {
|
fn validation_bundle_skills_dir() -> PathBuf {
|
||||||
let nanos = SystemTime::now()
|
PathBuf::from("dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills")
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_nanos();
|
|
||||||
let path = std::env::temp_dir().join(format!("{prefix}-{nanos}"));
|
|
||||||
fs::create_dir_all(&path).unwrap();
|
|
||||||
path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scheduled_monitoring_registry_loads_materialized_skill() {
|
fn scheduled_monitoring_registry_loads_materialized_skill() {
|
||||||
let output_root = temp_workspace("sgclaw-scheduled-monitoring-registry");
|
let registry = load_scheduled_monitoring_registry(&validation_bundle_skills_dir()).unwrap();
|
||||||
generate_scheduled_monitoring_action_skill_package(GenerateScheduledMonitoringActionSkillRequest {
|
let entry = registry
|
||||||
scene_id: "command-center-fee-control-monitor".to_string(),
|
.iter()
|
||||||
scene_name: "指挥中心费控异常监测".to_string(),
|
.find(|entry| entry.workflow_id == "command_center_fee_control_monitoring_action")
|
||||||
output_root: output_root.clone(),
|
.expect("command center skill must be registered");
|
||||||
source_evidence_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
|
||||||
),
|
|
||||||
ir_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
trigger_contract_json: PathBuf::from(
|
|
||||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let registry = load_scheduled_monitoring_registry(&output_root.join("skills")).unwrap();
|
|
||||||
assert_eq!(registry.len(), 1);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry[0].workflow_id,
|
entry.workflow_id,
|
||||||
"command_center_fee_control_monitoring_action"
|
"command_center_fee_control_monitoring_action"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry[0].manifest.scene.kind,
|
entry.manifest.scene.kind,
|
||||||
"scheduled_monitoring_action_workflow"
|
"scheduled_monitoring_action_workflow"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scheduled_monitoring_registry_loads_archive_workorder_skill() {
|
fn scheduled_monitoring_registry_loads_archive_workorder_skill() {
|
||||||
let skills_dir = PathBuf::from(
|
let registry = load_scheduled_monitoring_registry(&validation_bundle_skills_dir()).unwrap();
|
||||||
"dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills",
|
|
||||||
);
|
|
||||||
|
|
||||||
let registry = load_scheduled_monitoring_registry(&skills_dir).unwrap();
|
|
||||||
let entry = registry
|
let entry = registry
|
||||||
.iter()
|
.iter()
|
||||||
.find(|entry| entry.workflow_id == "archive_workorder_grid_push_monitoring_action")
|
.find(|entry| entry.workflow_id == "archive_workorder_grid_push_monitoring_action")
|
||||||
@@ -77,11 +49,7 @@ fn scheduled_monitoring_registry_loads_archive_workorder_skill() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scheduled_monitoring_registry_loads_available_balance_below_zero_skill() {
|
fn scheduled_monitoring_registry_loads_available_balance_below_zero_skill() {
|
||||||
let skills_dir = PathBuf::from(
|
let registry = load_scheduled_monitoring_registry(&validation_bundle_skills_dir()).unwrap();
|
||||||
"dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills",
|
|
||||||
);
|
|
||||||
|
|
||||||
let registry = load_scheduled_monitoring_registry(&skills_dir).unwrap();
|
|
||||||
let entry = registry
|
let entry = registry
|
||||||
.iter()
|
.iter()
|
||||||
.find(|entry| entry.workflow_id == "available_balance_below_zero_monitoring_action")
|
.find(|entry| entry.workflow_id == "available_balance_below_zero_monitoring_action")
|
||||||
@@ -104,11 +72,7 @@ fn scheduled_monitoring_registry_loads_available_balance_below_zero_skill() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scheduled_monitoring_registry_loads_sgcc_todo_crawler_skill() {
|
fn scheduled_monitoring_registry_loads_sgcc_todo_crawler_skill() {
|
||||||
let skills_dir = PathBuf::from(
|
let registry = load_scheduled_monitoring_registry(&validation_bundle_skills_dir()).unwrap();
|
||||||
"dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills",
|
|
||||||
);
|
|
||||||
|
|
||||||
let registry = load_scheduled_monitoring_registry(&skills_dir).unwrap();
|
|
||||||
let entry = registry
|
let entry = registry
|
||||||
.iter()
|
.iter()
|
||||||
.find(|entry| entry.workflow_id == "sgcc_todo_crawler_monitoring_action")
|
.find(|entry| entry.workflow_id == "sgcc_todo_crawler_monitoring_action")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user