refactor: keep generated scene runtime only in sgclaw
This commit is contained in:
@@ -1,37 +0,0 @@
|
||||
use sgclaw::generated_scene::lessons::{
|
||||
load_generation_lessons, GenerationLessons, BUILTIN_REPORT_COLLECTION_LESSONS,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn builtin_report_collection_lessons_match_required_generator_rules() {
|
||||
let lessons = GenerationLessons::default_report_collection();
|
||||
|
||||
assert_eq!(
|
||||
BUILTIN_REPORT_COLLECTION_LESSONS,
|
||||
"builtin:report_collection_v1"
|
||||
);
|
||||
assert!(lessons.routing.require_exact_suffix);
|
||||
assert!(lessons.routing.unsupported_scene_fail_closed);
|
||||
assert!(lessons.canonical_params.require_explicit_period);
|
||||
assert!(lessons.bootstrap.require_expected_domain);
|
||||
assert!(lessons.bootstrap.require_target_url);
|
||||
assert!(lessons.artifact.require_report_artifact);
|
||||
assert!(lessons.validation.require_pipe_and_ws_checks);
|
||||
assert!(lessons.validation.require_manual_service_console_smoke);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_lessons_toml_declares_required_generator_rules() {
|
||||
let lessons =
|
||||
load_generation_lessons("docs/superpowers/references/tq-lineloss-lessons-learned.toml")
|
||||
.unwrap();
|
||||
|
||||
assert!(lessons.routing.require_exact_suffix);
|
||||
assert!(lessons.routing.unsupported_scene_fail_closed);
|
||||
assert!(lessons.canonical_params.require_explicit_period);
|
||||
assert!(lessons.bootstrap.require_expected_domain);
|
||||
assert!(lessons.bootstrap.require_target_url);
|
||||
assert!(lessons.artifact.require_report_artifact);
|
||||
assert!(lessons.validation.require_pipe_and_ws_checks);
|
||||
assert!(lessons.validation.require_manual_service_console_smoke);
|
||||
}
|
||||
42
tests/generated_scene_runtime_boundary_test.rs
Normal file
42
tests/generated_scene_runtime_boundary_test.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
fn repo_root() -> &'static Path {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sgclaw_no_longer_ships_generated_scene_binary_entrypoint() {
|
||||
let binary_entrypoint = repo_root().join("src/bin/sg_scene_generate.rs");
|
||||
assert!(
|
||||
!binary_entrypoint.exists(),
|
||||
"sgclaw should not keep sg_scene_generate.rs once generated_scene_core owns generation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_scene_module_only_exports_runtime_adapter() {
|
||||
let module_source =
|
||||
fs::read_to_string(repo_root().join("src/generated_scene/mod.rs")).unwrap();
|
||||
|
||||
assert!(
|
||||
module_source.contains("pub mod scheduled_monitoring_runtime;"),
|
||||
"generated_scene runtime adapter must remain exported"
|
||||
);
|
||||
assert!(
|
||||
!module_source.contains("pub mod analyzer;"),
|
||||
"generated_scene analyzer export must move out of sgclaw"
|
||||
);
|
||||
assert!(
|
||||
!module_source.contains("pub mod generator;"),
|
||||
"generated_scene generator export must move out of sgclaw"
|
||||
);
|
||||
assert!(
|
||||
!module_source.contains("pub mod ir;"),
|
||||
"generated_scene ir export must move out of sgclaw"
|
||||
);
|
||||
assert!(
|
||||
!module_source.contains("pub mod lessons;"),
|
||||
"generated_scene lessons export must move out of sgclaw"
|
||||
);
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest};
|
||||
use sgclaw::generated_scene::ir::SceneIr;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CanonicalManifest {
|
||||
targets: Vec<CanonicalTarget>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CanonicalTarget {
|
||||
id: String,
|
||||
#[serde(rename = "fixtureDir")]
|
||||
fixture_dir: String,
|
||||
#[serde(rename = "canonicalSceneIr")]
|
||||
canonical_scene_ir: String,
|
||||
#[serde(rename = "requiredEvidenceTypes")]
|
||||
required_evidence_types: Vec<String>,
|
||||
#[serde(rename = "requiredWorkflowStepTypes")]
|
||||
required_workflow_step_types: Vec<String>,
|
||||
#[serde(rename = "requiredGateNames")]
|
||||
required_gate_names: Vec<String>,
|
||||
#[serde(rename = "acceptanceChecklist")]
|
||||
acceptance_checklist: Vec<String>,
|
||||
#[serde(rename = "failureTaxonomy")]
|
||||
failure_taxonomy: Vec<String>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn p0_canonical_manifest_is_actionable() {
|
||||
let manifest = load_manifest();
|
||||
assert_eq!(manifest.targets.len(), 3);
|
||||
|
||||
for target in manifest.targets {
|
||||
assert!(
|
||||
Path::new(&target.fixture_dir).exists(),
|
||||
"fixture dir missing: {}",
|
||||
target.fixture_dir
|
||||
);
|
||||
assert!(
|
||||
Path::new(&target.canonical_scene_ir).exists(),
|
||||
"canonical ir missing: {}",
|
||||
target.canonical_scene_ir
|
||||
);
|
||||
assert!(
|
||||
!target.required_evidence_types.is_empty(),
|
||||
"required_evidence_types should not be empty for {}",
|
||||
target.id
|
||||
);
|
||||
assert!(
|
||||
!target.required_workflow_step_types.is_empty(),
|
||||
"required_workflow_step_types should not be empty for {}",
|
||||
target.id
|
||||
);
|
||||
assert!(
|
||||
!target.required_gate_names.is_empty(),
|
||||
"required_gate_names should not be empty for {}",
|
||||
target.id
|
||||
);
|
||||
assert!(
|
||||
!target.acceptance_checklist.is_empty(),
|
||||
"acceptance_checklist should not be empty for {}",
|
||||
target.id
|
||||
);
|
||||
assert!(
|
||||
!target.failure_taxonomy.is_empty(),
|
||||
"failure_taxonomy should not be empty for {}",
|
||||
target.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_p0_fixtures_align_with_canonical_answers() {
|
||||
let manifest = load_manifest();
|
||||
|
||||
for target in manifest.targets {
|
||||
let output_root = temp_workspace(&format!("sgclaw-canonical-{}", target.id));
|
||||
let scene_id = scene_id_from_target(&target.id);
|
||||
let scene_name = scene_name_from_target(&target.id);
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from(&target.fixture_dir),
|
||||
scene_id,
|
||||
scene_name,
|
||||
scene_kind: None,
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: None,
|
||||
})
|
||||
.unwrap_or_else(|err| panic!("{} failed to generate: {}", target.id, err));
|
||||
|
||||
let generated_dir = output_root
|
||||
.join("skills")
|
||||
.join(scene_id_from_target(&target.id));
|
||||
let generated_report: SceneIr = serde_json::from_str(
|
||||
&fs::read_to_string(generated_dir.join("references/generation-report.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let canonical: SceneIr =
|
||||
serde_json::from_str(&fs::read_to_string(&target.canonical_scene_ir).unwrap()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
generated_report.workflow_archetype().as_str(),
|
||||
canonical.workflow_archetype().as_str(),
|
||||
"archetype mismatch for {}",
|
||||
target.id
|
||||
);
|
||||
assert_eq!(
|
||||
generated_report.bootstrap.expected_domain, canonical.bootstrap.expected_domain,
|
||||
"expectedDomain mismatch for {}",
|
||||
target.id
|
||||
);
|
||||
assert!(
|
||||
generated_report
|
||||
.bootstrap
|
||||
.target_url
|
||||
.starts_with(&canonical.bootstrap.target_url),
|
||||
"targetUrl mismatch for {}: {} vs {}",
|
||||
target.id,
|
||||
generated_report.bootstrap.target_url,
|
||||
canonical.bootstrap.target_url
|
||||
);
|
||||
|
||||
let generated_step_types = generated_report
|
||||
.workflow_steps
|
||||
.iter()
|
||||
.map(|step| step.step_type.clone())
|
||||
.collect::<Vec<_>>();
|
||||
for required in &target.required_workflow_step_types {
|
||||
assert!(
|
||||
generated_step_types.iter().any(|step| step == required),
|
||||
"missing workflow step {} for {}",
|
||||
required,
|
||||
target.id
|
||||
);
|
||||
}
|
||||
|
||||
let generated_gate_names = generated_report
|
||||
.readiness
|
||||
.gates
|
||||
.iter()
|
||||
.map(|gate| gate.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
for required in &target.required_gate_names {
|
||||
assert!(
|
||||
generated_gate_names.iter().any(|gate| gate == required),
|
||||
"missing readiness gate {} for {}",
|
||||
required,
|
||||
target.id
|
||||
);
|
||||
}
|
||||
|
||||
let generated_evidence_types = generated_report
|
||||
.evidence
|
||||
.iter()
|
||||
.map(|item| item.evidence_type.clone())
|
||||
.collect::<Vec<_>>();
|
||||
for required in &target.required_evidence_types {
|
||||
assert!(
|
||||
generated_evidence_types.iter().any(|kind| kind == required),
|
||||
"missing evidence type {} for {}",
|
||||
required,
|
||||
target.id
|
||||
);
|
||||
}
|
||||
|
||||
let generated_json: Value = serde_json::from_str(
|
||||
&fs::read_to_string(generated_dir.join("references/generation-report.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
generated_json.get("readiness").is_some(),
|
||||
"generation-report.json should include readiness for {}",
|
||||
target.id
|
||||
);
|
||||
|
||||
if target.id == "p0-3-paginated-enrichment" {
|
||||
assert_eq!(
|
||||
generated_report
|
||||
.main_request
|
||||
.as_ref()
|
||||
.map(|request| request.response_path.as_str()),
|
||||
canonical
|
||||
.main_request
|
||||
.as_ref()
|
||||
.map(|request| request.response_path.as_str()),
|
||||
"g3 main request response path mismatch for {}",
|
||||
target.id
|
||||
);
|
||||
assert_eq!(
|
||||
generated_report
|
||||
.pagination_plan
|
||||
.as_ref()
|
||||
.map(|plan| plan.page_field.as_str()),
|
||||
canonical
|
||||
.pagination_plan
|
||||
.as_ref()
|
||||
.map(|plan| plan.page_field.as_str()),
|
||||
"g3 page field mismatch for {}",
|
||||
target.id
|
||||
);
|
||||
assert_eq!(
|
||||
generated_report
|
||||
.pagination_plan
|
||||
.as_ref()
|
||||
.map(|plan| plan.termination_rule.as_str()),
|
||||
canonical
|
||||
.pagination_plan
|
||||
.as_ref()
|
||||
.map(|plan| plan.termination_rule.as_str()),
|
||||
"g3 termination rule mismatch for {}",
|
||||
target.id
|
||||
);
|
||||
assert_eq!(
|
||||
generated_report.join_keys, canonical.join_keys,
|
||||
"g3 join keys mismatch for {}",
|
||||
target.id
|
||||
);
|
||||
assert_eq!(
|
||||
generated_report.merge_or_dedupe_rules, canonical.merge_or_dedupe_rules,
|
||||
"g3 merge/dedupe rules mismatch for {}",
|
||||
target.id
|
||||
);
|
||||
assert_eq!(
|
||||
generated_report
|
||||
.export_plan
|
||||
.as_ref()
|
||||
.and_then(|plan| plan.entry.as_deref()),
|
||||
canonical
|
||||
.export_plan
|
||||
.as_ref()
|
||||
.and_then(|plan| plan.entry.as_deref()),
|
||||
"g3 export entry mismatch for {}",
|
||||
target.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_manifest() -> CanonicalManifest {
|
||||
serde_json::from_str(
|
||||
&fs::read_to_string(
|
||||
"tests/fixtures/generated_scene/p0_canonical_answers/p0-canonical-manifest.json",
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn scene_id_from_target(target_id: &str) -> String {
|
||||
match target_id {
|
||||
"p0-1-tq-lineloss-report" => "tq-lineloss-report".to_string(),
|
||||
"p0-2-single-request-table" => "single-request-report".to_string(),
|
||||
"p0-3-paginated-enrichment" => "paginated-enrichment-report".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scene_name_from_target(target_id: &str) -> String {
|
||||
match target_id {
|
||||
"p0-1-tq-lineloss-report" => "台区线损月周累计统计分析".to_string(),
|
||||
"p0-2-single-request-table" => "单请求通用报表".to_string(),
|
||||
"p0-3-paginated-enrichment" => "分页补数明细报表".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_workspace(prefix: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("{prefix}-{nanos}"));
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
path
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const {
|
||||
buildAnalyzePrompt,
|
||||
extractJsonFromResponse,
|
||||
isRetryableLlmError,
|
||||
repairCommonJsonIssues,
|
||||
} = require("../frontend/scene-generator/llm-client");
|
||||
const {
|
||||
buildDeterministicSceneIr,
|
||||
readDirectory,
|
||||
validateSceneIdCandidate,
|
||||
} = require("../frontend/scene-generator/generator-runner");
|
||||
const {
|
||||
getGenerationBlockers,
|
||||
mergeSceneIr,
|
||||
sanitizeSceneIr,
|
||||
} = require("../frontend/scene-generator/server");
|
||||
|
||||
function testBuildAnalyzePromptIncludesFileContents() {
|
||||
const dirContents = {
|
||||
"scene.toml": '[scene]\nid = "test-scene"',
|
||||
scripts: { "collect_test.js": "async function main() {}" },
|
||||
tree: "├── scene.toml\n└── collect_test.js",
|
||||
};
|
||||
|
||||
const prompt = buildAnalyzePrompt("D:/test/scenario", dirContents);
|
||||
|
||||
assert.ok(prompt.includes("scene.toml"), "should include scene.toml");
|
||||
assert.ok(prompt.includes("collect_test.js"), "should include script name");
|
||||
assert.ok(prompt.includes("D:/test/scenario"), "should include sourceDir");
|
||||
console.log("PASS: testBuildAnalyzePromptIncludesFileContents");
|
||||
}
|
||||
|
||||
function testExtractJsonFromResponse() {
|
||||
const withMarkdown =
|
||||
'```json\n{"sceneId": "test", "sceneName": "测试"}\n```';
|
||||
const plain = '{"sceneId": "test", "sceneName": "测试"}';
|
||||
const withPrefix =
|
||||
'Here is the result:\n{"sceneId": "test", "sceneName": "测试"}';
|
||||
|
||||
assert.deepStrictEqual(extractJsonFromResponse(withMarkdown), {
|
||||
sceneId: "test",
|
||||
sceneName: "测试",
|
||||
});
|
||||
assert.deepStrictEqual(extractJsonFromResponse(plain), {
|
||||
sceneId: "test",
|
||||
sceneName: "测试",
|
||||
});
|
||||
assert.deepStrictEqual(extractJsonFromResponse(withPrefix), {
|
||||
sceneId: "test",
|
||||
sceneName: "测试",
|
||||
});
|
||||
console.log("PASS: testExtractJsonFromResponse");
|
||||
}
|
||||
|
||||
function testExtractJsonFromResponseRepairsMissingArrayComma() {
|
||||
const malformed =
|
||||
'{"sceneId":"marketing-zero-consumer-report","evidence":[{"kind":"a"} {"kind":"b"}],"sceneName":"营销"}';
|
||||
|
||||
const result = extractJsonFromResponse(malformed);
|
||||
|
||||
assert.strictEqual(result.sceneId, "marketing-zero-consumer-report");
|
||||
assert.strictEqual(Array.isArray(result.evidence), true);
|
||||
assert.strictEqual(result.evidence.length, 2);
|
||||
console.log("PASS: testExtractJsonFromResponseRepairsMissingArrayComma");
|
||||
}
|
||||
|
||||
function testRepairCommonJsonIssuesRemovesTrailingCommas() {
|
||||
const malformed =
|
||||
'{\n "sceneId": "marketing-zero-consumer-report",\n "evidence": [{"kind":"a",},],\n}';
|
||||
const repaired = repairCommonJsonIssues(malformed);
|
||||
const parsed = JSON.parse(repaired);
|
||||
|
||||
assert.strictEqual(parsed.sceneId, "marketing-zero-consumer-report");
|
||||
assert.strictEqual(parsed.evidence.length, 1);
|
||||
console.log("PASS: testRepairCommonJsonIssuesRemovesTrailingCommas");
|
||||
}
|
||||
|
||||
function testIsRetryableLlmErrorRecognizesTimeouts() {
|
||||
assert.strictEqual(isRetryableLlmError(new Error("LLM API request timed out")), true);
|
||||
assert.strictEqual(isRetryableLlmError(new Error("LLM API error 503: upstream unavailable")), true);
|
||||
assert.strictEqual(isRetryableLlmError(new Error("LLM response missing sceneId")), false);
|
||||
console.log("PASS: testIsRetryableLlmErrorRecognizesTimeouts");
|
||||
}
|
||||
|
||||
function testDeterministicNamingAvoidsDegenerateSlugFallback() {
|
||||
const sceneIr = buildDeterministicSceneIr(
|
||||
{ deterministicSignals: {} },
|
||||
"D:/tmp/营销2.0零度户报表数据生成"
|
||||
);
|
||||
|
||||
assert.strictEqual(sceneIr.sceneId, "marketing-zero-consumer-report");
|
||||
assert.strictEqual(sceneIr.sceneIdDiagnostics.valid, true);
|
||||
assert.strictEqual(sceneIr.sceneIdDiagnostics.candidateSource, "deterministic_keywords");
|
||||
console.log("PASS: testDeterministicNamingAvoidsDegenerateSlugFallback");
|
||||
}
|
||||
|
||||
function testValidateSceneIdCandidateRejectsLowEntropyIds() {
|
||||
const invalid = validateSceneIdCandidate("2-0", {
|
||||
sceneName: "营销2.0零度户报表数据生成",
|
||||
sourceDir: "D:/tmp/营销2.0零度户报表数据生成",
|
||||
});
|
||||
|
||||
assert.strictEqual(invalid.valid, false);
|
||||
assert.ok(
|
||||
["numeric_only_scene_id", "numeric_dominant_scene_id", "scene_id_too_short"].includes(invalid.reason),
|
||||
`unexpected invalid reason: ${invalid.reason}`
|
||||
);
|
||||
console.log("PASS: testValidateSceneIdCandidateRejectsLowEntropyIds");
|
||||
}
|
||||
|
||||
function testMergeSceneIrPrefersValidSceneIdOverInvalidLlmValue() {
|
||||
const deterministic = sanitizeSceneIr({
|
||||
sceneId: "marketing-zero-consumer-report",
|
||||
sceneIdDiagnostics: {
|
||||
candidateSource: "deterministic_keywords",
|
||||
valid: true,
|
||||
candidates: [{ value: "marketing-zero-consumer-report", source: "deterministic_keywords", valid: true }],
|
||||
},
|
||||
sceneName: "营销2.0零度户报表数据生成",
|
||||
bootstrap: { expectedDomain: "yx.gs.sgcc.com.cn", targetUrl: "http://yx.gs.sgcc.com.cn" },
|
||||
workflowSteps: [{ type: "request" }],
|
||||
apiEndpoints: [{ name: "userList", url: "http://yx.gs.sgcc.com.cn/list", method: "POST" }],
|
||||
validationHints: { runtimeCompatible: true },
|
||||
readiness: { level: "B" },
|
||||
});
|
||||
const llm = sanitizeSceneIr({
|
||||
sceneId: "2-0",
|
||||
sceneIdDiagnostics: {
|
||||
candidateSource: "llm_semantic",
|
||||
valid: false,
|
||||
invalidReason: "numeric_dominant_scene_id",
|
||||
candidates: [{ value: "2-0", source: "llm_semantic", valid: false, reason: "numeric_dominant_scene_id" }],
|
||||
},
|
||||
sceneName: "营销2.0零度户报表数据生成",
|
||||
bootstrap: { expectedDomain: "yx.gs.sgcc.com.cn", targetUrl: "http://yx.gs.sgcc.com.cn" },
|
||||
workflowSteps: [{ type: "request" }],
|
||||
apiEndpoints: [{ name: "userList", url: "http://yx.gs.sgcc.com.cn/list", method: "POST" }],
|
||||
validationHints: { runtimeCompatible: true },
|
||||
readiness: { level: "B" },
|
||||
});
|
||||
const warnings = [];
|
||||
|
||||
const merged = mergeSceneIr(deterministic, llm, warnings);
|
||||
|
||||
assert.strictEqual(merged.sceneId, "marketing-zero-consumer-report");
|
||||
assert.strictEqual(merged.sceneIdDiagnostics.valid, true);
|
||||
assert.ok(warnings.some((item) => item.includes("SceneId conflict")));
|
||||
console.log("PASS: testMergeSceneIrPrefersValidSceneIdOverInvalidLlmValue");
|
||||
}
|
||||
|
||||
function testGetGenerationBlockersRejectsInvalidSceneId() {
|
||||
const blockers = getGenerationBlockers({
|
||||
sceneIr: {
|
||||
sceneIdDiagnostics: {
|
||||
valid: false,
|
||||
invalidReason: "numeric_dominant_scene_id",
|
||||
},
|
||||
},
|
||||
sceneId: "2-0",
|
||||
sceneName: "营销2.0零度户报表数据生成",
|
||||
sourceDir: "D:/tmp/营销2.0零度户报表数据生成",
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
blockers.some((item) => item.startsWith("invalid_scene_id:")),
|
||||
`expected invalid_scene_id blocker, got ${JSON.stringify(blockers)}`
|
||||
);
|
||||
assert.ok(blockers.includes("analysis_invalid_scene_id:numeric_dominant_scene_id"));
|
||||
console.log("PASS: testGetGenerationBlockersRejectsInvalidSceneId");
|
||||
}
|
||||
|
||||
function testBootstrapPrefersBusinessEntryOverLocalhostExport() {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sgclaw-bootstrap-"));
|
||||
const sceneDir = path.join(tempRoot, "bootstrap");
|
||||
fs.mkdirSync(sceneDir);
|
||||
fs.writeFileSync(
|
||||
path.join(sceneDir, "index.html"),
|
||||
`<!doctype html><html><body><script>
|
||||
const sourceUrl = "http://yx.gs.sgcc.com.cn";
|
||||
const apiUrl = "http://yxgateway.gs.sgcc.com.cn/api";
|
||||
function getRows() {
|
||||
return $.ajax({ url: "http://yxgateway.gs.sgcc.com.cn/marketing/userList", type: "POST" });
|
||||
}
|
||||
function exportExcel() {
|
||||
return $.ajax({ url: "http://localhost:13313/SurfaceServices/personalBread/export/faultDetailsExportXLSX", type: "POST" });
|
||||
}
|
||||
</script></body></html>`,
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const sceneIr = readDirectory(sceneDir).deterministic;
|
||||
|
||||
assert.strictEqual(sceneIr.bootstrap.expectedDomain, "yx.gs.sgcc.com.cn");
|
||||
assert.strictEqual(sceneIr.bootstrap.targetUrl, "http://yx.gs.sgcc.com.cn/");
|
||||
console.log("PASS: testBootstrapPrefersBusinessEntryOverLocalhostExport");
|
||||
}
|
||||
|
||||
function testBootstrapBecomesUnresolvedWhenOnlyLocalhostExists() {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sgclaw-bootstrap-local-"));
|
||||
const sceneDir = path.join(tempRoot, "bootstrap-local");
|
||||
fs.mkdirSync(sceneDir);
|
||||
fs.writeFileSync(
|
||||
path.join(sceneDir, "index.html"),
|
||||
`<!doctype html><html><body><script>
|
||||
function exportExcel() {
|
||||
return $.ajax({ url: "http://localhost:13313/SurfaceServices/personalBread/export/faultDetailsExportXLSX", type: "POST" });
|
||||
}
|
||||
</script></body></html>`,
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const sceneIr = readDirectory(sceneDir).deterministic;
|
||||
|
||||
assert.strictEqual(sceneIr.bootstrap.expectedDomain, "");
|
||||
assert.strictEqual(sceneIr.bootstrap.targetUrl, "");
|
||||
assert.ok(sceneIr.readiness.missingPieces.includes("bootstrap_target"));
|
||||
console.log("PASS: testBootstrapBecomesUnresolvedWhenOnlyLocalhostExists");
|
||||
}
|
||||
|
||||
function testWorkflowClassificationPrefersPaginatedOverGenericModeNoise() {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sgclaw-workflow-"));
|
||||
const sceneDir = path.join(tempRoot, "workflow");
|
||||
fs.mkdirSync(sceneDir);
|
||||
fs.writeFileSync(
|
||||
path.join(sceneDir, "index.html"),
|
||||
`<!doctype html><html><body><script>
|
||||
const type = "list";
|
||||
const status = "ready";
|
||||
async function loadData(page, pageSize) {
|
||||
return $.ajax({ url: "http://yx.gs.sgcc.com.cn/marketing/userList", type: "POST", data: JSON.stringify({ page, pageSize }) });
|
||||
}
|
||||
async function getChargeInfo(custNo) {
|
||||
return $.ajax({ url: "http://yx.gs.sgcc.com.cn/marketing/userCharges", type: "POST", data: JSON.stringify({ custNo }) });
|
||||
}
|
||||
function exportExcel(rows) { return rows.length; }
|
||||
function run(rows) {
|
||||
return rows.filter((row) => row.charge !== 0);
|
||||
}
|
||||
</script></body></html>`,
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const sceneIr = readDirectory(sceneDir).deterministic;
|
||||
|
||||
assert.strictEqual(sceneIr.workflowArchetype, "paginated_enrichment");
|
||||
assert.ok(sceneIr.workflowEvidence.paginationFields.length > 0);
|
||||
assert.ok(sceneIr.workflowEvidence.secondaryRequestEntries.length > 0);
|
||||
assert.ok(sceneIr.workflowEvidence.postProcessSteps.length > 0);
|
||||
console.log("PASS: testWorkflowClassificationPrefersPaginatedOverGenericModeNoise");
|
||||
}
|
||||
|
||||
function testWorkflowClassificationDoesNotEmitPaginatedWithoutPostProcess() {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sgclaw-workflow-no-post-"));
|
||||
const sceneDir = path.join(tempRoot, "workflow-no-post");
|
||||
fs.mkdirSync(sceneDir);
|
||||
fs.writeFileSync(
|
||||
path.join(sceneDir, "index.html"),
|
||||
`<!doctype html><html><body><script>
|
||||
async function loadData(page, pageSize) {
|
||||
return $.ajax({ url: "http://yx.gs.sgcc.com.cn/marketing/userList", type: "POST", data: JSON.stringify({ page, pageSize }) });
|
||||
}
|
||||
async function getChargeInfo(custNo) {
|
||||
return $.ajax({ url: "http://yx.gs.sgcc.com.cn/marketing/userCharges", type: "POST", data: JSON.stringify({ custNo }) });
|
||||
}
|
||||
</script></body></html>`,
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const sceneIr = readDirectory(sceneDir).deterministic;
|
||||
|
||||
assert.notStrictEqual(sceneIr.workflowArchetype, "paginated_enrichment");
|
||||
console.log("PASS: testWorkflowClassificationDoesNotEmitPaginatedWithoutPostProcess");
|
||||
}
|
||||
|
||||
function testGenerationBlockersIncludeFailedReadinessGates() {
|
||||
const blockers = getGenerationBlockers({
|
||||
sceneIr: {
|
||||
readiness: {
|
||||
gates: [
|
||||
{ name: "bootstrap_resolved", passed: false, reason: "bootstrap_target" },
|
||||
{ name: "request_contract_complete", passed: false, reason: "request_endpoint" },
|
||||
{ name: "response_contract_complete", passed: false, reason: "response_path" },
|
||||
{ name: "workflow_contract_complete", passed: false, reason: "post_process" },
|
||||
{ name: "workflow_complete_for_archetype", passed: false, reason: "post_process" },
|
||||
],
|
||||
},
|
||||
},
|
||||
sceneId: "marketing-zero-consumer-report",
|
||||
sceneName: "营销2.0零度户报表数据生成",
|
||||
sourceDir: "D:/tmp/营销2.0零度户报表数据生成",
|
||||
});
|
||||
|
||||
assert.ok(blockers.includes("gate_failed:bootstrap_resolved:bootstrap_target"));
|
||||
assert.ok(blockers.includes("gate_failed:request_contract_complete:request_endpoint"));
|
||||
assert.ok(blockers.includes("gate_failed:response_contract_complete:response_path"));
|
||||
assert.ok(blockers.includes("gate_failed:workflow_contract_complete:post_process"));
|
||||
assert.ok(blockers.includes("gate_failed:workflow_complete_for_archetype:post_process"));
|
||||
console.log("PASS: testGenerationBlockersIncludeFailedReadinessGates");
|
||||
}
|
||||
|
||||
testBuildAnalyzePromptIncludesFileContents();
|
||||
testExtractJsonFromResponse();
|
||||
testExtractJsonFromResponseRepairsMissingArrayComma();
|
||||
testRepairCommonJsonIssuesRemovesTrailingCommas();
|
||||
testIsRetryableLlmErrorRecognizesTimeouts();
|
||||
testDeterministicNamingAvoidsDegenerateSlugFallback();
|
||||
testValidateSceneIdCandidateRejectsLowEntropyIds();
|
||||
testMergeSceneIrPrefersValidSceneIdOverInvalidLlmValue();
|
||||
testGetGenerationBlockersRejectsInvalidSceneId();
|
||||
testBootstrapPrefersBusinessEntryOverLocalhostExport();
|
||||
testBootstrapBecomesUnresolvedWhenOnlyLocalhostExists();
|
||||
testWorkflowClassificationPrefersPaginatedOverGenericModeNoise();
|
||||
testWorkflowClassificationDoesNotEmitPaginatedWithoutPostProcess();
|
||||
testGenerationBlockersIncludeFailedReadinessGates();
|
||||
@@ -1,329 +0,0 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use sgclaw::generated_scene::analyzer::SceneKind;
|
||||
use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest};
|
||||
use sgclaw::generated_scene::ir::{
|
||||
ApiEndpointIr, ModeConditionIr, ModeIr, NormalizeRulesIr, SceneIdDiagnosticsIr, SceneIr,
|
||||
WorkflowArchetype,
|
||||
};
|
||||
|
||||
fn make_test_mode(
|
||||
name: &str,
|
||||
url: &str,
|
||||
content_type: Option<&str>,
|
||||
response_path: &str,
|
||||
) -> ModeIr {
|
||||
ModeIr {
|
||||
name: name.to_string(),
|
||||
label: Some(name.to_string()),
|
||||
condition: Some(ModeConditionIr {
|
||||
field: "period_mode".to_string(),
|
||||
operator: "equals".to_string(),
|
||||
value: serde_json::Value::String(name.to_string()),
|
||||
}),
|
||||
api_endpoint: Some(ApiEndpointIr {
|
||||
name: format!("{}_endpoint", name),
|
||||
url: url.to_string(),
|
||||
method: "POST".to_string(),
|
||||
content_type: content_type.map(|s| s.to_string()),
|
||||
description: None,
|
||||
}),
|
||||
column_defs: vec![("id".to_string(), "ID".to_string())],
|
||||
request_template: serde_json::json!({ "mode": name }),
|
||||
request_field_mappings: Vec::new(),
|
||||
normalize_rules: Some(NormalizeRulesIr {
|
||||
rules_type: "validate_required".to_string(),
|
||||
required_fields: vec!["id".to_string()],
|
||||
filter_null: true,
|
||||
}),
|
||||
response_path: response_path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_test_scene_ir(modes: Vec<ModeIr>) -> SceneIr {
|
||||
let is_multi = modes.len() > 1;
|
||||
let api_endpoints = modes
|
||||
.iter()
|
||||
.filter_map(|mode| mode.api_endpoint.clone())
|
||||
.collect::<Vec<_>>();
|
||||
SceneIr {
|
||||
scene_id: "test-scene".to_string(),
|
||||
scene_id_diagnostics: SceneIdDiagnosticsIr::default(),
|
||||
scene_name: "Test Scene".to_string(),
|
||||
scene_kind: "report_collection".to_string(),
|
||||
workflow_archetype: Some(if is_multi {
|
||||
WorkflowArchetype::MultiModeRequest
|
||||
} else {
|
||||
WorkflowArchetype::SingleRequestTable
|
||||
}),
|
||||
bootstrap: Default::default(),
|
||||
params: Vec::new(),
|
||||
modes,
|
||||
default_mode: Some("month".to_string()),
|
||||
mode_switch_field: Some("period_mode".to_string()),
|
||||
workflow_steps: vec![
|
||||
sgclaw::generated_scene::ir::WorkflowStepIr {
|
||||
step_type: "request".to_string(),
|
||||
description: Some("select mode and query corresponding endpoint".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
sgclaw::generated_scene::ir::WorkflowStepIr {
|
||||
step_type: "transform".to_string(),
|
||||
description: Some("normalize mode-specific table rows".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
workflow_evidence: Default::default(),
|
||||
main_request: None,
|
||||
pagination_plan: None,
|
||||
enrichment_requests: Vec::new(),
|
||||
join_keys: Vec::new(),
|
||||
merge_or_dedupe_rules: Vec::new(),
|
||||
export_plan: None,
|
||||
merge_plan: None,
|
||||
request_template: serde_json::Value::Null,
|
||||
response_path: "".to_string(),
|
||||
normalize_rules: None,
|
||||
artifact_contract: Default::default(),
|
||||
validation_hints: Default::default(),
|
||||
evidence: Vec::new(),
|
||||
readiness: Default::default(),
|
||||
api_endpoints,
|
||||
runtime_dependencies: Vec::new(),
|
||||
static_params: Default::default(),
|
||||
column_defs: Vec::new(),
|
||||
confidence: 0.0,
|
||||
uncertainties: Vec::new(),
|
||||
monitoring_action_workflow: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_workspace(prefix: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("{prefix}-{nanos}"));
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
/// Test 1: Single request table uses dedicated simple-request path instead of MODES fallback.
|
||||
#[test]
|
||||
fn test_single_request_table_uses_dedicated_path() {
|
||||
let output_root = temp_workspace("sgclaw-single-mode-test");
|
||||
let modes = vec![make_test_mode(
|
||||
"month",
|
||||
"http://example.com/api/month",
|
||||
None,
|
||||
"data",
|
||||
)];
|
||||
let scene_ir = make_test_scene_ir(modes);
|
||||
|
||||
// Use SingleRequestTable archetype - the compile path should stay on the dedicated single-request route.
|
||||
let mut scene_ir = scene_ir;
|
||||
scene_ir.workflow_archetype = Some(WorkflowArchetype::SingleRequestTable);
|
||||
scene_ir.api_endpoints = vec![ApiEndpointIr {
|
||||
name: "default_endpoint".to_string(),
|
||||
url: "http://example.com/api/data".to_string(),
|
||||
method: "POST".to_string(),
|
||||
content_type: None,
|
||||
description: None,
|
||||
}];
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "single-mode-scene".to_string(),
|
||||
scene_name: "Single Mode Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/single-mode-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_single_mode_scene.js")).unwrap();
|
||||
|
||||
assert!(
|
||||
generated_script.contains("const REQUEST_TEMPLATE ="),
|
||||
"Generated JS should contain REQUEST_TEMPLATE on the dedicated single-request path"
|
||||
);
|
||||
assert!(
|
||||
!generated_script.contains("const MODES ="),
|
||||
"Generated JS should no longer route SingleRequestTable through MODES fallback"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 2: Multi-mode generates mode routing (detectMode and MODES.find)
|
||||
#[test]
|
||||
fn test_multi_mode_generates_mode_routing() {
|
||||
let output_root = temp_workspace("sgclaw-multi-mode-test");
|
||||
let modes = vec![
|
||||
make_test_mode("month", "http://example.com/api/month", None, "data"),
|
||||
make_test_mode("week", "http://example.com/api/week", None, "data"),
|
||||
];
|
||||
let scene_ir = make_test_scene_ir(modes);
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "multi-mode-scene".to_string(),
|
||||
scene_name: "Multi Mode Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/multi-mode-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_multi_mode_scene.js")).unwrap();
|
||||
|
||||
assert!(
|
||||
generated_script.contains("function detectMode"),
|
||||
"Generated JS should contain 'detectMode' function for multi-mode routing"
|
||||
);
|
||||
assert!(
|
||||
generated_script.contains("MODES.find"),
|
||||
"Generated JS should contain 'MODES.find' for mode selection"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 3: Form-urlencoded request body uses Object.entries().join('&') not JSON.stringify
|
||||
#[test]
|
||||
fn test_form_urlencoded_request_body() {
|
||||
let output_root = temp_workspace("sgclaw-form-urlencoded-test");
|
||||
let modes = vec![make_test_mode(
|
||||
"month",
|
||||
"http://example.com/api/month",
|
||||
Some("application/x-www-form-urlencoded"),
|
||||
"data",
|
||||
)];
|
||||
let mut scene_ir = make_test_scene_ir(modes);
|
||||
scene_ir.workflow_archetype = Some(WorkflowArchetype::MultiModeRequest);
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "form-urlencoded-scene".to_string(),
|
||||
scene_name: "Form URL Encoded Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/form-urlencoded-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_form_urlencoded_scene.js")).unwrap();
|
||||
|
||||
// The buildModeRequest function should use Object.entries for form-urlencoded
|
||||
assert!(
|
||||
generated_script.contains("Object.entries(requestBody)"),
|
||||
"Generated JS should use Object.entries for form-urlencoded body encoding"
|
||||
);
|
||||
assert!(
|
||||
generated_script.contains(".join('&')"),
|
||||
"Generated JS should join form-urlencoded entries with '&'"
|
||||
);
|
||||
// Verify the conditional exists in buildModeRequest
|
||||
assert!(
|
||||
generated_script.contains("application/x-www-form-urlencoded"),
|
||||
"Generated JS should reference form-urlencoded content type"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 4: Response path extraction uses mode.responsePath in the template
|
||||
#[test]
|
||||
fn test_response_path_extraction_in_template() {
|
||||
let output_root = temp_workspace("sgclaw-response-path-test");
|
||||
let modes = vec![make_test_mode(
|
||||
"month",
|
||||
"http://example.com/api/month",
|
||||
None,
|
||||
"data.list",
|
||||
)];
|
||||
let mut scene_ir = make_test_scene_ir(modes);
|
||||
scene_ir.workflow_archetype = Some(WorkflowArchetype::MultiModeRequest);
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "response-path-scene".to_string(),
|
||||
scene_name: "Response Path Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/response-path-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_response_path_scene.js")).unwrap();
|
||||
|
||||
// The multi-mode template uses mode.responsePath for response extraction
|
||||
assert!(
|
||||
generated_script.contains("mode.responsePath"),
|
||||
"Generated JS should use 'mode.responsePath' for per-mode response extraction"
|
||||
);
|
||||
// The safeGet call should reference the mode's responsePath
|
||||
assert!(
|
||||
generated_script.contains("safeGet(raw, mode.responsePath"),
|
||||
"Generated JS should call safeGet with mode.responsePath"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 5: processData flag in $.ajax call with correct conditional
|
||||
#[test]
|
||||
fn test_process_data_flag_in_ajax() {
|
||||
let output_root = temp_workspace("sgclaw-process-data-test");
|
||||
let modes = vec![make_test_mode(
|
||||
"month",
|
||||
"http://example.com/api/month",
|
||||
Some("application/x-www-form-urlencoded"),
|
||||
"data",
|
||||
)];
|
||||
let scene_ir = make_test_scene_ir(modes);
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "process-data-scene".to_string(),
|
||||
scene_name: "Process Data Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/process-data-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_process_data_scene.js")).unwrap();
|
||||
|
||||
// The $.ajax call should contain processData flag
|
||||
assert!(
|
||||
generated_script.contains("processData:"),
|
||||
"Generated JS $.ajax call should contain 'processData:' flag"
|
||||
);
|
||||
// processData should be false for form-urlencoded (negated condition)
|
||||
assert!(
|
||||
generated_script.contains(
|
||||
"processData: request.headers['Content-Type'] !== 'application/x-www-form-urlencoded'"
|
||||
),
|
||||
"Generated JS should set processData to false for form-urlencoded content type"
|
||||
);
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest};
|
||||
use sgclaw::generated_scene::ir::SceneIr;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct P1FamilyManifest {
|
||||
families: Vec<P1FamilySpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct P1FamilySpec {
|
||||
id: String,
|
||||
group: String,
|
||||
#[serde(rename = "familyName")]
|
||||
family_name: String,
|
||||
#[serde(rename = "representativeFixtureDir")]
|
||||
representative_fixture_dir: String,
|
||||
#[serde(rename = "representativeSceneId")]
|
||||
representative_scene_id: String,
|
||||
#[serde(rename = "representativeSceneName")]
|
||||
representative_scene_name: String,
|
||||
#[serde(rename = "expectedArchetype")]
|
||||
expected_archetype: String,
|
||||
#[serde(rename = "requiredGateNames")]
|
||||
required_gate_names: Vec<String>,
|
||||
#[serde(rename = "requiredEvidenceTypes")]
|
||||
required_evidence_types: Vec<String>,
|
||||
#[serde(rename = "expansionFixtureDir", default)]
|
||||
expansion_fixture_dir: Option<String>,
|
||||
#[serde(rename = "expansionSceneId", default)]
|
||||
expansion_scene_id: Option<String>,
|
||||
#[serde(rename = "expansionSceneName", default)]
|
||||
expansion_scene_name: Option<String>,
|
||||
#[serde(rename = "expansionAssertions", default)]
|
||||
expansion_assertions: Option<ExpansionAssertions>,
|
||||
#[serde(rename = "batchCandidateAsset", default)]
|
||||
batch_candidate_asset: Option<String>,
|
||||
#[serde(rename = "batchExpansionFixtures", default)]
|
||||
batch_expansion_fixtures: Vec<BatchExpansionFixture>,
|
||||
#[serde(rename = "successRateSummary")]
|
||||
success_rate_summary: String,
|
||||
#[serde(rename = "failureTaxonomy")]
|
||||
failure_taxonomy: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct ExpansionAssertions {
|
||||
#[serde(rename = "requiredDefaultMode", default)]
|
||||
required_default_mode: Option<String>,
|
||||
#[serde(rename = "expectedPaginationField", default)]
|
||||
expected_pagination_field: Option<String>,
|
||||
#[serde(rename = "requiredJoinKey", default)]
|
||||
required_join_key: Option<String>,
|
||||
#[serde(rename = "requiredAggregateRule", default)]
|
||||
required_aggregate_rule: Option<String>,
|
||||
#[serde(rename = "requiredMainRequest", default)]
|
||||
required_main_request: Option<String>,
|
||||
#[serde(rename = "requiredEnrichmentRequest", default)]
|
||||
required_enrichment_request: Option<String>,
|
||||
#[serde(rename = "requiredMergeJoinKey", default)]
|
||||
required_merge_join_key: Option<String>,
|
||||
#[serde(rename = "requiredMergeAggregateRule", default)]
|
||||
required_merge_aggregate_rule: Option<String>,
|
||||
#[serde(rename = "requiredOutputColumn", default)]
|
||||
required_output_column: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct BatchExpansionFixture {
|
||||
#[serde(rename = "fixtureDir")]
|
||||
fixture_dir: String,
|
||||
#[serde(rename = "sceneId")]
|
||||
scene_id: String,
|
||||
#[serde(rename = "sceneName")]
|
||||
scene_name: String,
|
||||
assertions: ExpansionAssertions,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn p1_family_manifest_is_actionable() {
|
||||
let manifest = load_manifest();
|
||||
assert_eq!(manifest.families.len(), 7);
|
||||
|
||||
for family in manifest.families {
|
||||
assert!(matches!(
|
||||
family.group.as_str(),
|
||||
"G1" | "G2" | "G3" | "G6" | "G7" | "G8"
|
||||
));
|
||||
assert!(!family.family_name.trim().is_empty());
|
||||
assert!(Path::new(&family.representative_fixture_dir).exists());
|
||||
assert!(!family.expected_archetype.trim().is_empty());
|
||||
assert!(!family.required_gate_names.is_empty());
|
||||
assert!(!family.required_evidence_types.is_empty());
|
||||
assert!(!family.success_rate_summary.trim().is_empty());
|
||||
assert!(!family.failure_taxonomy.is_empty());
|
||||
if let Some(expansion_fixture_dir) = &family.expansion_fixture_dir {
|
||||
assert!(Path::new(expansion_fixture_dir).exists());
|
||||
assert!(!family
|
||||
.expansion_scene_id
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.is_empty());
|
||||
assert!(!family
|
||||
.expansion_scene_name
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.is_empty());
|
||||
}
|
||||
if let Some(batch_candidate_asset) = &family.batch_candidate_asset {
|
||||
assert!(Path::new(batch_candidate_asset).exists());
|
||||
}
|
||||
for fixture in &family.batch_expansion_fixtures {
|
||||
assert!(Path::new(&fixture.fixture_dir).exists());
|
||||
assert!(!fixture.scene_id.is_empty());
|
||||
assert!(!fixture.scene_name.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn representative_p1_family_migrations_are_reusable() {
|
||||
let manifest = load_manifest();
|
||||
|
||||
for family in manifest.families {
|
||||
let output_root = temp_workspace(&format!("sgclaw-p1-family-{}", family.id));
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from(&family.representative_fixture_dir),
|
||||
scene_id: family.representative_scene_id.clone(),
|
||||
scene_name: family.representative_scene_name.clone(),
|
||||
scene_kind: None,
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: None,
|
||||
})
|
||||
.unwrap_or_else(|err| panic!("{} failed representative migration: {}", family.id, err));
|
||||
|
||||
let generated_dir = output_root
|
||||
.join("skills")
|
||||
.join(&family.representative_scene_id);
|
||||
let generated_report: SceneIr = serde_json::from_str(
|
||||
&fs::read_to_string(generated_dir.join("references/generation-report.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
generated_report.workflow_archetype().as_str(),
|
||||
family.expected_archetype,
|
||||
"expected archetype mismatch for {}",
|
||||
family.id
|
||||
);
|
||||
|
||||
for gate_name in &family.required_gate_names {
|
||||
assert!(
|
||||
generated_report
|
||||
.readiness
|
||||
.gates
|
||||
.iter()
|
||||
.any(|gate| gate.name == *gate_name),
|
||||
"missing gate {} for {}",
|
||||
gate_name,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
|
||||
for evidence_type in &family.required_evidence_types {
|
||||
assert!(
|
||||
generated_report
|
||||
.evidence
|
||||
.iter()
|
||||
.any(|item| item.evidence_type == *evidence_type),
|
||||
"missing evidence type {} for {}",
|
||||
evidence_type,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
generated_report.readiness.level == "A" || generated_report.readiness.level == "B",
|
||||
"representative migration should be reusable for {}",
|
||||
family.id
|
||||
);
|
||||
|
||||
if let (Some(expansion_fixture_dir), Some(expansion_scene_id), Some(expansion_scene_name)) = (
|
||||
&family.expansion_fixture_dir,
|
||||
&family.expansion_scene_id,
|
||||
&family.expansion_scene_name,
|
||||
) {
|
||||
let expansion_output_root =
|
||||
temp_workspace(&format!("sgclaw-p1-family-expansion-{}", family.id));
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from(expansion_fixture_dir),
|
||||
scene_id: expansion_scene_id.clone(),
|
||||
scene_name: expansion_scene_name.clone(),
|
||||
scene_kind: None,
|
||||
target_url: None,
|
||||
output_root: expansion_output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: None,
|
||||
})
|
||||
.unwrap_or_else(|err| panic!("{} failed expansion migration: {}", family.id, err));
|
||||
|
||||
let expansion_dir = expansion_output_root
|
||||
.join("skills")
|
||||
.join(expansion_scene_id);
|
||||
let expansion_report: SceneIr = serde_json::from_str(
|
||||
&fs::read_to_string(expansion_dir.join("references/generation-report.json"))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expansion_report.workflow_archetype().as_str(),
|
||||
family.expected_archetype,
|
||||
"expected expansion archetype mismatch for {}",
|
||||
family.id
|
||||
);
|
||||
assert!(
|
||||
expansion_report.readiness.level == "A" || expansion_report.readiness.level == "B",
|
||||
"expansion migration should be reusable for {}",
|
||||
family.id
|
||||
);
|
||||
|
||||
if let Some(assertions) = &family.expansion_assertions {
|
||||
if let Some(required_default_mode) = &assertions.required_default_mode {
|
||||
assert_eq!(
|
||||
expansion_report.default_mode.as_deref(),
|
||||
Some(required_default_mode.as_str()),
|
||||
"missing expansion default mode {} for {}",
|
||||
required_default_mode,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(expected_pagination_field) = &assertions.expected_pagination_field {
|
||||
assert_eq!(
|
||||
expansion_report
|
||||
.pagination_plan
|
||||
.as_ref()
|
||||
.map(|plan| plan.page_field.as_str()),
|
||||
Some(expected_pagination_field.as_str()),
|
||||
"expansion pagination field mismatch for {}",
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(required_join_key) = &assertions.required_join_key {
|
||||
assert!(
|
||||
expansion_report
|
||||
.join_keys
|
||||
.iter()
|
||||
.any(|key| key == required_join_key),
|
||||
"missing expansion join key {} for {}",
|
||||
required_join_key,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(required_aggregate_rule) = &assertions.required_aggregate_rule {
|
||||
assert!(
|
||||
expansion_report
|
||||
.merge_or_dedupe_rules
|
||||
.iter()
|
||||
.any(|rule| rule == required_aggregate_rule),
|
||||
"missing expansion aggregate rule {} for {}",
|
||||
required_aggregate_rule,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(required_main_request) = &assertions.required_main_request {
|
||||
assert!(
|
||||
expansion_report
|
||||
.main_request
|
||||
.as_ref()
|
||||
.and_then(|request| request.api_endpoint.as_ref())
|
||||
.map(|endpoint| endpoint.name.contains(required_main_request))
|
||||
.unwrap_or(false),
|
||||
"missing expansion main request {} for {}",
|
||||
required_main_request,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(required_enrichment_request) = &assertions.required_enrichment_request {
|
||||
assert!(
|
||||
expansion_report
|
||||
.enrichment_requests
|
||||
.iter()
|
||||
.any(|request| request.name.contains(required_enrichment_request)),
|
||||
"missing expansion enrichment request {} for {}",
|
||||
required_enrichment_request,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(required_merge_join_key) = &assertions.required_merge_join_key {
|
||||
assert!(
|
||||
expansion_report
|
||||
.merge_plan
|
||||
.as_ref()
|
||||
.map(|plan| {
|
||||
plan.join_keys
|
||||
.iter()
|
||||
.any(|key| key == required_merge_join_key)
|
||||
})
|
||||
.unwrap_or(false),
|
||||
"missing expansion merge join key {} for {}",
|
||||
required_merge_join_key,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(required_merge_aggregate_rule) =
|
||||
&assertions.required_merge_aggregate_rule
|
||||
{
|
||||
assert!(
|
||||
expansion_report
|
||||
.merge_plan
|
||||
.as_ref()
|
||||
.map(|plan| {
|
||||
plan.aggregate_rules
|
||||
.iter()
|
||||
.any(|rule| rule == required_merge_aggregate_rule)
|
||||
})
|
||||
.unwrap_or(false),
|
||||
"missing expansion merge aggregate rule {} for {}",
|
||||
required_merge_aggregate_rule,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(required_output_column) = &assertions.required_output_column {
|
||||
assert!(
|
||||
expansion_report
|
||||
.merge_plan
|
||||
.as_ref()
|
||||
.map(|plan| {
|
||||
plan.output_columns
|
||||
.iter()
|
||||
.any(|(field, _)| field == required_output_column)
|
||||
})
|
||||
.unwrap_or(false),
|
||||
"missing expansion output column {} for {}",
|
||||
required_output_column,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for batch_fixture in &family.batch_expansion_fixtures {
|
||||
let batch_output_root = temp_workspace(&format!(
|
||||
"sgclaw-p1-family-batch-{}-{}",
|
||||
family.id, batch_fixture.scene_id
|
||||
));
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from(&batch_fixture.fixture_dir),
|
||||
scene_id: batch_fixture.scene_id.clone(),
|
||||
scene_name: batch_fixture.scene_name.clone(),
|
||||
scene_kind: None,
|
||||
target_url: None,
|
||||
output_root: batch_output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: None,
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("{} failed batch expansion migration: {}", family.id, err)
|
||||
});
|
||||
|
||||
let batch_dir = batch_output_root
|
||||
.join("skills")
|
||||
.join(&batch_fixture.scene_id);
|
||||
let batch_report: SceneIr = serde_json::from_str(
|
||||
&fs::read_to_string(batch_dir.join("references/generation-report.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
batch_report.workflow_archetype().as_str(),
|
||||
family.expected_archetype,
|
||||
"expected batch expansion archetype mismatch for {}",
|
||||
family.id
|
||||
);
|
||||
assert!(
|
||||
batch_report.readiness.level == "A" || batch_report.readiness.level == "B",
|
||||
"batch expansion migration should be reusable for {}",
|
||||
family.id
|
||||
);
|
||||
|
||||
if let Some(required_default_mode) = &batch_fixture.assertions.required_default_mode {
|
||||
assert_eq!(
|
||||
batch_report.default_mode.as_deref(),
|
||||
Some(required_default_mode.as_str()),
|
||||
"missing batch expansion default mode {} for {}",
|
||||
required_default_mode,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(expected_pagination_field) =
|
||||
&batch_fixture.assertions.expected_pagination_field
|
||||
{
|
||||
assert_eq!(
|
||||
batch_report
|
||||
.pagination_plan
|
||||
.as_ref()
|
||||
.map(|plan| plan.page_field.as_str()),
|
||||
Some(expected_pagination_field.as_str()),
|
||||
"batch expansion pagination field mismatch for {}",
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(required_join_key) = &batch_fixture.assertions.required_join_key {
|
||||
assert!(
|
||||
batch_report
|
||||
.join_keys
|
||||
.iter()
|
||||
.any(|key| key == required_join_key),
|
||||
"missing batch expansion join key {} for {}",
|
||||
required_join_key,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
if let Some(required_aggregate_rule) = &batch_fixture.assertions.required_aggregate_rule
|
||||
{
|
||||
assert!(
|
||||
batch_report
|
||||
.merge_or_dedupe_rules
|
||||
.iter()
|
||||
.any(|rule| rule == required_aggregate_rule),
|
||||
"missing batch expansion aggregate rule {} for {}",
|
||||
required_aggregate_rule,
|
||||
family.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_manifest() -> P1FamilyManifest {
|
||||
serde_json::from_str(
|
||||
&fs::read_to_string("tests/fixtures/generated_scene/p1_family_manifest.json").unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn temp_workspace(prefix: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("{prefix}-{nanos}"));
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
path
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use reqwest::blocking::Client;
|
||||
use 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"],
|
||||
"可用电费小于零监测提醒"
|
||||
|
||||
@@ -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")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user