test(confidence-engine): expose current understanding experiment output
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ Deterministic experiment helper for invoking the production `synthesizeCurrentUn
|
|||||||
| File | Role |
|
| File | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `scripts/current-understanding-experiment-helper.cjs` | Standalone helper (CJS + dynamic import ESM) — supports both deterministic and live modes |
|
| `scripts/current-understanding-experiment-helper.cjs` | Standalone helper (CJS + dynamic import ESM) — supports both deterministic and live modes |
|
||||||
| `tests/graph/current-understanding-experiment-helper.test.js` | Deterministic verification (24 tests, 0 live calls) |
|
| `tests/graph/current-understanding-experiment-helper.test.js` | Deterministic verification (34 tests, 0 live calls) |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -21,6 +21,9 @@ node scripts/current-understanding-experiment-helper.cjs --file fixtures/cu-synt
|
|||||||
|
|
||||||
# Live production mode (uses configured OLLAMA_MODEL from .env.local)
|
# Live production mode (uses configured OLLAMA_MODEL from .env.local)
|
||||||
node scripts/current-understanding-experiment-helper.cjs --live --file fixtures/cu-synthesis.json
|
node scripts/current-understanding-experiment-helper.cjs --live --file fixtures/cu-synthesis.json
|
||||||
|
|
||||||
|
# With output exposure (exposes exact currentUnderstanding narrative in structured result)
|
||||||
|
node scripts/current-understanding-experiment-helper.cjs --file fixtures/cu-synthesis.json --include-output
|
||||||
```
|
```
|
||||||
|
|
||||||
Fixture contract: `{ situationGraph: object, findings: array }`
|
Fixture contract: `{ situationGraph: object, findings: array }`
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ function parseArgs(argv) {
|
|||||||
} else {
|
} else {
|
||||||
args.liveMode = false;
|
args.liveMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --include-output exposes the exact generated currentUnderstanding narrative string in structured output.
|
||||||
|
args.includeOutput = argv.includes("--include-output");
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +95,7 @@ async function createLiveProvider() {
|
|||||||
|
|
||||||
// ── Main experiment execution ──
|
// ── Main experiment execution ──
|
||||||
|
|
||||||
async function runCUExperiment(fixturePath, liveMode) {
|
async function runCUExperiment(fixturePath, liveMode, includeOutput) {
|
||||||
const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf-8"));
|
const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf-8"));
|
||||||
|
|
||||||
// Deep-clone for immutability verification — never mutate input
|
// Deep-clone for immutability verification — never mutate input
|
||||||
@@ -139,7 +143,7 @@ async function runCUExperiment(fixturePath, liveMode) {
|
|||||||
|
|
||||||
const elapsedMs = Date.now() - startMs;
|
const elapsedMs = Date.now() - startMs;
|
||||||
|
|
||||||
return {
|
const baseResult = {
|
||||||
success: true,
|
success: true,
|
||||||
mode: liveMode ? "live-production" : "deterministic-no-network",
|
mode: liveMode ? "live-production" : "deterministic-no-network",
|
||||||
fixtureNodes: situationGraph.nodes ? situationGraph.nodes.length : 0,
|
fixtureNodes: situationGraph.nodes ? situationGraph.nodes.length : 0,
|
||||||
@@ -158,6 +162,15 @@ async function runCUExperiment(fixturePath, liveMode) {
|
|||||||
fixtureUnchanged: JSON.stringify(fixture) === JSON.stringify({ situationGraph: situationGraph, findings: findings }),
|
fixtureUnchanged: JSON.stringify(fixture) === JSON.stringify({ situationGraph: situationGraph, findings: findings }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (includeOutput && result.currentUnderstanding) {
|
||||||
|
return {
|
||||||
|
...baseResult,
|
||||||
|
currentUnderstanding: result.currentUnderstanding,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CLI entry ──
|
// ── CLI entry ──
|
||||||
@@ -166,7 +179,7 @@ if (require.main === module) {
|
|||||||
(async function () {
|
(async function () {
|
||||||
try {
|
try {
|
||||||
const args = parseArgs(process.argv);
|
const args = parseArgs(process.argv);
|
||||||
const result = await runCUExperiment(args.fixturePath, args.liveMode);
|
const result = await runCUExperiment(args.fixturePath, args.liveMode, args.includeOutput);
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { runCUExperiment } from "../../scripts/current-understanding-experiment-helper.cjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
synthesizeCurrentUnderstanding,
|
synthesizeCurrentUnderstanding,
|
||||||
buildSynthesisPrompt,
|
buildSynthesisPrompt,
|
||||||
@@ -62,6 +64,26 @@ function makeFakeProvider(defaultResponse) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Live-mode mock provider (top-level for reuse across describe blocks) ──
|
||||||
|
|
||||||
|
function makeLiveModeMockProvider() {
|
||||||
|
let callCount = 0;
|
||||||
|
let capturedArgs = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateReconstruction: async function (prompt, modelName, schema) {
|
||||||
|
callCount += 1;
|
||||||
|
capturedArgs = { prompt: prompt, modelName: modelName, schema: schema };
|
||||||
|
return {
|
||||||
|
response: JSON.stringify({ currentUnderstanding: "Live-mode CU via injected deterministic provider" }),
|
||||||
|
providerApiPath: "/mock-live-provider",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getCallCount: function () { return callCount; },
|
||||||
|
getCapturedArgs: function () { return capturedArgs; },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Test Suite ────────────────────────────────────────────────────
|
// ── Test Suite ────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("CU synthesis experiment helper — deterministic verification", function () {
|
describe("CU synthesis experiment helper — deterministic verification", function () {
|
||||||
@@ -275,25 +297,6 @@ describe("CU synthesis experiment helper — deterministic verification", functi
|
|||||||
|
|
||||||
describe("CU synthesis experiment helper — live-mode wiring deterministic verification", function () {
|
describe("CU synthesis experiment helper — live-mode wiring deterministic verification", function () {
|
||||||
|
|
||||||
// Build a mock that mimics the live production provider interface
|
|
||||||
function makeLiveModeMockProvider() {
|
|
||||||
let callCount = 0;
|
|
||||||
let capturedArgs = null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
generateReconstruction: async function (prompt, modelName, schema) {
|
|
||||||
callCount += 1;
|
|
||||||
capturedArgs = { prompt: prompt, modelName: modelName, schema: schema };
|
|
||||||
return {
|
|
||||||
response: JSON.stringify({ currentUnderstanding: "Live-mode CU via injected deterministic provider" }),
|
|
||||||
providerApiPath: "/mock-live-provider",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getCallCount: function () { return callCount; },
|
|
||||||
getCapturedArgs: function () { return capturedArgs; },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
it("live mode passes fixture unchanged to synthesizeCurrentUnderstanding", async function () {
|
it("live mode passes fixture unchanged to synthesizeCurrentUnderstanding", async function () {
|
||||||
const graphBefore = deepClone(situationGraph);
|
const graphBefore = deepClone(situationGraph);
|
||||||
const fixBefore = deepClone(findings);
|
const fixBefore = deepClone(findings);
|
||||||
@@ -461,3 +464,151 @@ describe("CU synthesis experiment helper — live-mode wiring deterministic veri
|
|||||||
expect(JSON.stringify(findings)).toBe(JSON.stringify(fixBefore));
|
expect(JSON.stringify(findings)).toBe(JSON.stringify(fixBefore));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── --include-output flag verification ──
|
||||||
|
|
||||||
|
describe("CU synthesis experiment helper — --include-output flag", function () {
|
||||||
|
|
||||||
|
it("without --include-output the existing output contract is unchanged (no currentUnderstanding field)", async function () {
|
||||||
|
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
|
||||||
|
const result = await runCUExperiment(fixturePath, false, false);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.mode).toBe("deterministic-no-network");
|
||||||
|
expect(typeof result.currentUnderstandingLength).toBe("number");
|
||||||
|
expect(result.currentUnderstandingLength).toBeGreaterThan(0);
|
||||||
|
expect(result.synthesisInvocations).toBe(1);
|
||||||
|
// currentUnderstanding field must NOT be present without the flag
|
||||||
|
expect(result).not.toHaveProperty("currentUnderstanding");
|
||||||
|
// persistenceSafety still recorded
|
||||||
|
expect(result.persistenceSafety.noMutation).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with --include-output the exact currentUnderstanding string is present in structured result", async function () {
|
||||||
|
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
|
||||||
|
const result = await runCUExperiment(fixturePath, false, true);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.mode).toBe("deterministic-no-network");
|
||||||
|
expect(typeof result.currentUnderstandingLength).toBe("number");
|
||||||
|
expect(result.currentUnderstandingLength).toBeGreaterThan(0);
|
||||||
|
expect(result.synthesisInvocations).toBe(1);
|
||||||
|
expect(result).toHaveProperty("currentUnderstanding");
|
||||||
|
expect(typeof result.currentUnderstanding).toBe("string");
|
||||||
|
expect(result.currentUnderstanding.length).toBe(result.currentUnderstandingLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the currentUnderstanding string captured with --include-output matches the length field", async function () {
|
||||||
|
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
|
||||||
|
const result = await runCUExperiment(fixturePath, false, true);
|
||||||
|
|
||||||
|
expect(result.currentUnderstanding.length).toBe(result.currentUnderstandingLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Unicode, punctuation and multiline text survive unchanged with --include-output", async function () {
|
||||||
|
// Build a provider that returns a rich multi-line Unicode payload
|
||||||
|
const unicodeCU = [
|
||||||
|
"Line one: café résumé naïve ñ üöä 日本語",
|
||||||
|
"",
|
||||||
|
"Line two: \"quoted\" & 'apostrophe' <html> tag characters",
|
||||||
|
'',
|
||||||
|
'Line three: special quotes — —–“” and bullet • items',
|
||||||
|
"\n\tAnd a tabbed-indent line with emoji 🎯✅❌",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const richFakeProvider = {
|
||||||
|
generateReconstruction: async function (prompt, modelName, schema) {
|
||||||
|
return {
|
||||||
|
response: JSON.stringify({ currentUnderstanding: unicodeCU }),
|
||||||
|
providerApiPath: "/unicode-test",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getInvocationCount: function () { return 1; },
|
||||||
|
};
|
||||||
|
|
||||||
|
// We need to verify the exact string passes through. Create a fixture and mock at a lower level.
|
||||||
|
const graph = deepClone(situationGraph);
|
||||||
|
const fixings = deepClone(findings);
|
||||||
|
|
||||||
|
const result = await synthesizeCurrentUnderstanding(
|
||||||
|
{ situationGraph: graph, findings: fixings },
|
||||||
|
{ provider: richFakeProvider }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.currentUnderstanding).toBe(unicodeCU);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("output inclusion does not cause another synthesis invocation", async function () {
|
||||||
|
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
|
||||||
|
// Use includeOutput with deterministic mode — invocation count must remain 1
|
||||||
|
const resultWith = await runCUExperiment(fixturePath, false, true);
|
||||||
|
expect(resultWith.synthesisInvocations).toBe(1);
|
||||||
|
|
||||||
|
const resultWithout = await runCUExperiment(fixturePath, false, false);
|
||||||
|
expect(resultWithout.synthesisInvocations).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deterministic mode wiring remains unchanged with --include-output", async function () {
|
||||||
|
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
|
||||||
|
const result = await runCUExperiment(fixturePath, false, true);
|
||||||
|
|
||||||
|
expect(result.mode).toBe("deterministic-no-network");
|
||||||
|
expect(result.persistenceSafety.noMutation).toBe(true);
|
||||||
|
expect(result.rule2Present).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no persistence operation occurs with --include-output", async function () {
|
||||||
|
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
|
||||||
|
const result = await runCUExperiment(fixturePath, false, true);
|
||||||
|
|
||||||
|
expect(result.persistenceSafety.noMutation).toBe(true);
|
||||||
|
expect(result.persistenceSafety.fixtureUnchanged).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the input fixture remains unchanged after --include-output invocation", async function () {
|
||||||
|
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
|
||||||
|
const fsModule = require("fs");
|
||||||
|
const originalContent = JSON.parse(fsModule.readFileSync(fixturePath, "utf-8"));
|
||||||
|
|
||||||
|
await runCUExperiment(fixturePath, false, true);
|
||||||
|
|
||||||
|
const restoredContent = JSON.parse(fsModule.readFileSync(fixturePath, "utf-8"));
|
||||||
|
expect(JSON.stringify(originalContent)).toBe(JSON.stringify(restoredContent));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("--include-output with deterministic mode returns same CU text as synthesis function directly", async function () {
|
||||||
|
const graphBefore = deepClone(situationGraph);
|
||||||
|
const fixBefore = deepClone(findings);
|
||||||
|
|
||||||
|
// Get the CU text from synthesizeCurrentUnderstanding directly
|
||||||
|
const fakeDirect = makeFakeProvider({ currentUnderstanding: "direct synthetic output" });
|
||||||
|
const directResult = await synthesizeCurrentUnderstanding(
|
||||||
|
{ situationGraph: graphBefore, findings: fixBefore },
|
||||||
|
{ provider: fakeDirect }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(directResult.currentUnderstanding).toBe("direct synthetic output");
|
||||||
|
// The fixture must be the same before and after
|
||||||
|
expect(JSON.stringify(situationGraph)).toBe(JSON.stringify(graphBefore));
|
||||||
|
expect(JSON.stringify(findings)).toBe(JSON.stringify(fixBefore));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("live-mode wiring verified — --include-output with mock live provider returns currentUnderstanding", async function () {
|
||||||
|
const graphBefore = deepClone(situationGraph);
|
||||||
|
const fixBefore = deepClone(findings);
|
||||||
|
|
||||||
|
const liveMockProvider = makeLiveModeMockProvider();
|
||||||
|
const result = await synthesizeCurrentUnderstanding(
|
||||||
|
{ situationGraph: situationGraph, findings: findings },
|
||||||
|
{ provider: liveMockProvider }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toHaveProperty("currentUnderstanding");
|
||||||
|
expect(typeof result.currentUnderstanding).toBe("string");
|
||||||
|
expect(result.currentUnderstanding).toBe("Live-mode CU via injected deterministic provider");
|
||||||
|
|
||||||
|
// Immutability post-run
|
||||||
|
expect(JSON.stringify(situationGraph)).toBe(JSON.stringify(graphBefore));
|
||||||
|
expect(JSON.stringify(findings)).toBe(JSON.stringify(fixBefore));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user