diff --git a/src/bin/sg_scene_generate.rs b/src/bin/sg_scene_generate.rs deleted file mode 100644 index 6f93d68..0000000 --- a/src/bin/sg_scene_generate.rs +++ /dev/null @@ -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 = 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 = 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, - target_url: Option, - output_root: PathBuf, - lessons_path: Option, - scene_info_json: Option, - scene_ir_json: Option, -} - -fn parse_args(args: impl Iterator) -> Result { - 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 = 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 --scene-id --scene-name [--scene-kind ] [--target-url ] --output-root [--lessons ] [--scene-info-json ''] [--scene-ir-json '']".to_string() -} diff --git a/src/generated_scene/mod.rs b/src/generated_scene/mod.rs index 3f68661..f14a5b4 100644 --- a/src/generated_scene/mod.rs +++ b/src/generated_scene/mod.rs @@ -1,5 +1 @@ -pub mod analyzer; -pub mod generator; -pub mod ir; -pub mod lessons; pub mod scheduled_monitoring_runtime; diff --git a/tests/generated_scene_lessons_test.rs b/tests/generated_scene_lessons_test.rs deleted file mode 100644 index 5e2ab8e..0000000 --- a/tests/generated_scene_lessons_test.rs +++ /dev/null @@ -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); -} diff --git a/tests/generated_scene_runtime_boundary_test.rs b/tests/generated_scene_runtime_boundary_test.rs new file mode 100644 index 0000000..dff784d --- /dev/null +++ b/tests/generated_scene_runtime_boundary_test.rs @@ -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" + ); +} diff --git a/tests/scene_generator_canonical_test.rs b/tests/scene_generator_canonical_test.rs deleted file mode 100644 index 234779b..0000000 --- a/tests/scene_generator_canonical_test.rs +++ /dev/null @@ -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, -} - -#[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, - #[serde(rename = "requiredWorkflowStepTypes")] - required_workflow_step_types: Vec, - #[serde(rename = "requiredGateNames")] - required_gate_names: Vec, - #[serde(rename = "acceptanceChecklist")] - acceptance_checklist: Vec, - #[serde(rename = "failureTaxonomy")] - failure_taxonomy: Vec, -} - -#[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::>(); - 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::>(); - 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::>(); - 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 -} diff --git a/tests/scene_generator_llm_test.js b/tests/scene_generator_llm_test.js deleted file mode 100644 index a1d015d..0000000 --- a/tests/scene_generator_llm_test.js +++ /dev/null @@ -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"), - ``, - "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"), - ``, - "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"), - ``, - "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"), - ``, - "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(); diff --git a/tests/scene_generator_modes_test.rs b/tests/scene_generator_modes_test.rs deleted file mode 100644 index 39f6d68..0000000 --- a/tests/scene_generator_modes_test.rs +++ /dev/null @@ -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) -> SceneIr { - let is_multi = modes.len() > 1; - let api_endpoints = modes - .iter() - .filter_map(|mode| mode.api_endpoint.clone()) - .collect::>(); - 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" - ); -} diff --git a/tests/scene_generator_p1_family_test.rs b/tests/scene_generator_p1_family_test.rs deleted file mode 100644 index a046e56..0000000 --- a/tests/scene_generator_p1_family_test.rs +++ /dev/null @@ -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, -} - -#[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, - #[serde(rename = "requiredEvidenceTypes")] - required_evidence_types: Vec, - #[serde(rename = "expansionFixtureDir", default)] - expansion_fixture_dir: Option, - #[serde(rename = "expansionSceneId", default)] - expansion_scene_id: Option, - #[serde(rename = "expansionSceneName", default)] - expansion_scene_name: Option, - #[serde(rename = "expansionAssertions", default)] - expansion_assertions: Option, - #[serde(rename = "batchCandidateAsset", default)] - batch_candidate_asset: Option, - #[serde(rename = "batchExpansionFixtures", default)] - batch_expansion_fixtures: Vec, - #[serde(rename = "successRateSummary")] - success_rate_summary: String, - #[serde(rename = "failureTaxonomy")] - failure_taxonomy: Vec, -} - -#[derive(Debug, Deserialize, Default)] -struct ExpansionAssertions { - #[serde(rename = "requiredDefaultMode", default)] - required_default_mode: Option, - #[serde(rename = "expectedPaginationField", default)] - expected_pagination_field: Option, - #[serde(rename = "requiredJoinKey", default)] - required_join_key: Option, - #[serde(rename = "requiredAggregateRule", default)] - required_aggregate_rule: Option, - #[serde(rename = "requiredMainRequest", default)] - required_main_request: Option, - #[serde(rename = "requiredEnrichmentRequest", default)] - required_enrichment_request: Option, - #[serde(rename = "requiredMergeJoinKey", default)] - required_merge_join_key: Option, - #[serde(rename = "requiredMergeAggregateRule", default)] - required_merge_aggregate_rule: Option, - #[serde(rename = "requiredOutputColumn", default)] - required_output_column: Option, -} - -#[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 -} diff --git a/tests/scene_generator_test.rs b/tests/scene_generator_test.rs deleted file mode 100644 index 9d7714b..0000000 --- a/tests/scene_generator_test.rs +++ /dev/null @@ -1,4267 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; - -use sgclaw::generated_scene::analyzer::{ - analyze_scene_source, analyze_scene_source_with_hint, extract_deterministic_scene_facts, - G2FamilyVariant, SceneKind, ToolKind, -}; -use sgclaw::generated_scene::generator::{ - compute_readiness_for_test, generate_monitoring_action_detect_preview_package, - generate_scene_package, generate_scheduled_monitoring_action_skill_package, - resolve_scene_ir_for_test, GenerateMonitoringActionPreviewRequest, - GenerateScheduledMonitoringActionSkillRequest, GenerateSceneRequest, -}; -use sgclaw::generated_scene::ir::{ - ApiEndpointIr, ArtifactContractIr, BootstrapIr, ModeConditionIr, ModeIr, ReadinessIr, - SceneIdDiagnosticsIr, SceneIr, ValidationHintsIr, WorkflowArchetype, WorkflowEvidenceIr, - WorkflowStepIr, -}; - -#[test] -fn analyzer_classifies_supported_report_collection_source() { - let analysis = analyze_scene_source(Path::new( - "tests/fixtures/generated_scene/report_collection", - )) - .unwrap(); - - assert_eq!(analysis.scene_kind, SceneKind::ReportCollection); - assert_eq!(analysis.tool_kind, ToolKind::BrowserScript); - assert_eq!( - analysis.bootstrap.target_url.as_deref(), - Some("http://20.76.57.61:18080/gsllys") - ); - assert_eq!( - analysis.bootstrap.app_entry_url.as_deref(), - Some("http://20.76.57.61:18080/gsllys") - ); - assert_eq!( - analysis.bootstrap.module_route_url.as_deref(), - Some("http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor") - ); - assert_eq!( - analysis.bootstrap.target_url_kind.as_deref(), - Some("runtime_context") - ); - assert_eq!( - analysis.bootstrap.expected_domain.as_deref(), - Some("20.76.57.61") - ); - assert_eq!( - analysis.collection_entry_script.as_deref(), - Some("js/report.js") - ); -} - -#[test] -fn analyzer_accepts_missing_meta_with_scene_kind_hint() { - let analysis = analyze_scene_source_with_hint( - Path::new("tests/fixtures/generated_scene/non_report"), - Some(SceneKind::ReportCollection), - ) - .unwrap(); - - assert_eq!(analysis.scene_kind, SceneKind::ReportCollection); -} - -#[test] -fn analyzer_uses_hint_when_meta_missing() { - let analysis = analyze_scene_source_with_hint( - Path::new("tests/fixtures/generated_scene/non_report"), - Some(SceneKind::Monitoring), - ) - .unwrap(); - - assert_eq!(analysis.scene_kind, SceneKind::Monitoring); -} - -#[test] -fn analyzer_prefers_business_source_url_over_external_script_host() { - let analysis = - analyze_scene_source(Path::new("tests/fixtures/generated_scene/external_script")).unwrap(); - - assert_eq!( - analysis.bootstrap.expected_domain.as_deref(), - Some("yx.gs.sgcc.com.cn") - ); -} - -#[test] -fn analyzer_prefers_business_source_url_over_localhost_helper_and_export() { - let analysis = analyze_scene_source(Path::new( - "tests/fixtures/generated_scene/bootstrap_localhost_pollution", - )) - .unwrap(); - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/bootstrap_localhost_pollution", - )) - .unwrap(); - - assert_eq!( - analysis.bootstrap.expected_domain.as_deref(), - Some("yx.gs.sgcc.com.cn") - ); - assert_eq!( - analysis.bootstrap.target_url.as_deref(), - Some("http://yx.gs.sgcc.com.cn") - ); - assert!(facts - .endpoints - .iter() - .all(|endpoint| !endpoint.url.contains("localhost"))); - assert!(facts - .localhost_dependencies - .iter() - .any(|url| url.contains("localhost:13313"))); -} - -#[test] -fn deterministic_analysis_classifies_multi_mode_fixture() { - let facts = - extract_deterministic_scene_facts(Path::new("tests/fixtures/generated_scene/multi_mode")) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::MultiModeRequest - ); - assert!(facts - .branch_fields - .iter() - .any(|field| field == "period_mode")); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("monthReport"))); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("weekReport"))); - let month_endpoint = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("monthReport")) - .expect("expected month endpoint"); - let week_endpoint = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("weekReport")) - .expect("expected week endpoint"); - assert_eq!( - month_endpoint - .request_template - .as_ref() - .and_then(|value| value.get("tjzq")) - .and_then(|value| value.as_str()), - Some("month") - ); - assert_eq!( - week_endpoint - .request_template - .as_ref() - .and_then(|value| value.get("tjzq")) - .and_then(|value| value.as_str()), - Some("week") - ); -} - -#[test] -fn deterministic_analysis_prefers_g2_multi_mode_over_pagination_noise() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g2_noisy_multi_mode", - )) - .unwrap(); - let analysis = analyze_scene_source(Path::new( - "tests/fixtures/generated_scene/g2_noisy_multi_mode", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::MultiModeRequest - ); - assert_eq!(facts.g2_family_variant, G2FamilyVariant::G2A); - assert_eq!( - analysis.bootstrap.expected_domain.as_deref(), - Some("20.76.57.61:18080") - ); - assert_eq!( - analysis.bootstrap.target_url.as_deref(), - Some("http://20.76.57.61:18080/gsllys") - ); - assert!(facts - .endpoints - .iter() - .any(|endpoint| { endpoint.url.contains("getYearMonWeekLinelossAnalysisList") })); - assert!(facts - .endpoints - .iter() - .any(|endpoint| { endpoint.url.contains("fourVerEightHorLinelossRateList") })); - assert!(facts - .endpoints - .iter() - .all(|endpoint| !endpoint.url.contains("github.com"))); - assert!(facts - .endpoints - .iter() - .all(|endpoint| !endpoint.url.contains("developer.mozilla.org"))); - assert!(facts - .endpoints - .iter() - .all(|endpoint| !endpoint.url.contains("stackoverflow.com"))); - assert!(facts.month_column_defs.iter().any(|(key, _)| key == "YGDL")); - assert!(facts - .week_column_defs - .iter() - .any(|(key, _)| key == "LINE_LOSS_RATE")); - let month_endpoint = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("fourVerEightHorLinelossRateList")) - .expect("expected month endpoint"); - let week_endpoint = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("getYearMonWeekLinelossAnalysisList")) - .expect("expected week endpoint"); - assert_eq!( - month_endpoint - .request_template - .as_ref() - .and_then(|value| value.get("yn_flag")) - .and_then(|value| value.as_u64()), - Some(0) - ); - assert_eq!( - week_endpoint - .request_template - .as_ref() - .and_then(|value| value.get("tjzq")) - .and_then(|value| value.as_str()), - Some("week") - ); - assert_eq!( - week_endpoint - .request_template - .as_ref() - .and_then(|value| value.get("weekSfdate")) - .and_then(|value| value.as_str()), - Some("${args.weekSfdate}") - ); -} - -#[test] -fn deterministic_analysis_classifies_g2_weekly_single_mode_variant() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g2_weekly_single_mode", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::MultiModeRequest - ); - assert_eq!(facts.g2_family_variant, G2FamilyVariant::G2B); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("getYearMonWeekLinelossAnalysisList"))); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("getTqLinelossInfoListRank"))); - assert!(facts.month_column_defs.is_empty()); - assert!(facts.week_column_defs.is_empty()); -} - -#[test] -fn deterministic_analysis_classifies_g2_mixed_linked_workflow_variant() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g2_mixed_linked_workflow", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::MultiModeRequest - ); - assert_eq!(facts.g2_family_variant, G2FamilyVariant::G2C); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("getTqLinelossInfoListRank"))); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("getUserElectricList"))); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("syncLineLossService/workbench"))); - assert!(facts.month_column_defs.is_empty()); - assert!(facts.week_column_defs.is_empty()); -} - -#[test] -fn deterministic_analysis_classifies_g2_comparison_crosscheck_variant() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g2_comparison_crosscheck", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::MultiModeRequest - ); - assert_eq!(facts.g2_family_variant, G2FamilyVariant::G2E); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("getTqLinelossInfoListRank"))); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("getUserElectricList"))); -} - -#[test] -fn deterministic_analysis_classifies_g2_diagnosis_drilldown_variant() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g2_diagnosis_drilldown", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::MultiModeRequest - ); - assert_eq!(facts.g2_family_variant, G2FamilyVariant::G2F); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("tqAutoDiagnoseAnalyse/search"))); - assert!(facts.endpoints.iter().any(|endpoint| endpoint - .url - .contains("stealElecAnalyse/getFlqdyhDetailList"))); -} - -#[test] -fn deterministic_analysis_classifies_g2_prediction_compute_variant() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g2_prediction_compute", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::MultiModeRequest - ); - assert_eq!(facts.g2_family_variant, G2FamilyVariant::G2D); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.url.contains("highLineLossForecast"))); -} - -#[test] -fn deterministic_analysis_classifies_paginated_enrichment_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - assert!(facts.pagination_fields.iter().any(|field| field == "page")); - assert!(facts - .secondary_request_methods - .iter() - .any(|method| method == "getUserCharges")); - assert!(facts - .filter_expressions - .iter() - .any(|expr| expr.contains("charge !== 0"))); -} - -#[test] -fn deterministic_analysis_classifies_g1e_light_enrichment_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g1e_light_enrichment", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::SingleRequestEnrichment - ); - assert!(facts.branch_fields.iter().any(|field| field == "mode")); - assert!(facts.branch_fields.iter().any(|field| field == "month")); - assert!(facts - .branch_fields - .iter() - .any(|field| field == "reportType")); - assert!(facts - .filter_expressions - .iter() - .any(|expr| expr.contains("status === 200"))); - assert!(facts - .g1e_main_endpoint - .as_deref() - .map(|name| name.contains("getWkorderAll")) - .unwrap_or(false)); - assert!(facts - .g1e_enrichment_endpoints - .iter() - .any(|name| name.contains("queryElectCustInfo"))); - assert!(facts.g1e_join_keys.iter().any(|key| key == "wkOrderNo")); - assert!(facts - .g1e_output_columns - .iter() - .any(|(field, _)| field == "countyCodeName")); -} - -#[test] -fn deterministic_analysis_classifies_g1e_light_enrichment_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g1e_light_enrichment_expansion", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::SingleRequestEnrichment - ); - assert!(facts - .g1e_main_endpoint - .as_deref() - .map(|name| name.contains("getWkorderAll")) - .unwrap_or(false)); - assert!(facts - .g1e_enrichment_endpoints - .iter() - .any(|name| name.contains("queryMeterInfo"))); - assert!(facts.g1e_join_keys.iter().any(|key| key == "wkOrderNo")); - assert!(facts - .g1e_join_keys - .iter() - .any(|key| key == "countyCodeName")); - assert!(facts - .g1e_output_columns - .iter() - .any(|(field, _)| field == "meterCapacityThisMonth")); - assert!(facts - .g1e_aggregate_rules - .iter() - .any(|rule| rule == "group_by:countyCodeName")); -} - -#[test] -fn deterministic_analysis_classifies_g1e_light_enrichment_additional_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g1e_light_enrichment_additional", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::SingleRequestEnrichment - ); - assert!(facts - .g1e_main_endpoint - .as_deref() - .map(|name| name.contains("getWkorderAll")) - .unwrap_or(false)); - assert!(facts - .g1e_enrichment_endpoints - .iter() - .any(|name| name.contains("queryBusAcpt"))); - assert!(facts.g1e_join_keys.iter().any(|key| key == "wkOrderNo")); - assert!(facts - .g1e_join_keys - .iter() - .any(|key| key == "countyCodeName")); - assert!(facts - .g1e_output_columns - .iter() - .any(|(field, _)| field == "batchCapacityThisMonth")); - assert!(facts - .g1e_aggregate_rules - .iter() - .any(|rule| rule == "group_by:countyCodeName")); -} - -#[test] -fn deterministic_analysis_classifies_g6_host_bridge_workflow_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g6_host_bridge_workflow", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::HostBridgeWorkflow - ); - assert!(facts - .host_bridge_actions - .iter() - .any(|action| action.contains("sgBrowerserJsAjax2"))); - assert!(facts - .localhost_dependencies - .iter() - .any(|dependency| dependency.contains("localhost:13313"))); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.name.contains("getWorkOrderToDoList"))); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.name.contains("queryMeterPlanFormulateApp"))); -} - -#[test] -fn deterministic_analysis_prefers_g6_for_fixed_real_sample_mixed_with_localhost_export() { - let facts = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/电能表现场检验完成率指标报表", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::HostBridgeWorkflow - ); - assert!(facts - .host_bridge_actions - .iter() - .any(|action| action.contains("location.origin") - || action.contains("location.href") - || action.contains("yx.gs.sgcc.com.cn"))); - assert!(facts - .localhost_dependencies - .iter() - .any(|dependency| dependency.contains("localhost:13313"))); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.name.contains("getWorkOrderToDoList"))); - assert!(facts - .endpoints - .iter() - .any(|endpoint| endpoint.name.contains("queryMeterPlanFormulateApp"))); -} - -#[test] -fn deterministic_analysis_classifies_g7_multi_endpoint_inventory_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g7_multi_endpoint_inventory", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::MultiEndpointInventory - ); - assert!(facts.inventory_endpoint_names.len() >= 5); - assert!(facts - .inventory_endpoint_names - .iter() - .any(|name| name.contains("assetStatsQueryMeter"))); - assert!(facts - .inventory_endpoint_names - .iter() - .any(|name| name.contains("assetStatsQueryJlGnModule"))); -} - -#[test] -fn deterministic_analysis_prefers_g7_for_mixed_g7_host_bridge_real_sample() { - let facts = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/计量资产库存统计", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::MultiEndpointInventory - ); - assert!(facts.inventory_endpoint_names.len() >= 5); - assert!(!facts.host_bridge_actions.is_empty()); - assert!(facts - .localhost_dependencies - .iter() - .any(|dependency| dependency.contains("localhost:13313"))); -} - -#[test] -fn deterministic_analysis_classifies_g8_local_doc_pipeline_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g8_local_doc_pipeline", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::LocalDocPipeline - ); - assert!(facts - .localhost_dependencies - .iter() - .any(|dependency| dependency.contains("localhost:13313"))); - assert!(facts - .local_doc_pipeline_actions - .iter() - .any(|action| action == "definedSqlQuery")); - assert!(facts - .local_doc_pipeline_actions - .iter() - .any(|action| action == "docExport")); - assert!(facts - .local_doc_pipeline_actions - .iter() - .any(|action| action.contains("selectData"))); -} - -#[test] -fn deterministic_analysis_prefers_g3_for_mixed_g3_g8_boundary_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/g3_g8_mixed_boundary", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - assert!(facts - .g3_business_endpoint_names - .iter() - .any(|name| name.contains("queryHisS95598WkstGrid") - || name.contains("newQueryS95598WkstGrid"))); - assert!(facts - .local_doc_pipeline_actions - .iter() - .any(|action| action == "selectData")); - assert!(facts - .local_doc_pipeline_actions - .iter() - .any(|action| action == "docExport")); - assert!(facts - .pagination_fields - .iter() - .any(|field| field == "PAGEPOLITCURRENTPAGEINDEX_grid")); - assert!(facts.response_paths.iter().any(|field| field == "rows")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - assert!(facts.endpoints.len() >= 2); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("ticketRiskDetail")) - .expect("expected secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for secondary endpoint"); - assert!(template.contains_key("ticketNo")); -} - -#[test] -fn real_g3_sample_deterministic_analysis_stays_in_paginated_enrichment() { - let facts = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/95598工单明细表", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - assert!(facts - .g3_business_endpoint_names - .iter() - .any(|name| name.contains("queryHisS95598WkstGrid") - || name.contains("newQueryS95598WkstGrid"))); - assert!(facts - .host_bridge_actions - .iter() - .any(|action| action.contains("sgBrowerserJsAjax2"))); - assert!(facts - .local_doc_pipeline_actions - .iter() - .any(|action| action == "selectData")); - assert!(facts - .local_doc_pipeline_actions - .iter() - .any(|action| action == "docExport")); - assert!(facts - .pagination_fields - .iter() - .any(|field| field == "PAGEPOLITCURRENTPAGEINDEX_grid")); -} - -#[test] -fn real_g3_sample_runtime_scope_gate_allows_subordinate_localhost_dependencies() { - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string( - "examples/g3_real_sample_validation/skills/g3-95598-ticket-detail-real/references/generation-report.json", - ) - .unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(generated_report - .runtime_dependencies - .iter() - .all(|item| item.subordinate_to_business_chain)); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g3_runtime_scope_compatible" && gate.passed)); - assert_ne!(generated_report.readiness.level, "C"); -} - -#[test] -fn real_g3_sample_output_contract_is_narrower_than_generic_g3_shape() { - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string( - "examples/g3_real_sample_validation/skills/g3-95598-ticket-detail-real/references/generation-report.json", - ) - .unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert_eq!( - generated_report - .main_request - .as_ref() - .and_then(|request| request.api_endpoint.as_ref()) - .map(|endpoint| endpoint.name.as_str()), - Some("queryHisS95598WkstGrid") - ); - assert_eq!( - generated_report - .main_request - .as_ref() - .map(|request| request.response_path.as_str()), - Some("rows") - ); - assert!(generated_report - .enrichment_requests - .iter() - .all(|item| item.name != "login.jsp" && item.name != "main1.jsp")); - assert!(generated_report.join_keys.iter().all(|key| matches!( - key.as_str(), - "appNo" | "custNo" | "ticketNo" | "workOrderNo" | "orderNo" - ))); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .all(|rule| rule.contains("appNo") - || rule.contains("custNo") - || rule.contains("ticketNo") - || rule.contains("workOrderNo") - || rule.contains("orderNo"))); -} - -#[test] -fn real_g2_sample_contract_matches_closed_multi_mode_shape() { - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string( - "examples/real_scene_batch_round1/skills/real-tq-lineloss-report-r4/references/generation-report.json", - ) - .unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::MultiModeRequest - ); - assert_eq!( - generated_report.bootstrap.expected_domain, - "20.76.57.61:18080" - ); - assert_eq!( - generated_report.bootstrap.target_url, - "http://20.76.57.61:18080/gsllys" - ); - assert_eq!(generated_report.readiness.level, "A"); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g2_bootstrap_resolved" && gate.passed)); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g2_request_contract_complete" && gate.passed)); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g2_response_contract_complete" && gate.passed)); - - assert_eq!(generated_report.modes.len(), 2); - assert_eq!(generated_report.default_mode.as_deref(), Some("month")); - assert_eq!( - generated_report.mode_switch_field.as_deref(), - Some("period_mode") - ); - - let month_mode = generated_report - .modes - .iter() - .find(|mode| mode.name == "month") - .expect("expected month mode"); - assert_eq!( - month_mode - .api_endpoint - .as_ref() - .map(|endpoint| endpoint.name.as_str()), - Some("fourVerEightHorLinelossRateList") - ); - assert_eq!(month_mode.response_path, "content"); - assert!(month_mode.request_template.as_object().is_some()); - assert!(month_mode - .request_template - .as_object() - .and_then(|template| template.get("orgno")) - .is_some()); - assert!(month_mode - .request_template - .as_object() - .and_then(|template| template.get("fdate")) - .is_some()); - assert!(!month_mode.column_defs.is_empty()); - - let week_mode = generated_report - .modes - .iter() - .find(|mode| mode.name == "week") - .expect("expected week mode"); - assert_eq!( - week_mode - .api_endpoint - .as_ref() - .map(|endpoint| endpoint.name.as_str()), - Some("getYearMonWeekLinelossAnalysisList") - ); - assert_eq!(week_mode.response_path, "content"); - assert!(week_mode.request_template.as_object().is_some()); - assert!(week_mode - .request_template - .as_object() - .and_then(|template| template.get("orgno")) - .is_some()); - assert!(week_mode - .request_template - .as_object() - .and_then(|template| template.get("weekSfdate")) - .is_some()); - assert!(week_mode - .request_template - .as_object() - .and_then(|template| template.get("weekEfdate")) - .is_some()); - assert!(!week_mode.column_defs.is_empty()); - - assert_eq!(generated_report.response_path, "content"); -} - -#[test] -fn generator_derives_reusable_request_field_mappings_for_real_g2_fixture() { - let source_dir = PathBuf::from("tests/fixtures/generated_scene/multi_mode"); - let analysis = - analyze_scene_source_with_hint(&source_dir, Some(SceneKind::ReportCollection)).unwrap(); - let facts = extract_deterministic_scene_facts(&source_dir).unwrap(); - let generated_report = resolve_scene_ir_for_test( - &GenerateSceneRequest { - source_dir: source_dir.clone(), - scene_id: "resolver-request-mapping-g2".to_string(), - scene_name: "Resolver Request Mapping G2".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root: temp_workspace("sgclaw-resolver-request-mapping-g2"), - lessons_path: None, - scene_info_json: None, - scene_ir_json: Some(build_multi_mode_scene_ir()), - }, - &analysis, - &facts, - ); - - let month_mode = generated_report - .modes - .iter() - .find(|mode| mode.name == "month") - .expect("expected month mode"); - assert!(month_mode.request_field_mappings.iter().any(|mapping| { - mapping.source_field == "org_code" - && mapping.target_field == "orgno" - && mapping.mode.as_deref() == Some("month") - })); - assert!(month_mode.request_field_mappings.iter().any(|mapping| { - mapping.source_field == "period_payload.fdate" - && mapping.target_field == "fdate" - && mapping.mode.as_deref() == Some("month") - })); - - let week_mode = generated_report - .modes - .iter() - .find(|mode| mode.name == "week") - .expect("expected week mode"); - assert!(week_mode.request_field_mappings.iter().any(|mapping| { - mapping.source_field == "org_code" - && mapping.target_field == "orgno" - && mapping.mode.as_deref() == Some("week") - })); - assert!(week_mode.request_field_mappings.iter().any(|mapping| { - mapping.source_field == "period_payload.weekSfdate" - && mapping.target_field == "weekSfdate" - && mapping.mode.as_deref() == Some("week") - })); - assert!(week_mode.request_field_mappings.iter().any(|mapping| { - mapping.source_field == "period_payload.weekEfdate" - && mapping.target_field == "weekEfdate" - && mapping.mode.as_deref() == Some("week") - })); -} - -#[test] -fn analyzer_extracts_embedded_org_dictionary_from_sweep_030_source() { - let facts = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/台区线损大数据-月_周累计线损率统计分析", - )) - .unwrap(); - - assert!( - facts.org_dictionary_entries.len() >= 10, - "expected source-driven org dictionary entries, got {}", - facts.org_dictionary_entries.len() - ); - assert!(facts - .org_dictionary_entries - .iter() - .any(|entry| entry.code == "62401")); - assert!(facts - .org_dictionary_entries - .iter() - .any(|entry| entry.code == "62408")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_workorder_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_workorder", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("workOrderSourceSummary")) - .expect("expected workorder secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for workorder secondary endpoint"); - assert!(template.contains_key("workOrderNo")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_orderno_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_orderno", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("serviceOrderTrend")) - .expect("expected orderno secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for orderno secondary endpoint"); - assert!(template.contains_key("orderNo")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_source_distribution_expansion_fixture() -{ - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_source_distribution", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("ticketSourceSummary")) - .expect("expected source distribution secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for source distribution secondary endpoint"); - assert!(template.contains_key("ticketNo")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_service_risk_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_service_risk", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("serviceRiskDetail")) - .expect("expected service risk secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for service risk secondary endpoint"); - assert!(template.contains_key("ticketNo")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_timeout_warning_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_timeout_warning", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("ticketTimeoutWarningDetail")) - .expect("expected timeout warning secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for timeout warning secondary endpoint"); - assert!(template.contains_key("ticketNo")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_device_monitor_weekly_expansion_fixture( -) { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_device_monitor_weekly", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("deviceMonitorWeeklyDetail")) - .expect("expected device monitor weekly secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for device monitor weekly secondary endpoint"); - assert!(template.contains_key("ticketNo")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_customer_satisfaction_expansion_fixture( -) { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_customer_satisfaction", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("customerSatisfactionDetail")) - .expect("expected customer satisfaction secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for customer satisfaction secondary endpoint"); - assert!(template.contains_key("ticketNo")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_repair_return_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_repair_return", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("repairReturnDetail")) - .expect("expected repair return secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for repair return secondary endpoint"); - assert!(template.contains_key("ticketNo")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_repair_daily_control_expansion_fixture() -{ - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_repair_daily_control", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("repairDailyControlDetail")) - .expect("expected repair daily control secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for repair daily control secondary endpoint"); - assert!(template.contains_key("ticketNo")); -} - -#[test] -fn deterministic_analysis_extracts_request_templates_for_g3_business_stats_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_business_stats", - )) - .unwrap(); - - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let secondary = facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("businessStatsDetail")) - .expect("expected business stats secondary endpoint"); - let template = secondary - .request_template - .as_ref() - .and_then(serde_json::Value::as_object) - .expect("expected parsed request template for business stats secondary endpoint"); - assert!(template.contains_key("ticketNo")); -} - -#[test] -fn generator_blocks_incomplete_multi_mode_contract() { - let output_root = temp_workspace("sgclaw-scene-generator-g2-incomplete"); - let mut scene_ir = build_multi_mode_scene_ir(); - scene_ir.modes[0].request_template = serde_json::Value::Null; - scene_ir.modes[0].column_defs.clear(); - - let error = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/multi_mode"), - scene_id: "g2-incomplete-scene".to_string(), - scene_name: "G2涓嶅畬鏁村満鏅�".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), - }) - .expect_err("expected incomplete G2 contract to be blocked"); - - assert!(error - .to_string() - .contains("workflow evidence is incomplete for archetype multi_mode_request")); -} - -#[test] -fn generator_writes_multi_mode_package_with_generation_report() { - let output_root = temp_workspace("sgclaw-scene-generator-multi-mode"); - let scene_ir = build_multi_mode_scene_ir(); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/multi_mode"), - scene_id: "multi-mode-scene".to_string(), - scene_name: "多模式场景".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(); - let generated_manifest = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); - let generation_report = - fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(); - - assert!(generated_script.contains("const MODES =")); - assert!(generated_script.contains("function applyRequestMappings")); - assert!(generated_script.contains("period_payload: normalizePayload")); - assert!(generated_manifest.contains("resolver = \"dictionary_entity\"")); - assert!(generated_manifest.contains("resolver = \"month_week_period\"")); - assert!(generated_manifest.contains("[[request_mappings]]")); - assert!(generated_manifest.contains("source_field = \"org_code\"")); - assert!(generated_manifest.contains("target_field = \"orgno\"")); - assert!(generated_manifest.contains("app_entry_url = \"http://20.76.57.61:18080/gsllys\"")); - assert!(generated_manifest - .contains("module_route_url = \"http://20.76.57.61:18080/gsllys/monthReport\"")); - assert!(generated_manifest.contains("target_url_kind = \"runtime_context\"")); - assert!(generation_report.contains("\"workflowArchetype\": \"multi_mode_request\"")); - assert!(generation_report.contains("\"tjzq\": \"month\"")); - assert!(generation_report.contains("\"tjzq\": \"week\"")); - assert!(generation_report.contains("\"requestFieldMappings\"")); - assert!(generation_report.contains("\"sourceField\": \"org_code\"")); - assert!(generation_report.contains("\"sourceField\": \"period_payload.weekSfdate\"")); - assert!(generation_report.contains("\"appEntryUrl\": \"http://20.76.57.61:18080/gsllys\"")); - assert!(generation_report - .contains("\"moduleRouteUrl\": \"http://20.76.57.61:18080/gsllys/monthReport\"")); - assert!(generation_report.contains("\"targetUrlKind\": \"runtime_context\"")); - assert!(generation_report.contains("\"request_contract_complete\"")); - assert!(generation_report.contains("\"response_contract_complete\"")); - assert!(generation_report.contains("\"workflow_contract_complete\"")); - assert!(generation_report.contains("\"g2_modes_present\"")); - assert!(generation_report.contains("\"g2_request_contract_complete\"")); - assert!(generation_report.contains("\"g2_response_contract_complete\"")); - assert!(generation_report.contains("\"evidenceType\": \"bootstrap_candidate\"")); -} - -#[test] -fn generator_writes_multi_mode_package_from_deterministic_analysis() { - let facts = - extract_deterministic_scene_facts(Path::new("tests/fixtures/generated_scene/multi_mode")) - .unwrap(); - assert!(facts - .endpoints - .iter() - .all(|endpoint| endpoint.request_template.is_some())); - - let output_root = temp_workspace("sgclaw-scene-generator-multi-mode-auto"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/multi_mode"), - scene_id: "multi-mode-auto".to_string(), - scene_name: "多模式自动场景".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: None, - }) - .unwrap(); - - let generation_report = fs::read_to_string( - output_root.join("skills/multi-mode-auto/references/generation-report.json"), - ) - .unwrap(); - let org_dictionary = fs::read_to_string( - output_root.join("skills/multi-mode-auto/references/org-dictionary.json"), - ) - .unwrap(); - assert!(generation_report.contains("\"workflowArchetype\": \"multi_mode_request\"")); - assert!(generation_report.contains("\"name\": \"month\"")); - assert!(generation_report.contains("\"name\": \"week\"")); - assert!(generation_report.contains("\"g2_modes_present\"")); - assert!(generation_report.contains("\"g2_request_contract_complete\"")); - assert!(generation_report.contains("\"g2_response_contract_complete\"")); - assert_eq!(org_dictionary.trim(), "[]"); -} - -#[test] -fn generator_writes_real_sweep_030_org_dictionary_from_embedded_source() { - let output_root = temp_workspace("sgclaw-scene-generator-sweep-030-dictionary"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "D:/desk/智能体资料/全量业务场景/一平台场景/台区线损大数据-月_周累计线损率统计分析", - ), - scene_id: "sweep-030-scene".to_string(), - scene_name: "台区线损大数据-月_周累计线损率统计分析".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: None, - }) - .unwrap(); - - let org_dictionary = fs::read_to_string( - output_root.join("skills/sweep-030-scene/references/org-dictionary.json"), - ) - .unwrap(); - let scene_manifest = - fs::read_to_string(output_root.join("skills/sweep-030-scene/scene.toml")).unwrap(); - assert!(org_dictionary.contains("\"code\": \"62401\"")); - assert!(org_dictionary.contains("\"code\": \"62408\"")); - assert!(scene_manifest.contains("suffix = \"。。。\"")); - assert!(scene_manifest.contains("default_strategy = \"lineloss_page_semantics\"")); - assert!(scene_manifest.contains( - "\u{7ebf}\u{635f}\u{5927}\u{6570}\u{636e} \u{6708}\u{7d2f}\u{8ba1}\u{7ebf}\u{635f}\u{7edf}\u{8ba1}\u{5206}\u{6790}" - )); - assert!(scene_manifest.contains( - "\u{7ebf}\u{635f}\u{5927}\u{6570}\u{636e} \u{5468}\u{7d2f}\u{8ba1}\u{7ebf}\u{635f}\u{7edf}\u{8ba1}\u{5206}\u{6790}" - )); - assert!(scene_manifest.contains("\u{53f0}\u{533a}\u{7ebf}\u{635f}")); -} - -#[test] -fn generator_escapes_request_mapping_fields_for_valid_toml() { - let output_root = temp_workspace("sgclaw-scene-generator-sweep-078-valid-toml"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("D:/desk/智能体资料/全量业务场景/一平台场景/线损同期差异报表"), - scene_id: "sweep-078-scene".to_string(), - scene_name: "线损同期差异报表".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: None, - }) - .unwrap(); - - let scene_manifest = - fs::read_to_string(output_root.join("skills/sweep-078-scene/scene.toml")).unwrap(); - assert!(scene_manifest.contains("suffix = \"。。。\"")); - toml::from_str::(&scene_manifest).unwrap(); -} - -#[test] -fn analyzer_recovers_local_doc_residual_export_workflow_evidence() { - let cases = [ - ( - "D:/desk/智能体资料/全量业务场景/一平台场景/力禾动环系统巡视记录", - "docExport", - ), - ( - "D:/desk/智能体资料/全量业务场景/一平台场景/基于全量报表的出口版本", - "docTemplateTransform", - ), - ( - "D:/desk/智能体资料/全量业务场景/一平台场景/安全督查周通报", - "docExport", - ), - ( - "D:/desk/智能体资料/全量业务场景/一平台场景/巡视计划完成情况自动检索", - "docExport", - ), - ( - "D:/desk/智能体资料/全量业务场景/一平台场景/消防工作分析报告", - "docExport", - ), - ( - "D:/desk/智能体资料/全量业务场景/一平台场景/输变电设备运行分析报告", - "reportLogSet", - ), - ]; - - for (source_dir, expected_action) in cases { - let facts = extract_deterministic_scene_facts(Path::new(source_dir)).unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::LocalDocPipeline, - "source_dir={source_dir}" - ); - assert!( - facts - .local_doc_pipeline_actions - .iter() - .any(|action| action == expected_action), - "source_dir={source_dir}; expected_action={expected_action}; actions={:?}", - facts.local_doc_pipeline_actions - ); - assert!( - !facts.local_doc_pipeline_actions.is_empty(), - "source_dir={source_dir}" - ); - } -} - -#[test] -fn generator_recovers_local_doc_residual_packages_from_source_evidence() { - let cases = [ - ( - "sweep-025-scene", - "力禾动环系统巡视记录", - "D:/desk/智能体资料/全量业务场景/一平台场景/力禾动环系统巡视记录", - ), - ( - "sweep-047-scene", - "基于全量报表的出口版本", - "D:/desk/智能体资料/全量业务场景/一平台场景/基于全量报表的出口版本", - ), - ( - "sweep-050-scene", - "安全督查周通报", - "D:/desk/智能体资料/全量业务场景/一平台场景/安全督查周通报", - ), - ( - "sweep-052-scene", - "巡视计划完成情况自动检索", - "D:/desk/智能体资料/全量业务场景/一平台场景/巡视计划完成情况自动检索", - ), - ( - "sweep-062-scene", - "消防工作分析报告", - "D:/desk/智能体资料/全量业务场景/一平台场景/消防工作分析报告", - ), - ( - "sweep-087-scene", - "输变电设备运行分析报告", - "D:/desk/智能体资料/全量业务场景/一平台场景/输变电设备运行分析报告", - ), - ]; - - for (scene_id, scene_name, source_dir) in cases { - let source_dir = PathBuf::from(source_dir); - if !source_dir.exists() { - eprintln!("skipping {scene_id} local-doc residual regression: source dir not found"); - continue; - } - - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir, - scene_id: scene_id.to_string(), - scene_name: scene_name.to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root: temp_workspace(&format!("sgclaw-{scene_id}-local-doc-residual")), - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap_or_else(|err| { - panic!("{scene_id} should generate after local-doc residual closure: {err}") - }); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::LocalDocPipeline, - "scene_id={scene_id}" - ); - assert!( - generated_report - .workflow_steps - .iter() - .any(|step| step.step_type == "doc_export"), - "scene_id={scene_id}; steps={:?}", - generated_report.workflow_steps - ); - assert!( - skill_root.join("SKILL.toml").exists(), - "scene_id={scene_id}" - ); - assert!( - skill_root.join("scene.toml").exists(), - "scene_id={scene_id}" - ); - } -} - -#[test] -fn analyzer_recovers_lineloss_period_default_strategy_from_source() { - let facts = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/台区线损大数据-月_周累计线损率统计分析", - )) - .unwrap(); - - assert_eq!( - facts.period_default_strategy.as_deref(), - Some("lineloss_page_semantics") - ); -} - -#[test] -fn generator_fills_empty_g2_mode_request_templates() { - let output_root = temp_workspace("sgclaw-scene-generator-g2-empty-template-fill"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g2_noisy_multi_mode"), - scene_id: "g2-empty-template-fill".to_string(), - scene_name: "G2 empty request template fill".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: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string( - output_root.join("skills/g2-empty-template-fill/references/generation-report.json"), - ) - .unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::MultiModeRequest - ); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g2_request_contract_complete" && gate.passed)); - assert!(generated_report.modes.iter().all(|mode| mode - .request_template - .as_object() - .is_some_and(|template| !template.is_empty()))); - assert!(generated_report - .modes - .iter() - .filter(|mode| mode.name == "month" || mode.name == "week") - .all(|mode| mode - .request_template - .as_object() - .and_then(|template| template.get("tjzq")) - .and_then(serde_json::Value::as_str) - == Some(mode.name.as_str()))); -} - -#[test] -fn generator_writes_paginated_enrichment_without_forced_org_period_defaults() { - let output_root = temp_workspace("sgclaw-scene-generator-paginated"); - let scene_ir = build_paginated_scene_ir(); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/paginated_enrichment"), - scene_id: "paginated-scene".to_string(), - scene_name: "分页补数场景".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/paginated-scene"); - let generated_script = - fs::read_to_string(skill_root.join("scripts/collect_paginated_scene.js")).unwrap(); - let generated_manifest = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); - let generation_report = - fs::read_to_string(skill_root.join("references/generation-report.md")).unwrap(); - - assert!(generated_script.contains("async function queryPrimaryPage")); - assert!(generated_script.contains("async function querySecondary")); - assert!(generated_script.contains("function applyFilter")); - assert!(!generated_manifest.contains("resolver = \"dictionary_entity\"")); - assert!(!generated_manifest.contains("resolver = \"month_week_period\"")); - assert!(generation_report.contains("Readiness")); - let generation_report_json = - fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(); - assert!(generation_report_json.contains("\"mainRequest\"")); - assert!(generation_report_json.contains("\"paginationPlan\"")); - assert!(generation_report_json.contains("\"enrichmentRequests\"")); - assert!(generation_report_json.contains("\"joinKeys\"")); - assert!(generation_report_json.contains("\"mergeOrDedupeRules\"")); - assert!(generation_report_json.contains("\"exportPlan\"")); - assert!(generation_report_json.contains("\"g3_main_request_resolved\"")); - assert!(generation_report_json.contains("\"g3_pagination_contract_complete\"")); - assert!(generation_report_json.contains("\"g3_enrichment_contract_complete\"")); - assert!(generation_report_json.contains("\"g3_join_key_resolved\"")); - assert!(generation_report_json.contains("\"g3_export_path_identified\"")); - assert!(generation_report_json.contains("\"g3_runtime_scope_compatible\"")); -} - -#[test] -fn paginated_enrichment_p0_regression_checklist_is_actionable() { - let output_root = temp_workspace("sgclaw-g3-p0-regression"); - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/paginated_enrichment"), - scene_id: "g3-p0-regression".to_string(), - scene_name: "G3 P0 Regression".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert_eq!(generated_report.readiness.level, "A"); - assert_eq!( - generated_report - .main_request - .as_ref() - .map(|request| request.response_path.as_str()), - Some("rows") - ); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("page") - ); - assert_eq!(generated_report.join_keys, vec!["custNo".to_string()]); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:charge")); - assert_eq!( - generated_report - .export_plan - .as_ref() - .and_then(|plan| plan.entry.as_deref()), - Some("exportExcel") - ); - assert!(generated_report - .evidence - .iter() - .all(|item| item.evidence_type != "localhost_dependency_candidate")); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| { gate.name == "g3_join_key_resolved" && gate.passed })); -} - -#[test] -fn generator_writes_paginated_enrichment_family_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - assert!(facts.endpoints.len() >= 2); - assert!(facts - .endpoints - .iter() - .find(|endpoint| endpoint.url.contains("ticketRiskDetail")) - .and_then(|endpoint| endpoint.request_template.as_ref()) - .and_then(serde_json::Value::as_object) - .map(|template| template.contains_key("ticketNo")) - .unwrap_or(false)); - - let output_root = temp_workspace("sgclaw-g3-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/paginated_enrichment_expansion"), - scene_id: "g3-family-expansion".to_string(), - scene_name: "G3 Family Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!( - "g3 expansion fixture should reuse paginated enrichment contract; error={err}; pagination={:?}; secondary={:?}; filters={:?}; exports={:?}; response_paths={:?}; join_keys={:?}", - facts.pagination_fields, - facts.secondary_request_methods, - facts.filter_expressions, - facts.export_methods, - facts.response_paths, - facts.g1e_join_keys, - ); - } - let skill_root = generation.unwrap(); - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("pageNum") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "ticketNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:riskLevel")); - assert_eq!( - generated_report - .export_plan - .as_ref() - .and_then(|plan| plan.entry.as_deref()), - Some("exportExcel") - ); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| { gate.name == "g3_main_request_resolved" && gate.passed })); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| { gate.name == "g3_join_key_resolved" && gate.passed })); -} - -#[test] -fn generator_writes_paginated_enrichment_workorder_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_workorder", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - - let output_root = temp_workspace("sgclaw-g3-workorder-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_workorder", - ), - scene_id: "g3-family-expansion-workorder".to_string(), - scene_name: "G3 WorkOrder Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!( - "g3 workorder expansion fixture should reuse paginated enrichment contract; error={err}; pagination={:?}; secondary={:?}; filters={:?}; exports={:?}; response_paths={:?}; join_keys={:?}", - facts.pagination_fields, - facts.secondary_request_methods, - facts.filter_expressions, - facts.export_methods, - facts.response_paths, - facts.g1e_join_keys, - ); - } - let skill_root = generation.unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("pageNo") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "workOrderNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:sourceType")); -} - -#[test] -fn generator_writes_paginated_enrichment_orderno_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_orderno", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - - let output_root = temp_workspace("sgclaw-g3-orderno-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_orderno", - ), - scene_id: "g3-family-expansion-orderno".to_string(), - scene_name: "G3 OrderNo Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!( - "g3 orderno expansion fixture should reuse paginated enrichment contract; error={err}; pagination={:?}; secondary={:?}; filters={:?}; exports={:?}; response_paths={:?}; join_keys={:?}", - facts.pagination_fields, - facts.secondary_request_methods, - facts.filter_expressions, - facts.export_methods, - facts.response_paths, - facts.g1e_join_keys, - ); - } - let skill_root = generation.unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("page") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "orderNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:sourceType")); -} - -#[test] -fn generator_writes_paginated_enrichment_source_distribution_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_source_distribution", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - - let output_root = temp_workspace("sgclaw-g3-source-distribution-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_source_distribution", - ), - scene_id: "g3-family-expansion-source-distribution".to_string(), - scene_name: "G3 Source Distribution Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!( - "g3 source distribution expansion fixture should reuse paginated enrichment contract; error={err}; pagination={:?}; secondary={:?}; filters={:?}; exports={:?}; response_paths={:?}; join_keys={:?}", - facts.pagination_fields, - facts.secondary_request_methods, - facts.filter_expressions, - facts.export_methods, - facts.response_paths, - facts.g1e_join_keys, - ); - } - let skill_root = generation.unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("pageNum") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "ticketNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:sourceType")); -} - -#[test] -fn generator_writes_paginated_enrichment_service_risk_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_service_risk", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - - let output_root = temp_workspace("sgclaw-g3-service-risk-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_service_risk", - ), - scene_id: "g3-family-expansion-service-risk".to_string(), - scene_name: "G3 Service Risk Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!( - "g3 service risk expansion fixture should reuse paginated enrichment contract; error={err}; pagination={:?}; secondary={:?}; filters={:?}; exports={:?}; response_paths={:?}; join_keys={:?}", - facts.pagination_fields, - facts.secondary_request_methods, - facts.filter_expressions, - facts.export_methods, - facts.response_paths, - facts.g1e_join_keys, - ); - } - let skill_root = generation.unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("pageNo") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "ticketNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:riskLevel")); -} - -#[test] -fn generator_writes_paginated_enrichment_timeout_warning_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_timeout_warning", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - - let output_root = temp_workspace("sgclaw-g3-timeout-warning-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_timeout_warning", - ), - scene_id: "g3-family-expansion-timeout-warning".to_string(), - scene_name: "G3 Timeout Warning Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!( - "g3 timeout warning expansion fixture should reuse paginated enrichment contract; error={err}; pagination={:?}; secondary={:?}; filters={:?}; exports={:?}; response_paths={:?}; join_keys={:?}", - facts.pagination_fields, - facts.secondary_request_methods, - facts.filter_expressions, - facts.export_methods, - facts.response_paths, - facts.g1e_join_keys, - ); - } - let skill_root = generation.unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("pageNum") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "ticketNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:riskLevel")); -} - -#[test] -fn generator_recovers_g3_enrichment_requests_from_secondary_request_signals() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - assert!(!facts.secondary_request_methods.is_empty()); - - let output_root = temp_workspace("sgclaw-g3-enrichment-request-recovery"); - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/paginated_enrichment_expansion"), - scene_id: "g3-enrichment-request-recovery".to_string(), - scene_name: "G3 Enrichment Request Recovery".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(!generated_report.enrichment_requests.is_empty()); - assert!(generated_report - .enrichment_requests - .iter() - .all(|item| item.api_endpoint.is_some())); - assert!( - generated_report - .workflow_evidence - .secondary_request_entries - .len() - >= 1 - ); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| { gate.name == "g3_enrichment_contract_complete" && gate.passed })); -} - -#[test] -fn analyzer_collects_word_export_as_g3_export_signal() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_business_stats", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - assert!(facts - .export_methods - .iter() - .any(|item| item.to_ascii_lowercase().contains("export") - || item.to_ascii_lowercase().contains("word"))); -} - -#[test] -fn generator_writes_paginated_enrichment_device_monitor_weekly_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_device_monitor_weekly", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let output_root = temp_workspace("sgclaw-g3-device-monitor-weekly-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_device_monitor_weekly", - ), - scene_id: "g3-family-expansion-device-monitor-weekly".to_string(), - scene_name: "G3 Device Monitor Weekly Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!("g3 device monitor weekly expansion fixture should reuse paginated enrichment contract; error={err}"); - } - let skill_root = generation.unwrap(); - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("pageNo") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "ticketNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:sourceType")); -} - -#[test] -fn generator_writes_paginated_enrichment_customer_satisfaction_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_customer_satisfaction", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let output_root = temp_workspace("sgclaw-g3-customer-satisfaction-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_customer_satisfaction", - ), - scene_id: "g3-family-expansion-customer-satisfaction".to_string(), - scene_name: "G3 Customer Satisfaction Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!("g3 customer satisfaction expansion fixture should reuse paginated enrichment contract; error={err}"); - } - let skill_root = generation.unwrap(); - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("page") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "ticketNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:sourceType")); -} - -#[test] -fn generator_writes_paginated_enrichment_repair_return_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_repair_return", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let output_root = temp_workspace("sgclaw-g3-repair-return-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_repair_return", - ), - scene_id: "g3-family-expansion-repair-return".to_string(), - scene_name: "G3 Repair Return Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!("g3 repair return expansion fixture should reuse paginated enrichment contract; error={err}"); - } - let skill_root = generation.unwrap(); - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("pageNum") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "ticketNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:riskLevel")); -} - -#[test] -fn generator_writes_paginated_enrichment_repair_daily_control_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_repair_daily_control", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let output_root = temp_workspace("sgclaw-g3-repair-daily-control-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_repair_daily_control", - ), - scene_id: "g3-family-expansion-repair-daily-control".to_string(), - scene_name: "G3 Repair Daily Control Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!("g3 repair daily control expansion fixture should reuse paginated enrichment contract; error={err}"); - } - let skill_root = generation.unwrap(); - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("pageNo") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "ticketNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:riskLevel")); -} - -#[test] -fn generator_writes_paginated_enrichment_business_stats_expansion_fixture() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_business_stats", - )) - .unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::PaginatedEnrichment - ); - let output_root = temp_workspace("sgclaw-g3-business-stats-expansion-fixture"); - let generation = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "tests/fixtures/generated_scene/paginated_enrichment_expansion_business_stats", - ), - scene_id: "g3-family-expansion-business-stats".to_string(), - scene_name: "G3 Business Stats Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }); - if let Err(err) = &generation { - panic!("g3 business stats expansion fixture should reuse paginated enrichment contract; error={err}"); - } - let skill_root = generation.unwrap(); - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert_eq!( - generated_report - .pagination_plan - .as_ref() - .map(|plan| plan.page_field.as_str()), - Some("page") - ); - assert!(generated_report - .join_keys - .iter() - .any(|key| key == "ticketNo")); - assert!(generated_report - .merge_or_dedupe_rules - .iter() - .any(|rule| rule == "aggregate:sourceType")); -} - -#[test] -fn generator_writes_g1e_light_enrichment_package() { - let output_root = temp_workspace("sgclaw-g1e-package"); - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g1e_light_enrichment"), - scene_id: "g1e-light-enrichment".to_string(), - scene_name: "高低压新增报装容量月度统计表".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: None, - }) - .unwrap(); - - let generation_report = - fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(); - let browser_script = - fs::read_to_string(skill_root.join("scripts/collect_g1e_light_enrichment.js")).unwrap(); - - assert!(generation_report.contains("\"workflowArchetype\": \"single_request_enrichment\"")); - assert!(generation_report.contains("\"mainRequest\"")); - assert!(generation_report.contains("\"enrichmentRequests\"")); - assert!(generation_report.contains("\"mergePlan\"")); - assert!(generation_report.contains("getWkorderAll")); - assert!(generation_report.contains("queryElectCustInfo")); - assert!(generation_report.contains("countyCodeName")); - assert!(browser_script.contains("const MAIN_REQUEST =")); - assert!(browser_script.contains("const ENRICHMENT_REQUESTS =")); - assert!(browser_script.contains("const MERGE_PLAN =")); -} - -#[test] -fn generator_writes_g1e_light_enrichment_expansion_package() { - let output_root = temp_workspace("sgclaw-g1e-expansion-package"); - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g1e_light_enrichment_expansion"), - scene_id: "g1e-light-enrichment-expansion".to_string(), - scene_name: "G1-E Light Enrichment Expansion".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::SingleRequestEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert!(generated_report - .main_request - .as_ref() - .and_then(|request| request.api_endpoint.as_ref()) - .map(|endpoint| endpoint.name.contains("getWkorderAll")) - .unwrap_or(false)); - assert!(generated_report - .enrichment_requests - .iter() - .any(|request| request.name.contains("queryMeterInfo"))); - let merge_plan = generated_report - .merge_plan - .as_ref() - .expect("expected g1e merge plan"); - assert!(merge_plan.join_keys.iter().any(|key| key == "wkOrderNo")); - assert!(merge_plan - .aggregate_rules - .iter() - .any(|rule| rule == "group_by:countyCodeName")); - assert!(merge_plan - .output_columns - .iter() - .any(|(field, _)| field == "meterCapacityThisMonth")); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| { gate.name == "g1e_scope_compatible" && gate.passed })); -} - -#[test] -fn generator_writes_g1e_light_enrichment_additional_package() { - let output_root = temp_workspace("sgclaw-g1e-additional-package"); - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g1e_light_enrichment_additional"), - scene_id: "g1e-light-enrichment-additional".to_string(), - scene_name: "G1-E Light Enrichment Additional".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::SingleRequestEnrichment - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert!(generated_report - .main_request - .as_ref() - .and_then(|request| request.api_endpoint.as_ref()) - .map(|endpoint| endpoint.name.contains("getWkorderAll")) - .unwrap_or(false)); - assert!(generated_report - .enrichment_requests - .iter() - .any(|request| request.name.contains("queryBusAcpt"))); - let merge_plan = generated_report - .merge_plan - .as_ref() - .expect("expected g1e additional merge plan"); - assert!(merge_plan.join_keys.iter().any(|key| key == "wkOrderNo")); - assert!(merge_plan - .aggregate_rules - .iter() - .any(|rule| rule == "group_by:countyCodeName")); - assert!(merge_plan - .output_columns - .iter() - .any(|(field, _)| field == "batchCapacityThisMonth")); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| { gate.name == "g1e_scope_compatible" && gate.passed })); -} - -#[test] -fn analyzer_recovers_g1e_output_columns_from_cols_push_and_wide_title_list() { - let quality_facts = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/业扩报装质量评价体系", - )) - .unwrap(); - assert_eq!( - quality_facts.workflow_archetype, - WorkflowArchetype::SingleRequestEnrichment - ); - assert!(quality_facts - .g1e_output_columns - .iter() - .any(|(field, _)| field == "dyfjmZs")); - assert!(quality_facts - .g1e_output_columns - .iter() - .any(|(field, _)| field == "gdfndfZs")); - - let install_facts = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/用电报装信息统计列表", - )) - .unwrap(); - assert_eq!( - install_facts.workflow_archetype, - WorkflowArchetype::SingleRequestEnrichment - ); - assert!(install_facts - .g1e_output_columns - .iter() - .any(|(field, _)| field == "wkOrderNo")); - assert!(install_facts - .g1e_output_columns - .iter() - .any(|(field, _)| field == "appCtrtCap")); -} - -#[test] -fn generator_writes_g2_weekly_single_mode_package() { - let output_root = temp_workspace("sgclaw-scene-generator-g2-weekly-single"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g2_weekly_single_mode"), - scene_id: "g2-weekly-single".to_string(), - scene_name: "G2 Weekly Single".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: None, - }) - .unwrap(); - - let generation_report = fs::read_to_string( - output_root.join("skills/g2-weekly-single/references/generation-report.json"), - ) - .unwrap(); - - assert!(generation_report.contains("\"workflowArchetype\": \"multi_mode_request\"")); - assert!(generation_report.contains("\"name\": \"week\"")); - assert!(generation_report.contains("\"LINE_LOSS_RATE\"")); - assert!(generation_report.contains("\"g2_request_contract_complete\"")); - assert!(generation_report.contains("\"g2_response_contract_complete\"")); -} - -#[test] -fn generator_writes_g2_mixed_linked_workflow_package() { - let output_root = temp_workspace("sgclaw-scene-generator-g2-mixed-linked"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g2_mixed_linked_workflow"), - scene_id: "g2-mixed-linked".to_string(), - scene_name: "G2 Mixed Linked".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: None, - }) - .unwrap(); - - let generation_report = fs::read_to_string( - output_root.join("skills/g2-mixed-linked/references/generation-report.json"), - ) - .unwrap(); - - assert!(generation_report.contains("\"workflowArchetype\": \"multi_mode_request\"")); - assert!(generation_report.contains("\"name\": \"primary\"")); - assert!(generation_report.contains("\"TG_NO\"")); - assert!(generation_report.contains("\"g2_request_contract_complete\"")); - assert!(generation_report.contains("\"g2_response_contract_complete\"")); -} - -#[test] -fn generator_writes_g2_comparison_crosscheck_package() { - let output_root = temp_workspace("sgclaw-scene-generator-g2-comparison"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g2_comparison_crosscheck"), - scene_id: "g2-comparison".to_string(), - scene_name: "G2 Comparison".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: None, - }) - .unwrap(); - - let generation_report = fs::read_to_string( - output_root.join("skills/g2-comparison/references/generation-report.json"), - ) - .unwrap(); - - assert!(generation_report.contains("\"workflowArchetype\": \"multi_mode_request\"")); - assert!(generation_report.contains("\"name\": \"comparison\"")); - assert!(generation_report.contains("\"TG_NO\"")); - assert!(generation_report.contains("\"consno\"")); - assert!(generation_report.contains("\"g2_request_contract_complete\"")); - assert!(generation_report.contains("\"g2_response_contract_complete\"")); -} - -#[test] -fn generator_writes_g2_diagnosis_drilldown_package() { - let output_root = temp_workspace("sgclaw-scene-generator-g2-diagnosis"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g2_diagnosis_drilldown"), - scene_id: "g2-diagnosis".to_string(), - scene_name: "G2 Diagnosis".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: None, - }) - .unwrap(); - - let generation_report = fs::read_to_string( - output_root.join("skills/g2-diagnosis/references/generation-report.json"), - ) - .unwrap(); - - assert!(generation_report.contains("\"workflowArchetype\": \"multi_mode_request\"")); - assert!(generation_report.contains("\"name\": \"diagnosis\"")); - assert!(generation_report.contains("\"LOSS_PQ\"")); - assert!(generation_report.contains("\"remark\"")); - assert!(generation_report.contains("\"g2_request_contract_complete\"")); - assert!(generation_report.contains("\"g2_response_contract_complete\"")); -} - -#[test] -fn generator_writes_g2_prediction_compute_package() { - let output_root = temp_workspace("sgclaw-scene-generator-g2-prediction"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g2_prediction_compute"), - scene_id: "g2-prediction".to_string(), - scene_name: "G2 Prediction".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: None, - }) - .unwrap(); - - let generation_report = fs::read_to_string( - output_root.join("skills/g2-prediction/references/generation-report.json"), - ) - .unwrap(); - - assert!(generation_report.contains("\"workflowArchetype\": \"multi_mode_request\"")); - assert!(generation_report.contains("\"name\": \"prediction\"")); - assert!(generation_report.contains("\"lineId\"")); - assert!(generation_report.contains("\"lineLossRate\"")); - assert!(generation_report.contains("\"powerLoss\"")); - assert!(generation_report.contains("\"g2_request_contract_complete\"")); - assert!(generation_report.contains("\"g2_response_contract_complete\"")); -} - -#[test] -fn generator_blocks_incomplete_paginated_enrichment_workflow() { - let output_root = temp_workspace("sgclaw-scene-generator-paginated-incomplete"); - let mut scene_ir = build_paginated_scene_ir(); - scene_ir - .workflow_steps - .retain(|step| step.step_type != "filter" && step.step_type != "export"); - - let error = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/paginated_enrichment"), - scene_id: "paginated-scene-incomplete".to_string(), - scene_name: "分页补数场景-不完整".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), - }) - .expect_err("expected incomplete paginated workflow to be blocked"); - - assert!(error - .to_string() - .contains("workflow evidence is incomplete for archetype paginated_enrichment")); - - let report_path = - output_root.join("skills/paginated-scene-incomplete/references/generation-report.json"); - let report: serde_json::Value = - serde_json::from_str(&fs::read_to_string(report_path).unwrap()).unwrap(); - assert_eq!(report["generationStatus"], "fail-closed"); - assert_eq!(report["failureStage"], "readiness-before-report"); - assert_eq!( - report["contractSnapshot"]["workflowArchetype"], - "paginated_enrichment" - ); - assert!(report["contractSnapshot"]["mainRequest"].is_object()); - assert!(report["contractSnapshot"]["paginationPlan"].is_object()); - assert!(report["contractSnapshot"]["enrichmentRequests"].is_array()); - assert!(report["contractSnapshot"]["joinKeys"].is_array()); -} - -#[test] -fn generator_blocks_incomplete_paginated_response_contract() { - let output_root = temp_workspace("sgclaw-scene-generator-paginated-response-incomplete"); - let mut scene_ir = build_paginated_scene_ir(); - scene_ir.response_path = String::new(); - - let error = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/paginated_enrichment"), - scene_id: "paginated-scene-response-incomplete".to_string(), - scene_name: "分页补数场景-响应不完整".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: Some(scene_ir), - }) - .expect_err("expected incomplete paginated response contract to be blocked"); - - assert!(error - .to_string() - .contains("workflow evidence is incomplete for archetype paginated_enrichment")); -} - -#[test] -fn generator_blocks_paginated_enrichment_without_join_keys() { - let output_root = temp_workspace("sgclaw-scene-generator-paginated-join-missing"); - let mut scene_ir = build_paginated_scene_ir(); - scene_ir.join_keys.clear(); - - let error = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/paginated_enrichment"), - scene_id: "paginated-scene-join-missing".to_string(), - scene_name: "鍒嗛〉琛ユ暟鍦烘櫙-鍏宠仈閿己澶?".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: Some(scene_ir), - }) - .expect_err("expected paginated enrichment without join keys to be blocked"); - - assert!(error - .to_string() - .contains("workflow evidence is incomplete for archetype paginated_enrichment")); -} - -#[test] -fn generator_writes_g6_host_bridge_workflow_package() { - let output_root = temp_workspace("sgclaw-g6-host-bridge"); - - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g6_host_bridge_workflow"), - scene_id: "g6-host-bridge".to_string(), - scene_name: "G6 Host Bridge Workflow".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - let browser_script = - fs::read_to_string(skill_root.join("scripts/collect_g6_host_bridge.js")).unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::HostBridgeWorkflow - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g6_host_bridge_detected" && gate.passed)); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g6_fail_closed" && gate.passed)); - assert!(browser_script.contains("const WORKFLOW_STEPS =")); - assert!(browser_script.contains("host_bridge_workflow")); - assert!(browser_script.contains("invokeHostBridge")); - assert!(browser_script.contains("queryCallbackEndpoint")); -} - -#[test] -fn generator_writes_fixed_real_g6_sample_as_host_bridge_workflow() { - let output_root = temp_workspace("sgclaw-g6-real-host-bridge"); - - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from( - "D:/desk/智能体资料/全量业务场景/一平台场景/电能表现场检验完成率指标报表", - ), - scene_id: "g6-real-meter-inspection-completion-test".to_string(), - scene_name: "电能表现场检验完成率指标报表".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::HostBridgeWorkflow - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g6_host_bridge_detected" && gate.passed)); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g6_fail_closed" && gate.passed)); -} - -#[test] -fn generator_blocks_incomplete_g6_host_bridge_contract() { - let output_root = temp_workspace("sgclaw-g6-host-bridge-incomplete"); - let mut scene_ir = SceneIr { - scene_id: "g6-host-bridge-incomplete".to_string(), - scene_id_diagnostics: SceneIdDiagnosticsIr::default(), - scene_name: "G6 Host Bridge Incomplete".to_string(), - scene_kind: "report_collection".to_string(), - workflow_archetype: Some(WorkflowArchetype::HostBridgeWorkflow), - bootstrap: BootstrapIr { - expected_domain: "yx.gscc.com.cn".to_string(), - target_url: "http://yx.gscc.com.cn".to_string(), - app_entry_url: "http://yx.gscc.com.cn".to_string(), - module_route_url: String::new(), - target_url_kind: Some("runtime_context".to_string()), - requires_target_page: true, - page_title_keywords: vec!["G6".to_string()], - source: Some("test".to_string()), - }, - params: Vec::new(), - modes: Vec::new(), - default_mode: None, - mode_switch_field: None, - workflow_steps: Vec::new(), - workflow_evidence: WorkflowEvidenceIr::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: String::new(), - normalize_rules: None, - artifact_contract: ArtifactContractIr::default(), - validation_hints: ValidationHintsIr::default(), - evidence: Vec::new(), - readiness: ReadinessIr::default(), - api_endpoints: Vec::new(), - runtime_dependencies: Vec::new(), - static_params: Default::default(), - column_defs: Vec::new(), - confidence: 0.7, - uncertainties: Vec::new(), - monitoring_action_workflow: None, - }; - scene_ir.workflow_steps.push(WorkflowStepIr { - step_type: "host_bridge".to_string(), - entry: Some("sgBrowerserJsAjax2".to_string()), - ..WorkflowStepIr::default() - }); - - let error = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g6_host_bridge_workflow"), - scene_id: "g6-host-bridge-incomplete".to_string(), - scene_name: "G6 Host Bridge Incomplete".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: Some(scene_ir), - }) - .expect_err("expected incomplete G6 host bridge workflow to fail closed"); - - assert!(error - .to_string() - .contains("workflow evidence is incomplete for archetype host_bridge_workflow")); -} - -#[test] -fn generator_writes_g7_multi_endpoint_inventory_package() { - let output_root = temp_workspace("sgclaw-g7-inventory"); - - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g7_multi_endpoint_inventory"), - scene_id: "g7-inventory".to_string(), - scene_name: "G7 Multi Endpoint Inventory".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - let browser_script = - fs::read_to_string(skill_root.join("scripts/collect_g7_inventory.js")).unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::MultiEndpointInventory - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g7_inventory_endpoints_detected" && gate.passed)); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g7_fail_closed" && gate.passed)); - assert!(browser_script.contains("multi_endpoint_inventory")); - assert!(browser_script.contains("inventoryEndpoints")); - assert!(browser_script.contains("aggregateEntry")); -} - -#[test] -fn generator_blocks_incomplete_g7_inventory_contract() { - let output_root = temp_workspace("sgclaw-g7-inventory-incomplete"); - let scene_ir = SceneIr { - scene_id: "g7-inventory-incomplete".to_string(), - scene_id_diagnostics: SceneIdDiagnosticsIr::default(), - scene_name: "G7 Inventory Incomplete".to_string(), - scene_kind: "report_collection".to_string(), - workflow_archetype: Some(WorkflowArchetype::MultiEndpointInventory), - bootstrap: BootstrapIr { - expected_domain: "yx.gscc.com.cn".to_string(), - target_url: "http://yx.gscc.com.cn/asset".to_string(), - app_entry_url: "http://yx.gscc.com.cn/asset".to_string(), - module_route_url: String::new(), - target_url_kind: Some("runtime_context".to_string()), - requires_target_page: true, - page_title_keywords: vec!["G7".to_string()], - source: Some("test".to_string()), - }, - params: Vec::new(), - modes: Vec::new(), - default_mode: None, - mode_switch_field: None, - workflow_steps: vec![ - WorkflowStepIr { - step_type: "inventory_request".to_string(), - endpoint: Some("assetStatsQueryMeter".to_string()), - ..WorkflowStepIr::default() - }, - WorkflowStepIr { - step_type: "transform".to_string(), - entry: Some("aggregateInventory".to_string()), - ..WorkflowStepIr::default() - }, - ], - workflow_evidence: WorkflowEvidenceIr::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: String::new(), - normalize_rules: None, - artifact_contract: ArtifactContractIr::default(), - validation_hints: ValidationHintsIr::default(), - evidence: Vec::new(), - readiness: ReadinessIr::default(), - api_endpoints: vec![ApiEndpointIr { - name: "assetStatsQueryMeter".to_string(), - url: "http://yx.gscc.com.cn/asset/assetStatsQueryMeter".to_string(), - method: "POST".to_string(), - content_type: Some("application/json".to_string()), - description: None, - }], - runtime_dependencies: Vec::new(), - static_params: Default::default(), - column_defs: Vec::new(), - confidence: 0.7, - uncertainties: Vec::new(), - monitoring_action_workflow: None, - }; - - let error = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g7_multi_endpoint_inventory"), - scene_id: "g7-inventory-incomplete".to_string(), - scene_name: "G7 Inventory Incomplete".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: Some(scene_ir), - }) - .expect_err("expected incomplete G7 inventory workflow to fail closed"); - - assert!(error - .to_string() - .contains("workflow evidence is incomplete for archetype multi_endpoint_inventory")); -} - -#[test] -fn generator_writes_g8_local_doc_pipeline_package() { - let output_root = temp_workspace("sgclaw-g8-doc-pipeline"); - - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g8_local_doc_pipeline"), - scene_id: "g8-doc-pipeline".to_string(), - scene_name: "G8 Local Document Pipeline".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - let browser_script = - fs::read_to_string(skill_root.join("scripts/collect_g8_doc_pipeline.js")).unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::LocalDocPipeline - ); - assert!(matches!( - generated_report.readiness.level.as_str(), - "A" | "B" - )); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g8_local_doc_pipeline_detected" && gate.passed)); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g8_fail_closed" && gate.passed)); - assert!(browser_script.contains("local_doc_pipeline")); - assert!(browser_script.contains("localDataEndpoints")); - assert!(browser_script.contains("docExportEntry")); -} - -#[test] -fn generator_recovers_sweep_012_report_log_doc_pipeline_package() { - let source_dir = PathBuf::from("D:/desk/智能体资料/全量业务场景/一平台场景/业扩报装管理制度"); - if !source_dir.exists() { - eprintln!("skipping sweep-012 real source regression: source dir not found"); - return; - } - - let facts = extract_deterministic_scene_facts(source_dir.as_path()).unwrap(); - assert_eq!( - facts.workflow_archetype, - WorkflowArchetype::LocalDocPipeline - ); - assert!(facts - .local_doc_pipeline_actions - .iter() - .any(|action| action == "reportLogQuery")); - assert!(facts - .local_doc_pipeline_actions - .iter() - .any(|action| action == "docExport")); - - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir, - scene_id: "sweep-012-scene".to_string(), - scene_name: "业扩报装管理制度".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root: temp_workspace("sgclaw-sweep-012-recovery"), - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::LocalDocPipeline - ); - assert_eq!(generated_report.readiness.level, "A"); - assert!(skill_root.join("SKILL.toml").exists()); - assert!(skill_root.join("scene.toml").exists()); -} - -#[test] -fn generator_writes_g3_g8_mixed_boundary_fixture_as_paginated_enrichment() { - let request = GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g3_g8_mixed_boundary"), - scene_id: "g3-g8-mixed-boundary".to_string(), - scene_name: "G3 G8 Mixed Boundary".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root: temp_workspace("sgclaw-g3-g8-mixed-boundary"), - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }; - let analysis = analyze_scene_source(request.source_dir.as_path()).unwrap(); - let facts = extract_deterministic_scene_facts(request.source_dir.as_path()).unwrap(); - let scene_ir = resolve_scene_ir_for_test(&request, &analysis, &facts); - let readiness = compute_readiness_for_test(&scene_ir, &facts); - - assert_eq!( - scene_ir.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert_eq!(readiness.level, "A"); - - let skill_root = generate_scene_package(request).unwrap(); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::PaginatedEnrichment - ); - assert!(generated_report - .main_request - .as_ref() - .and_then(|request| request.api_endpoint.as_ref()) - .map(|endpoint| endpoint.name.contains("queryHisS95598WkstGrid") - || endpoint.name.contains("newQueryS95598WkstGrid")) - .unwrap_or(false)); - assert!(generated_report - .workflow_steps - .iter() - .any(|step| step.step_type == "host_bridge")); -} - -#[test] -fn generator_blocks_incomplete_g8_local_doc_pipeline_contract() { - let output_root = temp_workspace("sgclaw-g8-doc-pipeline-incomplete"); - let scene_ir = SceneIr { - scene_id: "g8-doc-pipeline-incomplete".to_string(), - scene_id_diagnostics: SceneIdDiagnosticsIr::default(), - scene_name: "G8 Doc Pipeline Incomplete".to_string(), - scene_kind: "report_collection".to_string(), - workflow_archetype: Some(WorkflowArchetype::LocalDocPipeline), - bootstrap: BootstrapIr { - expected_domain: "south.95598.sgcc.com.cn".to_string(), - target_url: "http://south.95598.sgcc.com.cn/report".to_string(), - app_entry_url: "http://south.95598.sgcc.com.cn/report".to_string(), - module_route_url: String::new(), - target_url_kind: Some("runtime_context".to_string()), - requires_target_page: true, - page_title_keywords: vec!["G8".to_string()], - source: Some("test".to_string()), - }, - params: Vec::new(), - modes: Vec::new(), - default_mode: None, - mode_switch_field: None, - workflow_steps: vec![ - WorkflowStepIr { - step_type: "local_doc_pipeline".to_string(), - entry: Some("selectData".to_string()), - ..WorkflowStepIr::default() - }, - WorkflowStepIr { - step_type: "sql_query".to_string(), - entry: Some("definedSqlQuery".to_string()), - ..WorkflowStepIr::default() - }, - ], - workflow_evidence: WorkflowEvidenceIr::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: String::new(), - normalize_rules: None, - artifact_contract: ArtifactContractIr::default(), - validation_hints: ValidationHintsIr::default(), - evidence: Vec::new(), - readiness: ReadinessIr::default(), - api_endpoints: vec![ApiEndpointIr { - name: "selectData".to_string(), - url: "http://localhost:13313/configServices/selectData".to_string(), - method: "POST".to_string(), - content_type: Some("application/json".to_string()), - description: None, - }], - runtime_dependencies: Vec::new(), - static_params: Default::default(), - column_defs: Vec::new(), - confidence: 0.7, - uncertainties: Vec::new(), - monitoring_action_workflow: None, - }; - - let error = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g8_local_doc_pipeline"), - scene_id: "g8-doc-pipeline-incomplete".to_string(), - scene_name: "G8 Doc Pipeline Incomplete".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: Some(scene_ir), - }) - .expect_err("expected incomplete G8 local document pipeline to fail closed"); - - assert!(error - .to_string() - .contains("workflow evidence is incomplete for archetype local_doc_pipeline")); -} - -#[test] -fn generator_accepts_g8_local_doc_select_data_contract() { - let output_root = temp_workspace("sgclaw-g8-doc-pipeline-select-data"); - let scene_ir = SceneIr { - scene_id: "g8-doc-pipeline-select-data".to_string(), - scene_id_diagnostics: SceneIdDiagnosticsIr::default(), - scene_name: "G8 Doc Pipeline Select Data".to_string(), - scene_kind: "report_collection".to_string(), - workflow_archetype: Some(WorkflowArchetype::LocalDocPipeline), - bootstrap: BootstrapIr { - expected_domain: "localhost:13313".to_string(), - target_url: "http://localhost:13313/report".to_string(), - app_entry_url: "http://localhost:13313/report".to_string(), - module_route_url: String::new(), - target_url_kind: Some("runtime_context".to_string()), - requires_target_page: true, - page_title_keywords: vec!["G8".to_string()], - source: Some("test".to_string()), - }, - params: Vec::new(), - modes: Vec::new(), - default_mode: None, - mode_switch_field: None, - workflow_steps: vec![ - WorkflowStepIr { - step_type: "doc_export".to_string(), - entry: Some("docExport".to_string()), - ..WorkflowStepIr::default() - }, - WorkflowStepIr { - step_type: "local_doc_pipeline".to_string(), - entry: Some("configServices/selectData".to_string()), - ..WorkflowStepIr::default() - }, - ], - workflow_evidence: WorkflowEvidenceIr::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: "content".to_string(), - normalize_rules: None, - artifact_contract: ArtifactContractIr::default(), - validation_hints: ValidationHintsIr::default(), - evidence: Vec::new(), - readiness: ReadinessIr::default(), - api_endpoints: vec![ApiEndpointIr { - name: "selectData".to_string(), - url: "http://localhost:13313/configServices/selectData".to_string(), - method: "POST".to_string(), - content_type: Some("application/json".to_string()), - description: None, - }], - runtime_dependencies: Vec::new(), - static_params: Default::default(), - column_defs: Vec::new(), - confidence: 0.7, - uncertainties: Vec::new(), - monitoring_action_workflow: None, - }; - - let skill_root = generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/g8_local_doc_pipeline"), - scene_id: "g8-doc-pipeline-select-data".to_string(), - scene_name: "G8 Doc Pipeline Select Data".to_string(), - scene_kind: Some(SceneKind::ReportCollection), - target_url: None, - output_root, - lessons_path: None, - scene_info_json: None, - scene_ir_json: Some(scene_ir), - }) - .expect("selectData local-doc contract should compile"); - - let generated_report: SceneIr = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(), - ) - .unwrap(); - - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::LocalDocPipeline - ); - assert_eq!(generated_report.readiness.level, "A"); - assert!(generated_report - .readiness - .gates - .iter() - .any(|gate| gate.name == "g8_fail_closed" && gate.passed)); -} - -#[test] -fn paginated_enrichment_readiness_marks_join_key_missing_taxonomy() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment", - )) - .unwrap(); - let mut scene_ir = build_paginated_scene_ir(); - scene_ir.join_keys.clear(); - - let readiness = compute_readiness_for_test(&scene_ir, &facts); - assert_eq!(readiness.level, "C"); - assert!(readiness - .missing_pieces - .iter() - .any(|item| item == "g3_join_keys")); - assert!(readiness - .risks - .iter() - .any(|item| item == "g3_join_keys_missing")); - assert!(readiness.gates.iter().any(|gate| { - gate.name == "g3_join_key_resolved" - && !gate.passed - && gate.reason.as_deref() == Some("g3_join_keys") - })); -} - -#[test] -fn paginated_enrichment_readiness_marks_pagination_incomplete_taxonomy() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment", - )) - .unwrap(); - let mut scene_ir = build_paginated_scene_ir(); - scene_ir - .pagination_plan - .as_mut() - .unwrap() - .termination_rule - .clear(); - - let readiness = compute_readiness_for_test(&scene_ir, &facts); - assert_eq!(readiness.level, "C"); - assert!(readiness - .missing_pieces - .iter() - .any(|item| item == "g3_pagination_contract")); - assert!(readiness - .risks - .iter() - .any(|item| item == "g3_pagination_contract_incomplete")); - assert!(readiness.gates.iter().any(|gate| { - gate.name == "g3_pagination_contract_complete" - && !gate.passed - && gate.reason.as_deref() == Some("g3_pagination_contract") - })); -} - -#[test] -fn paginated_enrichment_readiness_marks_export_only_without_business_chain_taxonomy() { - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/paginated_enrichment", - )) - .unwrap(); - let mut scene_ir = build_paginated_scene_ir(); - scene_ir.main_request = None; - scene_ir.enrichment_requests.clear(); - scene_ir.workflow_steps = vec![WorkflowStepIr { - step_type: "export".to_string(), - entry: Some("exportExcel".to_string()), - ..WorkflowStepIr::default() - }]; - scene_ir.workflow_evidence.request_entries.clear(); - scene_ir.workflow_evidence.secondary_request_entries.clear(); - - let readiness = compute_readiness_for_test(&scene_ir, &facts); - assert_eq!(readiness.level, "C"); - assert!(readiness - .missing_pieces - .iter() - .any(|item| item == "g3_main_request")); - assert!(readiness - .missing_pieces - .iter() - .any(|item| item == "g3_enrichment_contract")); - assert!(readiness.gates.iter().any(|gate| { - gate.name == "g3_main_request_resolved" - && !gate.passed - && gate.reason.as_deref() == Some("g3_main_request") - })); - assert!(readiness.gates.iter().any(|gate| { - gate.name == "g3_enrichment_contract_complete" - && !gate.passed - && gate.reason.as_deref() == Some("g3_enrichment_contract") - })); -} - -#[test] -fn analyzer_recovers_g3_residual_export_fn_and_operational_join_keys() { - let g3_monthly = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/95598供电服务月报", - )) - .unwrap(); - assert!( - g3_monthly - .export_methods - .iter() - .any(|method| method == "excelExportFn"), - "expected export method with Fn suffix to be detected; exports={:?}", - g3_monthly.export_methods - ); - - let g3_repair = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/故障报修工单信息统计表", - )) - .unwrap(); - assert!( - g3_repair - .g3_join_key_hints - .iter() - .any(|key| key.eq_ignore_ascii_case("userId") || key.eq_ignore_ascii_case("reportNo")), - "expected operational join key hints to be detected; hints={:?}", - g3_repair.g3_join_key_hints - ); - - let g3_patrol = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/巡视计划完成情况自动检索", - )) - .unwrap(); - assert!( - g3_patrol - .g3_join_key_hints - .iter() - .any(|key| key.eq_ignore_ascii_case("orgId") || key.eq_ignore_ascii_case("userID")), - "expected org/user join key hints to be detected; hints={:?}", - g3_patrol.g3_join_key_hints - ); - - let g3_transformer = extract_deterministic_scene_facts(Path::new( - "D:/desk/智能体资料/全量业务场景/一平台场景/输变电设备运行分析报告", - )) - .unwrap(); - assert!( - g3_transformer - .export_methods - .iter() - .any(|method| method == "aSaveExcelFile"), - "expected ExcelFile export method to be detected; exports={:?}", - g3_transformer.export_methods - ); -} - -#[test] -fn generator_emits_monitoring_template() { - let output_root = temp_workspace("sgclaw-monitoring-generator"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/monitoring"), - scene_id: "sample-monitor-scene".to_string(), - scene_name: "示例监测场景".to_string(), - scene_kind: Some(SceneKind::Monitoring), - target_url: None, - output_root: output_root.clone(), - lessons_path: None, - scene_info_json: None, - scene_ir_json: None, - }) - .unwrap(); - - let skill_root = output_root.join("skills/sample-monitor-scene"); - let generated_manifest = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); - assert!(generated_manifest.contains("category = \"monitoring\"")); -} - -#[test] -fn generator_emits_monitoring_action_detect_preview_anchor_package() { - let output_root = temp_workspace("sgclaw-monitoring-action-detect-preview"); - let skill_root = - generate_monitoring_action_detect_preview_package(GenerateMonitoringActionPreviewRequest { - scene_id: "command-center-fee-control-monitor".to_string(), - scene_name: "指挥中心费控异常监测与处置预览".to_string(), - output_root: output_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/monitoring_action_ir_contract_2026-04-21.json", - ), - }) - .unwrap(); - - let scene_toml = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); - assert!(scene_toml.contains("category = \"monitoring\"")); - assert!(scene_toml.contains("suffix = \"")); - assert!(scene_toml.contains("[safety]")); - assert!(scene_toml.contains("dry_run_default = true")); - assert!(scene_toml.contains("action_modes_enabled = false")); - assert!(scene_toml.contains("mode = \"detect_preview\"")); - assert!(scene_toml.contains("repetCtrlSend")); - - let generation_report = - fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(); - let generated_report: SceneIr = serde_json::from_str(&generation_report).unwrap(); - assert_eq!( - generated_report.workflow_archetype(), - WorkflowArchetype::MonitoringActionWorkflow - ); - let monitoring = generated_report - .monitoring_action_workflow - .as_ref() - .expect("expected monitoring action workflow metadata"); - assert_eq!(monitoring.default_mode, "detect_preview"); - assert!(monitoring.side_effect_policy.dry_run_default); - assert!(monitoring - .side_effect_policy - .blocked_call_signatures - .iter() - .any(|item| item == "repetCtrlSend")); - - let script = fs::read_to_string(skill_root.join("scripts/detect_preview.js")).unwrap(); - assert!(script.contains("monitoring_action_workflow")); - assert!(script.contains("detect_preview")); - assert!(!script.contains("repetCtrlSend(")); - assert!(!script.contains("mac.sendMessages(")); - assert!(!script.contains("mac.callOutLogin(")); - assert!(!script.contains("mac.audioPlay(")); - assert!(!script.contains("mac.exeTQueue(")); - - let blocked = - fs::read_to_string(skill_root.join("references/blocked-side-effects.json")).unwrap(); - assert!(blocked.contains("repetCtrlSend")); - - let test_status = std::process::Command::new("node") - .arg("detect_preview.test.js") - .current_dir(skill_root.join("scripts")) - .status() - .unwrap(); - assert!(test_status.success()); -} - -#[test] -fn generator_emits_scheduled_monitoring_action_skill_package() { - let output_root = temp_workspace("sgclaw-scheduled-monitoring-action-skill"); - let skill_root = generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "command-center-fee-control-monitor".to_string(), - scene_name: "指挥中心费控异常监测".to_string(), - output_root: output_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(); - - for relative in [ - "SKILL.toml", - "SKILL.md", - "scene.toml", - "scripts/detect.js", - "scripts/decide.js", - "scripts/action_plan.js", - "scripts/detect.test.js", - "references/source-evidence.json", - "references/workflow-ir.json", - "references/trigger-contract.json", - "references/platform-dependencies.json", - "references/side-effect-policy.json", - "references/audit-policy.json", - "references/idempotency-policy.json", - "references/generation-report.json", - ] { - assert!( - skill_root.join(relative).exists(), - "expected scheduled skill package file {relative}" - ); - } - - let scene_toml = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); - toml::from_str::(&scene_toml).unwrap(); - assert!(scene_toml.contains("kind = \"scheduled_monitoring_action_workflow\"")); - assert!(scene_toml.contains("natural_language_primary = false")); - assert!(scene_toml.contains("enabled = [\"dry_run\", \"monitor_only\"]")); - assert!(scene_toml.contains("disabled = [\"active\", \"queue_process\"]")); - assert!(scene_toml.contains("active_enabled = false")); - assert!(scene_toml.contains("queue_process_enabled = false")); - assert!(!scene_toml.contains("[deterministic]")); - - let skill_toml = fs::read_to_string(skill_root.join("SKILL.toml")).unwrap(); - toml::from_str::(&skill_toml).unwrap(); - assert!(skill_toml.contains("name = \"detect\"")); - assert!(skill_toml.contains("name = \"decide\"")); - assert!(skill_toml.contains("name = \"action_plan\"")); - assert!(skill_toml.contains("kind = \"scheduled_monitoring_action_workflow\"")); - - let generation_report = - fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(); - let report: serde_json::Value = serde_json::from_str(&generation_report).unwrap(); - assert_eq!(report["family"], "scheduled_monitoring_action_workflow"); - assert_eq!(report["naturalLanguagePrimary"], false); - assert_eq!(report["safety"]["activeEnabled"], false); - assert_eq!(report["safety"]["queueProcessEnabled"], false); - - let side_effect_policy = - fs::read_to_string(skill_root.join("references/side-effect-policy.json")).unwrap(); - let side_effect_policy: serde_json::Value = serde_json::from_str(&side_effect_policy).unwrap(); - assert!(side_effect_policy["blockedCallSignatures"] - .as_array() - .unwrap() - .iter() - .any(|item| item == "repetCtrlSend")); - - let enabled_scripts = [ - fs::read_to_string(skill_root.join("scripts/detect.js")).unwrap(), - fs::read_to_string(skill_root.join("scripts/decide.js")).unwrap(), - fs::read_to_string(skill_root.join("scripts/action_plan.js")).unwrap(), - ]; - assert!(enabled_scripts[0].contains("platformServiceBaseUrl")); - assert!(enabled_scripts[0].contains("postViaPageAxios")); - assert!(enabled_scripts[0].contains("getViaPageAxios")); - assert!(enabled_scripts[0].contains("EmssLib.dataEncrypt_PUB")); - assert!(enabled_scripts[0].contains("readStepTraces")); - assert!(enabled_scripts[0].contains("read_step_timeout_ms")); - assert!(enabled_scripts[0].contains("scheduled monitoring read step")); - for script in enabled_scripts { - for forbidden in [ - "repetCtrlSend(", - "mac.sendMessages(", - "mac.callOutLogin(", - "mac.audioPlay(", - "mac.exeTQueue(", - "_this.autoTask(", - "_this.processQueue(", - "setDisposeLog(", - "setMonitorData(", - "setMonitorLog(", - "setSendMessageLog(", - "setAudioPlayLog(", - ] { - assert!( - !script.contains(forbidden), - "enabled scheduled script must not contain forbidden executable call {forbidden}" - ); - } - } - - let test_status = std::process::Command::new("node") - .arg("detect.test.js") - .current_dir(skill_root.join("scripts")) - .status() - .unwrap(); - assert!(test_status.success()); -} - -#[test] -fn command_center_materializes_automation_semantics_into_workflow_ir() { - let output_root = temp_workspace("sgclaw-command-center-automation-workflow-ir"); - let skill_root = generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "command-center-fee-control-monitor".to_string(), - scene_name: "指挥中心费控异常监测".to_string(), - output_root: output_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 workflow_ir: serde_json::Value = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(), - ) - .unwrap(); - - let action_contracts = workflow_ir["actionContracts"] - .as_array() - .expect("expected actionContracts array"); - assert!( - action_contracts.iter().any(|item| { - item["targetEndpointOrHostCall"] == "repetCtrlSend" - || item["actionId"] == "dispatch_exception_order" - }), - "expected action contract for repetCtrlSend dispatch" - ); - - assert_eq!( - workflow_ir["iterationContract"]["sourceCollection"], - "pendingList", - "expected iteration contract over pendingList" - ); - assert_eq!( - workflow_ir["iterationContract"]["iterationMode"], - "sequential_per_item", - "expected sequential per-item iteration" - ); - - let queue_transition_rules = workflow_ir["queueTransitionRules"] - .as_array() - .expect("expected queueTransitionRules array"); - assert!( - queue_transition_rules.iter().any(|item| { - item["triggerPoint"] == "on_empty_collection" - || item["transitionId"] == "queue_continue_on_empty" - }), - "expected queue continue transition for empty collection" - ); - assert!( - queue_transition_rules.iter().any(|item| { - item["triggerPoint"] == "on_all_items_done" - || item["transitionId"] == "queue_continue_on_done" - }), - "expected queue continue transition for completed collection" - ); - - let log_write_contracts = workflow_ir["logWriteContracts"] - .as_array() - .expect("expected logWriteContracts array"); - assert!( - log_write_contracts.iter().any(|item| { - item["targetEndpointOrHostCall"] == "setDisposeLog" - || item["logId"] == "dispose_log_after_dispatch" - }), - "expected dispose-log contract" - ); -} - -#[test] -fn generator_preserves_localhost_dependency_as_host_runtime_evidence() { - let analysis = analyze_scene_source(Path::new( - "tests/fixtures/generated_scene/bootstrap_localhost_pollution", - )) - .unwrap(); - let facts = extract_deterministic_scene_facts(Path::new( - "tests/fixtures/generated_scene/bootstrap_localhost_pollution", - )) - .unwrap(); - - assert_eq!( - analysis.bootstrap.expected_domain.as_deref(), - Some("yx.gs.sgcc.com.cn") - ); - assert!(facts - .localhost_dependencies - .iter() - .any(|item| item.contains("localhost:13313"))); - assert_ne!( - facts.workflow_archetype, - WorkflowArchetype::LocalDocPipeline - ); - return; - - let output_root = temp_workspace("sgclaw-scene-generator-localhost-evidence"); - - generate_scene_package(GenerateSceneRequest { - source_dir: PathBuf::from("tests/fixtures/generated_scene/bootstrap_localhost_pollution"), - scene_id: "localhost-evidence-scene".to_string(), - scene_name: "宿主依赖证据场景".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: None, - }) - .unwrap(); - - let skill_root = output_root.join("skills/localhost-evidence-scene"); - let generation_report = - fs::read_to_string(skill_root.join("references/generation-report.json")).unwrap(); - - assert!(generation_report.contains("\"localhost_dependency_candidate\"")); - assert!(generation_report.contains("localhost:13313")); - assert!(generation_report.contains("\"expectedDomain\": \"yx.gs.sgcc.com.cn\"")); -} - -fn build_multi_mode_scene_ir() -> SceneIr { - SceneIr { - scene_id: "multi-mode-scene".to_string(), - scene_id_diagnostics: SceneIdDiagnosticsIr::default(), - scene_name: "多模式场景".to_string(), - scene_kind: "report_collection".to_string(), - workflow_archetype: Some(WorkflowArchetype::MultiModeRequest), - bootstrap: BootstrapIr { - expected_domain: "20.76.57.61".to_string(), - target_url: "http://20.76.57.61:18080/gsllys".to_string(), - app_entry_url: "http://20.76.57.61:18080/gsllys".to_string(), - module_route_url: "http://20.76.57.61:18080/gsllys/monthReport".to_string(), - target_url_kind: Some("runtime_context".to_string()), - requires_target_page: true, - page_title_keywords: vec!["多模式".to_string()], - source: Some("test".to_string()), - }, - params: Vec::new(), - modes: vec![ - ModeIr { - name: "month".to_string(), - label: Some("month".to_string()), - condition: Some(ModeConditionIr { - field: "period_mode".to_string(), - operator: "equals".to_string(), - value: serde_json::Value::String("month".to_string()), - }), - api_endpoint: Some(ApiEndpointIr { - name: "monthReport".to_string(), - url: "http://20.76.57.61:18080/gsllys/monthReport".to_string(), - method: "POST".to_string(), - content_type: Some("application/x-www-form-urlencoded".to_string()), - description: None, - }), - column_defs: vec![("ORG_NAME".to_string(), "供电单位".to_string())], - request_template: serde_json::json!({ - "orgno":"${args.org_code}", - "fdate":"${args.fdate}", - "tjzq":"month" - }), - request_field_mappings: Vec::new(), - normalize_rules: None, - response_path: "content".to_string(), - }, - ModeIr { - name: "week".to_string(), - label: Some("week".to_string()), - condition: Some(ModeConditionIr { - field: "period_mode".to_string(), - operator: "equals".to_string(), - value: serde_json::Value::String("week".to_string()), - }), - api_endpoint: Some(ApiEndpointIr { - name: "weekReport".to_string(), - url: "http://20.76.57.61:18080/gsllys/weekReport".to_string(), - method: "POST".to_string(), - content_type: Some("application/x-www-form-urlencoded".to_string()), - description: None, - }), - column_defs: vec![("ORG_NAME".to_string(), "供电单位".to_string())], - request_template: serde_json::json!({ - "orgno":"${args.org_code}", - "weekSfdate":"${args.weekSfdate}", - "weekEfdate":"${args.weekEfdate}", - "tjzq":"week" - }), - request_field_mappings: Vec::new(), - normalize_rules: None, - response_path: "content".to_string(), - }, - ], - default_mode: Some("month".to_string()), - mode_switch_field: Some("period_mode".to_string()), - workflow_steps: vec![ - WorkflowStepIr { - step_type: "request".to_string(), - description: Some("select mode".to_string()), - ..WorkflowStepIr::default() - }, - WorkflowStepIr { - step_type: "transform".to_string(), - description: Some("normalize rows".to_string()), - ..WorkflowStepIr::default() - }, - ], - workflow_evidence: WorkflowEvidenceIr::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: "content".to_string(), - normalize_rules: None, - artifact_contract: ArtifactContractIr::default(), - validation_hints: ValidationHintsIr::default(), - evidence: Vec::new(), - readiness: ReadinessIr::default(), - api_endpoints: Vec::new(), - runtime_dependencies: Vec::new(), - static_params: Default::default(), - column_defs: Vec::new(), - confidence: 0.9, - uncertainties: Vec::new(), - monitoring_action_workflow: None, - } -} - -fn build_paginated_scene_ir() -> SceneIr { - SceneIr { - scene_id: "paginated-scene".to_string(), - scene_id_diagnostics: SceneIdDiagnosticsIr::default(), - scene_name: "分页补数场景".to_string(), - scene_kind: "report_collection".to_string(), - workflow_archetype: Some(WorkflowArchetype::PaginatedEnrichment), - bootstrap: BootstrapIr { - expected_domain: "yx.gs.sgcc.com.cn".to_string(), - target_url: "http://yx.gs.sgcc.com.cn".to_string(), - app_entry_url: "http://yx.gs.sgcc.com.cn".to_string(), - module_route_url: String::new(), - target_url_kind: Some("runtime_context".to_string()), - requires_target_page: true, - page_title_keywords: vec!["零度户".to_string()], - source: Some("test".to_string()), - }, - params: Vec::new(), - modes: Vec::new(), - default_mode: None, - mode_switch_field: None, - workflow_steps: vec![ - WorkflowStepIr { - step_type: "request".to_string(), - entry: Some("getUserList".to_string()), - endpoint: Some("userList".to_string()), - ..WorkflowStepIr::default() - }, - WorkflowStepIr { - step_type: "paginate".to_string(), - entry: Some("getUserList".to_string()), - ..WorkflowStepIr::default() - }, - WorkflowStepIr { - step_type: "secondary_request".to_string(), - entry: Some("getUserCharges".to_string()), - ..WorkflowStepIr::default() - }, - WorkflowStepIr { - step_type: "filter".to_string(), - expr: Some("row.charge !== 0".to_string()), - ..WorkflowStepIr::default() - }, - WorkflowStepIr { - step_type: "export".to_string(), - entry: Some("exportExcel".to_string()), - ..WorkflowStepIr::default() - }, - ], - workflow_evidence: WorkflowEvidenceIr { - request_entries: vec!["getUserList".to_string()], - pagination_fields: vec!["page".to_string(), "pageSize".to_string()], - secondary_request_entries: vec!["getUserCharges".to_string()], - post_process_steps: vec!["filter".to_string(), "export".to_string()], - }, - main_request: Some(sgclaw::generated_scene::ir::MainRequestIr { - api_endpoint: Some(ApiEndpointIr { - name: "userList".to_string(), - url: "http://yx.gs.sgcc.com.cn/marketing/userList".to_string(), - method: "POST".to_string(), - content_type: Some("application/json".to_string()), - description: Some("g3_main_request".to_string()), - }), - request_template: serde_json::json!({"page":"${args.page}","pageSize":"${args.page_size}"}), - response_path: "rows".to_string(), - column_defs: Vec::new(), - }), - pagination_plan: Some(sgclaw::generated_scene::ir::PaginationPlanIr { - page_field: "page".to_string(), - page_size_field: Some("pageSize".to_string()), - start_page: Some(1), - termination_rule: "stop_when_page_rows_empty".to_string(), - }), - enrichment_requests: vec![sgclaw::generated_scene::ir::EnrichmentRequestIr { - name: "userCharges".to_string(), - api_endpoint: Some(ApiEndpointIr { - name: "userCharges".to_string(), - url: "http://yx.gs.sgcc.com.cn/marketing/userCharges".to_string(), - method: "POST".to_string(), - content_type: Some("application/json".to_string()), - description: Some("g3_enrichment_request".to_string()), - }), - param_bindings: serde_json::Map::new(), - response_path: "rows".to_string(), - consumed_fields: vec!["custNo".to_string()], - }], - join_keys: vec!["custNo".to_string()], - merge_or_dedupe_rules: vec!["dedupe:custNo".to_string(), "aggregate:charge".to_string()], - export_plan: Some(sgclaw::generated_scene::ir::ExportPlanIr { - entry: Some("exportExcel".to_string()), - artifact_type: Some("report-artifact".to_string()), - depends_on_host_bridge: false, - }), - merge_plan: None, - request_template: serde_json::Value::Null, - response_path: "rows".to_string(), - normalize_rules: None, - artifact_contract: ArtifactContractIr::default(), - validation_hints: ValidationHintsIr::default(), - evidence: Vec::new(), - readiness: ReadinessIr::default(), - api_endpoints: vec![ - ApiEndpointIr { - name: "userList".to_string(), - url: "http://yx.gs.sgcc.com.cn/marketing/userList".to_string(), - method: "POST".to_string(), - content_type: Some("application/json".to_string()), - description: None, - }, - ApiEndpointIr { - name: "userCharges".to_string(), - url: "http://yx.gs.sgcc.com.cn/marketing/userCharges".to_string(), - method: "POST".to_string(), - content_type: Some("application/json".to_string()), - description: None, - }, - ], - runtime_dependencies: Vec::new(), - static_params: Default::default(), - column_defs: Vec::new(), - confidence: 0.9, - 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 -} diff --git a/tests/scheduled_monitoring_action_binary_wiring_test.rs b/tests/scheduled_monitoring_action_binary_wiring_test.rs index 3185287..bac6160 100644 --- a/tests/scheduled_monitoring_action_binary_wiring_test.rs +++ b/tests/scheduled_monitoring_action_binary_wiring_test.rs @@ -7,9 +7,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; use reqwest::blocking::Client; use serde_json::{json, Value}; -use sgclaw::generated_scene::generator::{ - generate_scheduled_monitoring_action_skill_package, GenerateScheduledMonitoringActionSkillRequest, -}; use tungstenite::{accept, Message}; fn bin_path() -> PathBuf { @@ -36,6 +33,12 @@ fn temp_workspace(prefix: &str) -> PathBuf { 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 { json!({ "trigger_type": "scheduled", @@ -529,8 +532,6 @@ fn binary_wiring_loads_registry_backed_scheduled_skill() { let output_path = workspace.join("run-record.json"); let config_path = workspace.join("sgclaw_config.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_runtime_rules(&rules_path); 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); 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( &trigger_path, - &materialization_root.join("skills"), + &validation_bundle_skills_dir(), &config_path, &workspace, &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 config_path = workspace.join("sgclaw_config.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_with_runtime_inputs("monitor_only"), ); 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!({ "type": "scheduled-monitoring-detect-snapshot", "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( &trigger_path, - &materialization_root.join("skills"), + &validation_bundle_skills_dir(), &config_path, &workspace, &output_path, @@ -744,32 +711,12 @@ fn command_center_preview_reflects_automation_semantics() { let output_path = workspace.join("run-record.json"); let config_path = workspace.join("sgclaw_config.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_with_runtime_inputs("monitor_only"), ); 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!({ "type": "scheduled-monitoring-detect-snapshot", "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( &trigger_path, - &materialization_root.join("skills"), + &validation_bundle_skills_dir(), &config_path, &workspace, &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 config_path = workspace.join("sgclaw_config.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_with_runtime_inputs("monitor_only"), ); 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!({ "type": "scheduled-monitoring-detect-snapshot", "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( &trigger_path, - &materialization_root.join("skills"), + &validation_bundle_skills_dir(), &config_path, &workspace, &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 config_path = workspace.join("sgclaw_config.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_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!({ "type": "scheduled-monitoring-detect-snapshot", "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( &trigger_path, - &materialization_root.join("skills"), + &validation_bundle_skills_dir(), &config_path, &workspace, &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"]["notify_count"], 0); - assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 1); + assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 0); assert_eq!( 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"]["notify_count"], 0); - assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 1); + assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 0); assert_eq!( record["auditPreview"]["detectReadDiagnostics"]["businessType"], "可用电费小于零监测提醒" diff --git a/tests/scheduled_monitoring_action_registry_test.rs b/tests/scheduled_monitoring_action_registry_test.rs index 04a2a91..4c91d75 100644 --- a/tests/scheduled_monitoring_action_registry_test.rs +++ b/tests/scheduled_monitoring_action_registry_test.rs @@ -1,60 +1,32 @@ -use std::fs; use std::path::PathBuf; -use std::time::{SystemTime, UNIX_EPOCH}; 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 { - 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 +fn validation_bundle_skills_dir() -> PathBuf { + PathBuf::from("dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills") } #[test] fn scheduled_monitoring_registry_loads_materialized_skill() { - let output_root = temp_workspace("sgclaw-scheduled-monitoring-registry"); - generate_scheduled_monitoring_action_skill_package(GenerateScheduledMonitoringActionSkillRequest { - scene_id: "command-center-fee-control-monitor".to_string(), - scene_name: "指挥中心费控异常监测".to_string(), - output_root: output_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 registry = load_scheduled_monitoring_registry(&validation_bundle_skills_dir()).unwrap(); + let entry = registry + .iter() + .find(|entry| entry.workflow_id == "command_center_fee_control_monitoring_action") + .expect("command center skill must be registered"); - let registry = load_scheduled_monitoring_registry(&output_root.join("skills")).unwrap(); - assert_eq!(registry.len(), 1); assert_eq!( - registry[0].workflow_id, + entry.workflow_id, "command_center_fee_control_monitoring_action" ); assert_eq!( - registry[0].manifest.scene.kind, + entry.manifest.scene.kind, "scheduled_monitoring_action_workflow" ); } #[test] fn scheduled_monitoring_registry_loads_archive_workorder_skill() { - let skills_dir = PathBuf::from( - "dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills", - ); - - let registry = load_scheduled_monitoring_registry(&skills_dir).unwrap(); + let registry = load_scheduled_monitoring_registry(&validation_bundle_skills_dir()).unwrap(); let entry = registry .iter() .find(|entry| entry.workflow_id == "archive_workorder_grid_push_monitoring_action") @@ -77,11 +49,7 @@ fn scheduled_monitoring_registry_loads_archive_workorder_skill() { #[test] fn scheduled_monitoring_registry_loads_available_balance_below_zero_skill() { - let skills_dir = PathBuf::from( - "dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills", - ); - - let registry = load_scheduled_monitoring_registry(&skills_dir).unwrap(); + let registry = load_scheduled_monitoring_registry(&validation_bundle_skills_dir()).unwrap(); let entry = registry .iter() .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] fn scheduled_monitoring_registry_loads_sgcc_todo_crawler_skill() { - let skills_dir = PathBuf::from( - "dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills", - ); - - let registry = load_scheduled_monitoring_registry(&skills_dir).unwrap(); + let registry = load_scheduled_monitoring_registry(&validation_bundle_skills_dir()).unwrap(); let entry = registry .iter() .find(|entry| entry.workflow_id == "sgcc_todo_crawler_monitoring_action") diff --git a/tests/scheduled_monitoring_generated_scene_hardening_test.rs b/tests/scheduled_monitoring_generated_scene_hardening_test.rs deleted file mode 100644 index dda214a..0000000 --- a/tests/scheduled_monitoring_generated_scene_hardening_test.rs +++ /dev/null @@ -1,1097 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::{SystemTime, UNIX_EPOCH}; - -use serde_json::{json, Value}; -use sgclaw::generated_scene::generator::{ - generate_scheduled_monitoring_action_skill_package, GenerateScheduledMonitoringActionSkillRequest, -}; -use sgclaw::generated_scene::ir::MonitoringDependencyIr; -use sgclaw::generated_scene::scheduled_monitoring_runtime::{ - run_scheduled_monitoring_skill_command_adapter, ScheduledMonitoringSkillCommandAdapterRequest, -}; - -fn temp_workspace(prefix: &str) -> PathBuf { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let root = std::env::temp_dir().join(format!("{prefix}-{nanos}")); - fs::create_dir_all(&root).unwrap(); - root -} - -fn write_json(path: &Path, value: &Value) { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).unwrap(); - } - fs::write(path, serde_json::to_string_pretty(value).unwrap()).unwrap(); -} - -fn minimal_trigger_contract() -> Value { - json!({ - "family": "scheduled_monitoring_action_workflow", - "activeModeEnabled": false, - "queueProcessModeEnabled": false - }) -} - -fn run_node_script(script_path: &Path) -> std::process::Output { - Command::new("node").arg(script_path).output().unwrap() -} - -fn write_runtime_rules(path: &Path) { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).unwrap(); - } - fs::write( - path, - serde_json::to_string_pretty(&json!({ - "mac": { - "sendMessages": { "enabled": false }, - "callOutLogin": { "enabled": false }, - "audioPlay": { "enabled": false }, - "exeTQueue": { "enabled": false } - } - })) - .unwrap(), - ) - .unwrap(); -} - -fn normalize_monitoring_dependency_classification( - dependency: &MonitoringDependencyIr, -) -> String { - dependency.normalized_classification() -} - -#[test] -fn localhost_host_runtime_dependency_classification() { - let localhost_dependency = MonitoringDependencyIr { - name: "getMonitorLog".to_string(), - url: "http://localhost:13313/MonitorServices/getMonitorLog".to_string(), - classification: "read_state".to_string(), - side_effect: false, - blocked_by_default: false, - }; - let remote_platform_dependency = MonitoringDependencyIr { - name: "remoteMonitorLog".to_string(), - url: "http://25.215.213.128:18080/MonitorServices/getMonitorLog".to_string(), - classification: "read_state".to_string(), - side_effect: false, - blocked_by_default: false, - }; - - assert_eq!( - normalize_monitoring_dependency_classification(&localhost_dependency), - "host_runtime_local_service" - ); - assert_eq!( - normalize_monitoring_dependency_classification(&remote_platform_dependency), - "remote_platform_service" - ); -} - -#[test] -fn available_balance_materialization_emits_storage_slice_encryption_and_timeout_contracts() { - let root = temp_workspace("sgclaw-available-balance-hardening"); - let source_evidence = root.join("source-evidence.json"); - let contract_path = root.join("available-balance-contract.json"); - let trigger_path = root.join("trigger-contract.json"); - write_json(&source_evidence, &json!({})); - write_json( - &contract_path, - &json!({ - "workflowId": "available_balance_below_zero_monitoring_action", - "displayName": "available balance below zero", - "defaultMode": "monitor_only", - "archetype": "business_page_report_monitor", - "runtimeContext": { - "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", - "expectedDomain": "yx.gs.sgcc.com.cn", - "gatewayDomain": "yxgateway.gs.sgcc.com.cn", - "localhostServiceBase": "http://localhost:13313", - "browserAttachedRequired": true, - "hostBridgeRequired": true, - "executionContextMode": "attached_page_direct", - "requestClientMode": "isolated_xhr", - "encryptionMode": "window_encrypt_old", - "attachedPageBrowserActionPolicy": "forbid_secondary_jump", - "platformWritePolicy": "skip_when_zero" - }, - "businessApiDependencies": [ - { - "name": "load_report", - "url": "http://yxgateway.gs.sgcc.com.cn/report/load", - "classification": "read_report", - "sideEffect": false - } - ], - "storageReads": [ - { - "key": "markToken", - "source": "localStorage", - "fallbackOrder": ["sessionStorage"], - "required": true, - "parseMode": "raw" - }, - { - "key": "markYXObj", - "source": "localStorage", - "fallbackOrder": ["sessionStorage"], - "required": true, - "parseMode": "json" - }, - { - "key": "loginUserInfo", - "source": "sessionStorage", - "fallbackOrder": ["localStorage"], - "required": true, - "parseMode": "json" - } - ], - "readSlices": [ - { - "name": "slice_01", - "endpointBinding": "load_report", - "requestTemplateOverride": { "sliceType": "01" }, - "responsePath": "data.tableData", - "timeoutMs": 11111, - "mergeRole": "concat", - "required": true - }, - { - "name": "slice_02", - "endpointBinding": "load_report", - "requestTemplateOverride": { "sliceType": "02" }, - "responsePath": "data.tableData", - "timeoutMs": 22222, - "mergeRole": "concat", - "required": false - }, - { - "name": "slice_03", - "endpointBinding": "load_report", - "requestTemplateOverride": { "sliceType": "03" }, - "responsePath": "data.tableData", - "timeoutMs": 33333, - "mergeRole": "fail_partial", - "required": false - } - ], - "encryptionResolution": { - "primaryMethod": "window.encrypt_old", - "fallbackMethods": [ - "EmssLib.dataEncrypt_CBC_New", - "EmssLib.dataEncrypt_PUB" - ], - "requiredContext": ["business_page"], - "hardFail": true - }, - "timeoutContract": { - "perStepTimeoutMs": 11111, - "overallDetectTimeoutMs": 45000, - "statusOnTimeout": "timeout", - "statusOnPartial": "partial" - }, - "sideEffectPolicy": { - "blockedCallSignatures": [ - "mac.sendMessages", - "mac.callOutLogin" - ] - } - }), - ); - write_json(&trigger_path, &minimal_trigger_contract()); - - let skill_root = generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "available-balance-below-zero-monitor".to_string(), - scene_name: "available-balance-below-zero-monitor".to_string(), - output_root: root.clone(), - source_evidence_json: source_evidence, - ir_contract_json: contract_path, - trigger_contract_json: trigger_path, - }, - ) - .unwrap(); - - let scene_toml = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); - assert!(scene_toml.contains("[[runtime_context.storage_reads]]")); - assert!(scene_toml.contains("key = \"markToken\"")); - assert!(scene_toml.contains("[[runtime_context.read_slices]]")); - assert!(scene_toml.contains("name = \"slice_01\"")); - assert!(scene_toml.contains("[runtime_context.encryption_resolution]")); - assert!(scene_toml.contains("primary_method = \"window.encrypt_old\"")); - assert!(scene_toml.contains("[runtime_context.timeout_contract]")); - assert!(scene_toml.contains("overall_detect_timeout_ms = 45000")); - - let workflow_ir = - fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(); - assert!(workflow_ir.contains("\"storageReads\"")); - assert!(workflow_ir.contains("\"readSlices\"")); - assert!(workflow_ir.contains("\"encryptionResolution\"")); - assert!(workflow_ir.contains("\"timeoutContract\"")); - - let detect_script = fs::read_to_string(skill_root.join("scripts/detect.js")).unwrap(); - assert!(detect_script.contains("slice_01")); - assert!(detect_script.contains("slice_02")); - assert!(detect_script.contains("slice_03")); - assert!(detect_script.contains("EmssLib.dataEncrypt_CBC_New")); - assert!(detect_script.contains("EmssLib.dataEncrypt_PUB")); - assert!(detect_script.contains("overallDetectTimeoutMs")); - assert!(detect_script.contains("storageReads")); - assert!(detect_script.contains("read_slices") || detect_script.contains("readSlices")); - assert!(detect_script.contains("mergeRole") || detect_script.contains("merge_role")); -} - -#[test] -fn fee_control_materialization_emits_timeout_contract_in_generated_artifacts() { - let root = temp_workspace("sgclaw-fee-control-timeout-hardening"); - let skill_root = 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: 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 scene_toml = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); - assert!(scene_toml.contains("[runtime_context.timeout_contract]")); - - let workflow_ir = - fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(); - assert!(workflow_ir.contains("\"timeoutContract\"")); - - let detect_script = fs::read_to_string(skill_root.join("scripts/detect.js")).unwrap(); - assert!(detect_script.contains("overallDetectTimeoutMs")); - assert!(detect_script.contains("statusOnTimeout")); -} - -#[test] -fn command_center_workflow_ir_includes_automation_semantics() { - let root = temp_workspace("sgclaw-command-center-automation-semantics"); - let skill_root = 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: 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 workflow_ir: Value = serde_json::from_str( - &fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(), - ) - .unwrap(); - - assert!( - workflow_ir.get("actionContracts").is_some(), - "workflow-ir.json must include actionContracts" - ); - assert!( - workflow_ir.get("iterationContract").is_some(), - "workflow-ir.json must include iterationContract" - ); - assert!( - workflow_ir.get("executionFlow").is_some(), - "workflow-ir.json must include executionFlow" - ); - assert!( - workflow_ir.get("resultStateMachines").is_some(), - "workflow-ir.json must include resultStateMachines" - ); - assert!( - workflow_ir.get("queueTransitionRules").is_some(), - "workflow-ir.json must include queueTransitionRules" - ); - assert!( - workflow_ir.get("logWriteContracts").is_some(), - "workflow-ir.json must include logWriteContracts" - ); -} - -#[test] -fn available_balance_and_archive_materialization_emit_explicit_dependency_classification_in_references() { - let example_output = Command::new("cargo") - .args([ - "run", - "--example", - "refresh_scheduled_monitoring_reference_metadata", - ]) - .current_dir(std::env::current_dir().unwrap()) - .output() - .unwrap(); - assert!( - example_output.status.success(), - "refresh_scheduled_monitoring_skill failed: stdout={}\nstderr={}", - String::from_utf8_lossy(&example_output.stdout), - String::from_utf8_lossy(&example_output.stderr) - ); - - let skills_root = PathBuf::from( - "dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills", - ); - for scene_id in [ - "available-balance-below-zero-monitor", - "archive-workorder-grid-push-monitor", - ] { - let references_root = skills_root.join(scene_id).join("references"); - let workflow_ir = - fs::read_to_string(references_root.join("workflow-ir.json")).unwrap_or_default(); - let platform_dependencies = - fs::read_to_string(references_root.join("platform-dependencies.json")) - .unwrap_or_default(); - let source_evidence = - fs::read_to_string(references_root.join("source-evidence.json")).unwrap_or_default(); - - assert!( - workflow_ir.contains("normalizedClassification"), - "{scene_id} workflow-ir.json must emit normalizedClassification" - ); - assert!( - workflow_ir.contains("dependencyClassificationSummary"), - "{scene_id} workflow-ir.json must emit dependencyClassificationSummary" - ); - assert!( - platform_dependencies.contains("normalizedClassification"), - "{scene_id} platform-dependencies.json must emit normalizedClassification" - ); - assert!( - platform_dependencies.contains("dependencyClassificationSummary"), - "{scene_id} platform-dependencies.json must emit dependencyClassificationSummary" - ); - assert!( - source_evidence.contains("normalizedClassification"), - "{scene_id} source-evidence.json must emit normalizedClassification" - ); - assert!( - source_evidence.contains("dependencyClassificationSummary"), - "{scene_id} source-evidence.json must emit dependencyClassificationSummary" - ); - assert!( - workflow_ir.contains("host_runtime_local_service") - || platform_dependencies.contains("host_runtime_local_service") - || source_evidence.contains("host_runtime_local_service"), - "{scene_id} references must include host_runtime_local_service" - ); - assert!( - workflow_ir.contains("business_gateway_service") - || platform_dependencies.contains("business_gateway_service") - || source_evidence.contains("business_gateway_service"), - "{scene_id} references must include business_gateway_service" - ); - } - -} - -#[test] -fn fee_control_materialization_emits_explicit_localhost_vs_platform_classification_in_references() { - let root = temp_workspace("sgclaw-fee-control-reference-classification"); - let skill_root = 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: 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 workflow_ir = - fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(); - let platform_dependencies = - fs::read_to_string(skill_root.join("references/platform-dependencies.json")).unwrap(); - let source_evidence = - fs::read_to_string(skill_root.join("references/source-evidence.json")).unwrap(); - - assert!(workflow_ir.contains("host_runtime_local_service")); - assert!(platform_dependencies.contains("host_runtime_local_service")); - assert!(source_evidence.contains("host_runtime_local_service")); - assert!(workflow_ir.contains("business_gateway_service")); - assert!(platform_dependencies.contains("business_gateway_service")); - assert!(source_evidence.contains("business_gateway_service")); -} - -#[test] -fn archive_workorder_materialization_emits_delta_contract_and_identity_based_dedupe() { - let root = temp_workspace("sgclaw-archive-workorder-hardening"); - let source_evidence = root.join("source-evidence.json"); - let contract_path = root.join("archive-workorder-contract.json"); - let trigger_path = root.join("trigger-contract.json"); - write_json(&source_evidence, &json!({})); - write_json( - &contract_path, - &json!({ - "workflowId": "archive_workorder_grid_push_monitoring_action", - "displayName": "archive workorder grid push", - "defaultMode": "monitor_only", - "archetype": "list_dedupe_monitor", - "runtimeContext": { - "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", - "expectedDomain": "yx.gs.sgcc.com.cn", - "gatewayDomain": "yxgateway.gs.sgcc.com.cn", - "localhostServiceBase": "http://localhost:13313", - "browserAttachedRequired": true, - "hostBridgeRequired": true, - "executionContextMode": "attached_page_direct", - "requestClientMode": "isolated_xhr", - "encryptionMode": "window_encrypt_old", - "attachedPageBrowserActionPolicy": "forbid_secondary_jump", - "platformWritePolicy": "skip_when_zero" - }, - "businessApiDependencies": [ - { - "name": "get_workorders", - "url": "http://yxgateway.gs.sgcc.com.cn/workorders", - "classification": "read_workorders", - "sideEffect": false - } - ], - "storageReads": [ - { - "key": "markToken", - "source": "localStorage", - "fallbackOrder": ["sessionStorage"], - "required": true, - "parseMode": "raw" - } - ], - "deltaState": { - "identityFields": ["wkOrderNo", "mgtOrgCodeName"], - "stateSidecarPath": "state/archive-workorder-grid-push-monitor.state.json", - "comparisonMode": "new_rows_only", - "emitPolicy": "new_rows_only" - }, - "timeoutContract": { - "perStepTimeoutMs": 15000, - "overallDetectTimeoutMs": 30000, - "statusOnTimeout": "timeout", - "statusOnPartial": "partial" - }, - "sideEffectPolicy": { - "blockedCallSignatures": [ - "mac.sendMessages", - "_this.autoTask" - ] - } - }), - ); - write_json(&trigger_path, &minimal_trigger_contract()); - - let skill_root = generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "archive-workorder-grid-push-monitor".to_string(), - scene_name: "archive-workorder-grid-push-monitor".to_string(), - output_root: root.clone(), - source_evidence_json: source_evidence, - ir_contract_json: contract_path, - trigger_contract_json: trigger_path, - }, - ) - .unwrap(); - - let scene_toml = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); - assert!(scene_toml.contains("[runtime_context.output_contract.delta_state]")); - assert!(scene_toml.contains("identity_fields = [\"wkOrderNo\", \"mgtOrgCodeName\"]")); - assert!(scene_toml.contains("emit_policy = \"new_rows_only\"")); - - let workflow_ir = - fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(); - assert!(workflow_ir.contains("\"deltaState\"")); - assert!(workflow_ir.contains("\"identityFields\"")); - - let detect_script = fs::read_to_string(skill_root.join("scripts/detect.js")).unwrap(); - assert!(detect_script.contains("wkOrderNo")); - assert!(detect_script.contains("mgtOrgCodeName")); - assert!(detect_script.contains("new_rows_only")); -} - -#[test] -fn business_page_report_detect_executes_all_configured_slices_and_merges_rows() { - let root = temp_workspace("sgclaw-business-page-slices"); - let source_evidence = root.join("source-evidence.json"); - let contract_path = root.join("contract.json"); - let trigger_path = root.join("trigger-contract.json"); - write_json(&source_evidence, &json!({})); - write_json( - &contract_path, - &json!({ - "workflowId": "available_balance_below_zero_monitoring_action", - "displayName": "available balance below zero", - "defaultMode": "monitor_only", - "archetype": "business_page_report_monitor", - "runtimeContext": { - "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", - "expectedDomain": "yx.gs.sgcc.com.cn", - "gatewayDomain": "yxgateway.gs.sgcc.com.cn", - "localhostServiceBase": "http://localhost:13313", - "browserAttachedRequired": true, - "hostBridgeRequired": true, - "executionContextMode": "attached_page_direct", - "requestClientMode": "isolated_xhr", - "encryptionMode": "window_encrypt_old", - "attachedPageBrowserActionPolicy": "forbid_secondary_jump", - "platformWritePolicy": "skip_when_zero", - "storageReads": [ - { - "key": "markToken", - "source": "localStorage", - "fallbackOrder": ["sessionStorage"], - "required": true, - "parseMode": "raw" - } - ], - "readSlices": [ - { - "name": "slice_01", - "endpointBinding": "load_report", - "requestTemplateOverride": { "sliceType": "01" }, - "responsePath": "data.items", - "timeoutMs": 1000, - "mergeRole": "concat", - "required": true - }, - { - "name": "slice_02", - "endpointBinding": "load_report", - "requestTemplateOverride": { "sliceType": "02" }, - "responsePath": "data.items", - "timeoutMs": 1000, - "mergeRole": "concat", - "required": true - }, - { - "name": "slice_03", - "endpointBinding": "load_report", - "requestTemplateOverride": { "sliceType": "03" }, - "responsePath": "data.items", - "timeoutMs": 1000, - "mergeRole": "concat", - "required": true - } - ], - "encryptionResolution": { - "primaryMethod": "deps.encrypt_old", - "fallbackMethods": [], - "requiredContext": [], - "hardFail": true - }, - "timeoutContract": { - "perStepTimeoutMs": 1000, - "overallDetectTimeoutMs": 3000, - "statusOnTimeout": "timeout", - "statusOnPartial": "partial" - } - }, - "businessApiDependencies": [ - { - "name": "load_report", - "url": "http://yxgateway.gs.sgcc.com.cn/report/load", - "classification": "read_report", - "sideEffect": false - } - ], - "sideEffectPolicy": { - "blockedCallSignatures": ["mac.sendMessages"] - } - }), - ); - write_json(&trigger_path, &minimal_trigger_contract()); - - let skill_root = generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "available-balance-below-zero-monitor".to_string(), - scene_name: "available-balance-below-zero-monitor".to_string(), - output_root: root.clone(), - source_evidence_json: source_evidence, - ir_contract_json: contract_path, - trigger_contract_json: trigger_path, - }, - ) - .unwrap(); - let runner_path = root.join("run-detect.js"); - fs::write( - &runner_path, - format!( - r#"const detect = require({detect_path:?}); -let calls = 0; -global.fetch = async (_url, _opts) => {{ - calls += 1; - const payloads = [ - {{ data: {{ items: [{{ id: 'A1' }}] }} }}, - {{ data: {{ items: [{{ id: 'A2' }}] }} }}, - {{ data: {{ items: [{{ id: 'A3' }}] }} }} - ]; - return {{ - ok: true, - status: 200, - text: async () => JSON.stringify(payloads[calls - 1] || {{ data: {{ items: [] }} }}), - json: async () => (payloads[calls - 1] || {{ data: {{ items: [] }} }}) - }}; -}}; -(async () => {{ - const result = await detect.detect({{ mode: 'monitor_only' }}, {{ - localStorage: {{ getItem: () => 'mock-token' }}, - encrypt_old: (value) => value - }}); - if (calls !== 3) {{ - throw new Error(`expected 3 slice reads, got ${{calls}}`); - }} - if (!Array.isArray(result.pendingList) || result.pendingList.length !== 3) {{ - throw new Error(`expected merged pendingList length 3, got ${{result.pendingList && result.pendingList.length}}`); - }} - if (!result.readDiagnostics || !Array.isArray(result.readDiagnostics.readStepTraces) || result.readDiagnostics.readStepTraces.length !== 3) {{ - throw new Error('expected three readStepTraces entries'); - }} -}})().catch((err) => {{ - console.error(err.stack || String(err)); - process.exit(1); -}}); -"#, - detect_path = skill_root.join("scripts").join("detect.js") - ), - ) - .unwrap(); - - let output = run_node_script(&runner_path); - assert!( - output.status.success(), - "stdout={}\nstderr={}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} - -#[test] -fn business_page_report_detect_marks_timeout_as_structured_root_cause() { - let root = temp_workspace("sgclaw-business-page-timeout-root-cause"); - let source_evidence = root.join("source-evidence.json"); - let contract_path = root.join("contract.json"); - let trigger_path = root.join("trigger-contract.json"); - write_json(&source_evidence, &json!({})); - write_json( - &contract_path, - &json!({ - "workflowId": "available_balance_timeout_monitoring_action", - "displayName": "available balance timeout", - "defaultMode": "monitor_only", - "archetype": "business_page_report_monitor", - "runtimeContext": { - "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", - "expectedDomain": "yx.gs.sgcc.com.cn", - "gatewayDomain": "yxgateway.gs.sgcc.com.cn", - "localhostServiceBase": "http://localhost:13313", - "browserAttachedRequired": true, - "hostBridgeRequired": true, - "executionContextMode": "attached_page_direct", - "requestClientMode": "isolated_xhr", - "encryptionMode": "window_encrypt_old", - "attachedPageBrowserActionPolicy": "forbid_secondary_jump", - "platformWritePolicy": "skip_when_zero", - "readSlices": [ - { - "name": "slice_timeout", - "endpointBinding": "load_report", - "requestTemplateOverride": { "sliceType": "01" }, - "responsePath": "data.items", - "timeoutMs": 1, - "mergeRole": "concat", - "required": true - } - ], - "encryptionResolution": { - "primaryMethod": "deps.encrypt_old", - "fallbackMethods": [], - "requiredContext": [], - "hardFail": true - }, - "timeoutContract": { - "perStepTimeoutMs": 1, - "overallDetectTimeoutMs": 10, - "statusOnTimeout": "timeout", - "statusOnPartial": "partial" - } - }, - "businessApiDependencies": [ - { - "name": "load_report", - "url": "http://yxgateway.gs.sgcc.com.cn/report/load", - "classification": "read_report", - "sideEffect": false - } - ], - "sideEffectPolicy": { - "blockedCallSignatures": ["mac.sendMessages"] - } - }), - ); - write_json(&trigger_path, &minimal_trigger_contract()); - - let skill_root = generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "available-balance-timeout".to_string(), - scene_name: "available-balance-timeout".to_string(), - output_root: root.clone(), - source_evidence_json: source_evidence, - ir_contract_json: contract_path, - trigger_contract_json: trigger_path, - }, - ) - .unwrap(); - - let runner_path = root.join("run-timeout-detect.js"); - fs::write( - &runner_path, - format!( - r#"const detect = require({detect_path:?}); -global.fetch = async () => {{ - await new Promise((resolve) => setTimeout(resolve, 30)); - return {{ - ok: true, - status: 200, - json: async () => ({{ data: {{ items: [{{ id: 'late' }}] }} }}), - text: async () => JSON.stringify({{ data: {{ items: [{{ id: 'late' }}] }} }}) - }}; -}}; -(async () => {{ - const result = await detect.detect({{ mode: 'monitor_only' }}, {{ - localStorage: {{ getItem: () => 'mock-token' }}, - encrypt_old: (value) => value - }}); - if (result.status !== 'timeout') {{ - throw new Error(`expected timeout status, got ${{result.status}}`); - }} - if (!Array.isArray(result.readDiagnostics.readStepTraces) || result.readDiagnostics.readStepTraces[0].status !== 'timeout') {{ - throw new Error('expected timeout trace'); - }} -}})().catch((err) => {{ - console.error(err.stack || String(err)); - process.exit(1); -}}); -"#, - detect_path = skill_root.join("scripts").join("detect.js") - ), - ) - .unwrap(); - - let output = run_node_script(&runner_path); - assert!( - output.status.success(), - "stdout={}\nstderr={}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} - -#[test] -fn runtime_preserves_detect_root_cause_when_pending_count_is_zero() { - let root = temp_workspace("sgclaw-runtime-root-cause"); - let source_evidence = root.join("source-evidence.json"); - let contract_path = root.join("contract.json"); - let trigger_contract_path = root.join("trigger-contract.json"); - let trigger_path = root.join("scheduled-trigger.json"); - let output_path = root.join("results").join("available-balance-root-cause.run-record.json"); - let rules_path = root.join("resources").join("rules.json"); - write_json(&source_evidence, &json!({})); - write_json( - &contract_path, - &json!({ - "workflowId": "available_balance_root_cause_monitoring_action", - "displayName": "available balance root cause", - "defaultMode": "monitor_only", - "archetype": "business_page_report_monitor", - "runtimeContext": { - "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", - "expectedDomain": "yx.gs.sgcc.com.cn", - "gatewayDomain": "yxgateway.gs.sgcc.com.cn", - "localhostServiceBase": "http://localhost:13313", - "browserAttachedRequired": false, - "hostBridgeRequired": false, - "executionContextMode": "attached_page_direct", - "requestClientMode": "isolated_xhr", - "encryptionMode": "window_encrypt_old", - "attachedPageBrowserActionPolicy": "forbid_secondary_jump", - "platformWritePolicy": "skip_when_zero", - "readSlices": [ - { - "name": "slice_timeout", - "endpointBinding": "load_report", - "requestTemplateOverride": { "sliceType": "01" }, - "responsePath": "data.items", - "timeoutMs": 1, - "mergeRole": "concat", - "required": true - } - ], - "encryptionResolution": { - "primaryMethod": "deps.encrypt_old", - "fallbackMethods": [], - "requiredContext": [], - "hardFail": true - }, - "timeoutContract": { - "perStepTimeoutMs": 1, - "overallDetectTimeoutMs": 10, - "statusOnTimeout": "timeout", - "statusOnPartial": "partial" - } - }, - "businessApiDependencies": [ - { - "name": "load_report", - "url": "http://127.0.0.1:9/report/load", - "classification": "read_report", - "sideEffect": false - } - ], - "sideEffectPolicy": { - "blockedCallSignatures": ["mac.sendMessages"] - } - }), - ); - write_json(&trigger_contract_path, &minimal_trigger_contract()); - write_json( - &trigger_path, - &json!({ - "trigger_type": "scheduled", - "trigger_id": "available-balance-root-cause-read-only", - "workflow_id": "available_balance_root_cause_monitoring_action", - "mode": "monitor_only", - "interval_or_cron": "*/5 * * * *", - "timezone": "Asia/Shanghai", - "overlap_policy": "skip_if_running", - "scheduler_identity": "mock-scheduler", - "max_runtime_seconds": 60 - }), - ); - write_runtime_rules(&rules_path); - - let materialization_root = root.join("materialized"); - fs::create_dir_all(&materialization_root).unwrap(); - generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "available-balance-root-cause".to_string(), - scene_name: "available-balance-root-cause".to_string(), - output_root: materialization_root.clone(), - source_evidence_json: source_evidence, - ir_contract_json: contract_path, - trigger_contract_json: trigger_contract_path, - }, - ) - .unwrap(); - - let current_dir = std::env::current_dir().unwrap(); - std::env::set_current_dir(&root).unwrap(); - let record = run_scheduled_monitoring_skill_command_adapter( - ScheduledMonitoringSkillCommandAdapterRequest { - trigger_path: &trigger_path, - skills_dir: &materialization_root.join("skills"), - config_path: None, - output_path: &output_path, - watch: false, - max_runs: None, - }, - ) - .unwrap(); - std::env::set_current_dir(current_dir).unwrap(); - - assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 0); - assert_eq!( - record["previewArtifact"]["summary"]["detect_root_cause"], - "soft_error" - ); - assert_eq!(record["auditPreview"]["detectRootCause"], "soft_error"); - assert_eq!(record["previewArtifact"]["status"], "soft_error"); -} - -#[test] -fn materialization_rejects_business_page_report_without_slices() { - let root = temp_workspace("sgclaw-missing-slices"); - let source_evidence = root.join("source-evidence.json"); - let contract_path = root.join("contract.json"); - let trigger_path = root.join("trigger-contract.json"); - write_json(&source_evidence, &json!({})); - write_json( - &contract_path, - &json!({ - "workflowId": "missing_slices_monitoring_action", - "displayName": "missing slices", - "defaultMode": "monitor_only", - "archetype": "business_page_report_monitor", - "runtimeContext": { - "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", - "expectedDomain": "yx.gs.sgcc.com.cn", - "gatewayDomain": "yxgateway.gs.sgcc.com.cn", - "localhostServiceBase": "http://localhost:13313", - "browserAttachedRequired": true, - "hostBridgeRequired": true - }, - "sideEffectPolicy": { - "blockedCallSignatures": ["mac.sendMessages"] - } - }), - ); - write_json(&trigger_path, &minimal_trigger_contract()); - - let error = generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "missing-slices".to_string(), - scene_name: "missing-slices".to_string(), - output_root: root.clone(), - source_evidence_json: source_evidence, - ir_contract_json: contract_path, - trigger_contract_json: trigger_path, - }, - ) - .unwrap_err(); - assert!(error.to_string().contains("requires at least one read slice")); -} - -#[test] -fn materialization_rejects_delta_monitor_without_identity_fields() { - let root = temp_workspace("sgclaw-missing-delta-identity"); - let source_evidence = root.join("source-evidence.json"); - let contract_path = root.join("contract.json"); - let trigger_path = root.join("trigger-contract.json"); - write_json(&source_evidence, &json!({})); - write_json( - &contract_path, - &json!({ - "workflowId": "missing_delta_identity_monitoring_action", - "displayName": "missing delta identity", - "defaultMode": "monitor_only", - "archetype": "list_dedupe_monitor", - "runtimeContext": { - "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", - "expectedDomain": "yx.gs.sgcc.com.cn", - "gatewayDomain": "yxgateway.gs.sgcc.com.cn", - "localhostServiceBase": "http://localhost:13313", - "browserAttachedRequired": true, - "hostBridgeRequired": true, - "outputContract": { - "deltaState": { - "identityFields": [], - "stateSidecarPath": "state/test.json", - "comparisonMode": "new_rows_only", - "emitPolicy": "new_rows_only" - } - } - }, - "sideEffectPolicy": { - "blockedCallSignatures": ["mac.sendMessages"] - } - }), - ); - write_json(&trigger_path, &minimal_trigger_contract()); - - let error = generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "missing-delta-identity".to_string(), - scene_name: "missing-delta-identity".to_string(), - output_root: root.clone(), - source_evidence_json: source_evidence, - ir_contract_json: contract_path, - trigger_contract_json: trigger_path, - }, - ) - .unwrap_err(); - assert!(error.to_string().contains("delta_state.identity_fields")); -} - -#[test] -fn materialization_rejects_empty_encryption_resolution() { - let root = temp_workspace("sgclaw-empty-encryption"); - let source_evidence = root.join("source-evidence.json"); - let contract_path = root.join("contract.json"); - let trigger_path = root.join("trigger-contract.json"); - write_json(&source_evidence, &json!({})); - write_json( - &contract_path, - &json!({ - "workflowId": "empty_encryption_monitoring_action", - "displayName": "empty encryption", - "defaultMode": "monitor_only", - "archetype": "business_page_report_monitor", - "runtimeContext": { - "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", - "expectedDomain": "yx.gs.sgcc.com.cn", - "gatewayDomain": "yxgateway.gs.sgcc.com.cn", - "localhostServiceBase": "http://localhost:13313", - "browserAttachedRequired": true, - "hostBridgeRequired": true, - "encryptionMode": "", - "readSlices": [ - { - "name": "slice_01", - "endpointBinding": "load_report", - "requestTemplateOverride": {}, - "responsePath": "data.items", - "timeoutMs": 1000, - "mergeRole": "concat", - "required": true - } - ], - "encryptionResolution": { - "primaryMethod": "", - "fallbackMethods": [], - "requiredContext": [], - "hardFail": true - } - }, - "businessApiDependencies": [ - { - "name": "load_report", - "url": "http://yxgateway.gs.sgcc.com.cn/report/load", - "classification": "read_report", - "sideEffect": false - } - ], - "sideEffectPolicy": { - "blockedCallSignatures": ["mac.sendMessages"] - } - }), - ); - write_json(&trigger_path, &minimal_trigger_contract()); - - let error = generate_scheduled_monitoring_action_skill_package( - GenerateScheduledMonitoringActionSkillRequest { - scene_id: "empty-encryption".to_string(), - scene_name: "empty-encryption".to_string(), - output_root: root.clone(), - source_evidence_json: source_evidence, - ir_contract_json: contract_path, - trigger_contract_json: trigger_path, - }, - ) - .unwrap_err(); - assert!(error.to_string().contains("encryption resolution")); -}