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();