Carry explicit local date intervals instead of inferring the day from clock order. Cluster width, delivery, adoption, and reports keep the actual civil date; adopted date is stored separately from the reported birth_date. Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation thresholds, and Skill version are unchanged. Isolated Linux final-3 gates passed; four pre-existing Python failures remain. This is not a production release.
504 lines
33 KiB
TypeScript
504 lines
33 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
cachedEngineScoreIsReusable,
|
|
liveEngineScoringIdentityFromEnv,
|
|
readV9EngineScoringIdentity,
|
|
v9EngineVersion,
|
|
} from "../src/lib/rectification-agentic/v9/engine-client.ts";
|
|
import { openRectificationCase } from "../src/lib/rectification-agentic/v9/case-service.ts";
|
|
import { loadV9TurnReceipt, parseV9CaseDossier, evidenceLedgerFingerprint, candidateRangeFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
|
import { scoreAndPersistCurrentEvidence } from "../src/lib/rectification-agentic/v9/score-persist.ts";
|
|
import { acceptV9Candidate, confirmV9BirthTime, advanceV9CaseFromBlockScan } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
|
import { assertV9ResultWritable, resultIdentityView } from "../src/lib/rectification-agentic/v9/result-identity.ts";
|
|
import { ensureNonTerminalTurnExit, persistNextInterviewIfIdle } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
|
|
import { dossierResponseWithIdentity } from "../src/lib/rectification-agentic/v9/case-dossier-response.ts";
|
|
import { recompareHistoricalResult } from "../src/lib/rectification-surface-state.ts";
|
|
import { resolveSkillPackageVersion } from "../src/lib/skill-package-registry.ts";
|
|
import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
|
|
import {
|
|
CASE_ID, SESSION_ID, TURN_ID, USER_ID, FOCUS_ID,
|
|
computeFixture, dossierFixture, fakeAccounting, receiptHandlers,
|
|
} from "./rectification-v9-test-support.ts";
|
|
|
|
// Real native-engine response, using explicitly fictional input, not a hand-built contract.
|
|
const golden = JSON.parse(readFileSync(new URL(
|
|
"./fixtures/rectification-engine-version-cross-midnight-golden.json", import.meta.url,
|
|
), "utf8"));
|
|
// Historical golden identity stays fixed; it must not be relabeled as the live engine.
|
|
const CURRENT = "rectification-v5-matrix-scoring-8";
|
|
const PREVIOUS = "rectification-v5-matrix-scoring-7";
|
|
const LIVE_CURRENT = "rectification-v5-matrix-scoring-9";
|
|
const liveGolden = JSON.parse(readFileSync(new URL(
|
|
"./fixtures/rectification-midnight-date-anchor.delivery.native.json", import.meta.url,
|
|
), "utf8"));
|
|
const envKeys = [
|
|
"RECTIFICATION_ENGINE_VERSION", "RECTIFICATION_ALGORITHM_VERSION",
|
|
"RECTIFICATION_DECISION_POLICY_VERSION",
|
|
] as const;
|
|
|
|
function isolateIdentityEnv(t: test.TestContext) {
|
|
const saved = envKeys.map((key) => [key, process.env[key]] as const);
|
|
for (const key of envKeys) delete process.env[key];
|
|
t.after(() => {
|
|
for (const [key, value] of saved) {
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
test("receipt default matches the cross-midnight native scoring version without changing Skill identity", (t) => {
|
|
isolateIdentityEnv(t);
|
|
assert.equal(v9EngineVersion(), LIVE_CURRENT);
|
|
for (const blank of ["", " "]) {
|
|
process.env.RECTIFICATION_ENGINE_VERSION = blank;
|
|
assert.equal(v9EngineVersion(), LIVE_CURRENT);
|
|
}
|
|
assert.equal(golden.score.algorithm_version, CURRENT);
|
|
assert.equal(golden.versions.algorithm_version, CURRENT);
|
|
const python = readFileSync(new URL("../../scripts/rectification/scoring_service.py", import.meta.url), "utf8");
|
|
assert.match(python, /ALGORITHM_VERSION = "rectification-v5-matrix-scoring-9"/);
|
|
assert.equal(liveGolden.response.algorithm_version, LIVE_CURRENT);
|
|
const fingerprints = { evidenceLedgerFingerprint: "e".repeat(64), candidateRangeFingerprint: "c".repeat(64) };
|
|
const live = {
|
|
algorithmVersion: liveGolden.response.algorithm_version,
|
|
policyVersion: liveGolden.response.decision_policy_version,
|
|
};
|
|
const historical = { ...fingerprints, algorithmVersion: CURRENT, policyVersion: golden.versions.decision_policy_version };
|
|
assert.equal(cachedEngineScoreIsReusable(historical, fingerprints, live), false);
|
|
assert.equal(cachedEngineScoreIsReusable({ ...historical, algorithmVersion: LIVE_CURRENT }, fingerprints, live), true);
|
|
assert.equal(historical.algorithmVersion, CURRENT, "checking live identity never relabels history");
|
|
// A receipt default must not become an environment override for cache identity.
|
|
assert.deepEqual(liveEngineScoringIdentityFromEnv({}), { algorithmVersion: null, policyVersion: null });
|
|
});
|
|
|
|
test("explicit engine override still wins and remains a deployment prerequisite, not silently rewritten", (t) => {
|
|
isolateIdentityEnv(t);
|
|
process.env.RECTIFICATION_ENGINE_VERSION = " rectification-v5 ";
|
|
assert.equal(v9EngineVersion(), "rectification-v5");
|
|
assert.equal(liveEngineScoringIdentityFromEnv().algorithmVersion, null);
|
|
process.env.RECTIFICATION_ENGINE_VERSION = ` ${PREVIOUS} `;
|
|
assert.equal(v9EngineVersion(), PREVIOUS);
|
|
// This existing cache-identity fallback predates this change; no new open gate is introduced.
|
|
assert.equal(liveEngineScoringIdentityFromEnv().algorithmVersion, PREVIOUS);
|
|
});
|
|
|
|
test("native versions invalidate matching-input old scores while same-version scores remain reusable", async (t) => {
|
|
isolateIdentityEnv(t);
|
|
t.mock.method(globalThis, "fetch", async (url: unknown) => {
|
|
assert.ok(String(url).endsWith("/api/rectification/v5/versions"));
|
|
return Response.json(golden.versions);
|
|
});
|
|
const live = await readV9EngineScoringIdentity();
|
|
assert.equal(live.algorithmVersion, CURRENT);
|
|
const fingerprints = { evidenceLedgerFingerprint: "e".repeat(64), candidateRangeFingerprint: "c".repeat(64) };
|
|
const previous = { ...fingerprints, algorithmVersion: PREVIOUS, policyVersion: golden.versions.decision_policy_version };
|
|
assert.equal(cachedEngineScoreIsReusable(previous, fingerprints, live), false);
|
|
assert.equal(cachedEngineScoreIsReusable({ ...previous, algorithmVersion: CURRENT }, fingerprints, live), true);
|
|
assert.equal(previous.algorithmVersion, PREVIOUS);
|
|
});
|
|
|
|
test("partial override is completed by native identity; complete override needs no request", async (t) => {
|
|
isolateIdentityEnv(t);
|
|
let fetches = 0;
|
|
t.mock.method(globalThis, "fetch", async () => {
|
|
fetches += 1;
|
|
return Response.json(golden.versions);
|
|
});
|
|
const fingerprints = { evidenceLedgerFingerprint: "e".repeat(64), candidateRangeFingerprint: "c".repeat(64) };
|
|
const stored = { ...fingerprints, algorithmVersion: PREVIOUS, policyVersion: golden.versions.decision_policy_version };
|
|
process.env.RECTIFICATION_DECISION_POLICY_VERSION = golden.versions.decision_policy_version;
|
|
const policyOnly = await readV9EngineScoringIdentity();
|
|
// 原值 null/true;新值 CURRENT/false;原因:部分覆盖不能绕过实际算法身份。
|
|
assert.equal(policyOnly.algorithmVersion, CURRENT);
|
|
assert.equal(cachedEngineScoreIsReusable(stored, fingerprints, policyOnly), false);
|
|
process.env.RECTIFICATION_ALGORITHM_VERSION = PREVIOUS;
|
|
const staleOverride = await readV9EngineScoringIdentity();
|
|
assert.equal(staleOverride.algorithmVersion, PREVIOUS);
|
|
assert.equal(cachedEngineScoreIsReusable(stored, fingerprints, staleOverride), true);
|
|
assert.equal(fetches, 1); // 原值 0;部分覆盖必须补齐,完整覆盖仍不请求。
|
|
});
|
|
|
|
test("unreachable versions never validate a cached result as current", async (t) => {
|
|
isolateIdentityEnv(t);
|
|
t.mock.method(globalThis, "fetch", async () => { throw new Error("fixture versions unavailable"); });
|
|
const live = await readV9EngineScoringIdentity();
|
|
assert.deepEqual(live, { algorithmVersion: null, policyVersion: null });
|
|
const fingerprints = { evidenceLedgerFingerprint: "e".repeat(64), candidateRangeFingerprint: "c".repeat(64) };
|
|
// 原值 true;新值 false;原因:BUG-984 策略 b,只读不是当前缓存命中。
|
|
assert.equal(cachedEngineScoreIsReusable({ ...fingerprints, algorithmVersion: PREVIOUS }, fingerprints, live), false);
|
|
});
|
|
|
|
for (const storedVersion of [PREVIOUS, CURRENT]) {
|
|
test(`native late-night block cache checks actual identity: ${storedVersion}`, async (t) => {
|
|
isolateIdentityEnv(t);
|
|
const block = JSON.parse(readFileSync(new URL("./fixtures/rectification-engine-version-cross-midnight-block-golden.json", import.meta.url), "utf8"));
|
|
const range = { start_time: block.request.start_time, end_time: block.request.end_time };
|
|
const raw = dossierFixture({ stage: "block_scan", candidateRange: range, latestResult: null });
|
|
const fingerprint = evidenceLedgerFingerprint(parseV9CaseDossier(raw)!.evidence);
|
|
raw.case.block_scan = {
|
|
...block.response, algorithm_version: storedVersion,
|
|
decision_policy_version: block.response.decision_receipt.decision_policy_version,
|
|
evidence_ledger_fingerprint: fingerprint,
|
|
candidate_range_fingerprint: candidateRangeFingerprint(range, "a".repeat(64)),
|
|
};
|
|
const accounting = fakeAccounting({
|
|
get_agentic_rectification_case_dossier: () => raw,
|
|
get_agentic_rectification_case_compute: () => ({ ...computeFixture(), candidate_range: range }),
|
|
write_agentic_rectification_block_scan: (_fn, args) => { raw.case.block_scan = args.p_block_scan as Record<string, unknown>; return {}; },
|
|
});
|
|
const requests: string[] = [];
|
|
t.mock.method(globalThis, "fetch", async (url: unknown) => {
|
|
requests.push(String(url));
|
|
return Response.json(String(url).endsWith("/versions") ? golden.versions : block.response);
|
|
});
|
|
const opened = await dossierResponseWithIdentity(parseV9CaseDossier(raw)!, [], { status: "verified", requiresSkillAdoption: false } as never);
|
|
assert.equal(opened.case.result_identity.can_recompare, storedVersion === PREVIOUS);
|
|
assert.equal(opened.case.result_identity.read_only, storedVersion === PREVIOUS);
|
|
const result = await scoreAndPersistCurrentEvidence({ accounting: accounting.client, userId: USER_ID, caseId: CASE_ID });
|
|
assert.equal(result.persisted.algorithmVersion, CURRENT);
|
|
assert.equal(result.persisted.cached, storedVersion === CURRENT);
|
|
assert.equal(requests.filter((url) => url.endsWith("/block_scan")).length, storedVersion === CURRENT ? 0 : 1);
|
|
assert.ok(requests.some((url) => url.endsWith("/versions")));
|
|
});
|
|
}
|
|
|
|
for (const storedVersion of [PREVIOUS, CURRENT]) {
|
|
test(`native minute cache checks actual identity: ${storedVersion}`, async (t) => {
|
|
isolateIdentityEnv(t);
|
|
const range = { start_time: golden.request.start_time, end_time: golden.request.end_time };
|
|
const raw = dossierFixture({ candidateRange: range, latestResult: null });
|
|
raw.latest_result = {
|
|
...golden.score, ...golden.score.decision_receipt,
|
|
result_id: golden.score.result_id, candidates: golden.score.candidate_decisions,
|
|
algorithm_version: storedVersion, decision_receipt: golden.score.decision_receipt,
|
|
evidence_ledger_fingerprint: evidenceLedgerFingerprint(parseV9CaseDossier(raw)!.evidence),
|
|
candidate_range_fingerprint: candidateRangeFingerprint(range, "a".repeat(64)),
|
|
};
|
|
const accounting = fakeAccounting({
|
|
get_agentic_rectification_case_dossier: () => raw,
|
|
get_agentic_rectification_case_compute: () => ({ ...computeFixture(), candidate_range: range }),
|
|
persist_agentic_rectification_candidate_v2: (_fn, args) => ({
|
|
...golden.score.decision_receipt, result_id: args.p_engine_result_id,
|
|
candidates: args.p_candidates, algorithm_version: args.p_algorithm_version,
|
|
decision_policy_version: args.p_decision_policy_version, decision_receipt: args.p_decision_receipt,
|
|
}),
|
|
});
|
|
const requests: string[] = [];
|
|
t.mock.method(globalThis, "fetch", async (url: unknown) => {
|
|
requests.push(String(url));
|
|
assert.ok(String(url).endsWith("/versions") || String(url).endsWith("/score"));
|
|
return Response.json(String(url).endsWith("/versions") ? golden.versions : golden.score);
|
|
});
|
|
const opened = await dossierResponseWithIdentity(parseV9CaseDossier(raw)!, [], { status: "verified", requiresSkillAdoption: false } as never);
|
|
assert.equal(opened.case.result_identity.can_recompare, storedVersion === PREVIOUS);
|
|
assert.equal(opened.case.result_identity.read_only, storedVersion === PREVIOUS);
|
|
const result = await scoreAndPersistCurrentEvidence({ accounting: accounting.client, userId: USER_ID, caseId: CASE_ID, keepAnswers: true });
|
|
assert.equal(result.persisted.algorithmVersion, CURRENT);
|
|
assert.equal(result.persisted.cached, storedVersion === CURRENT);
|
|
assert.equal(requests.filter((url) => url.endsWith("/score")).length, storedVersion === CURRENT ? 0 : 1);
|
|
assert.ok(requests.some((url) => url.endsWith("/versions")));
|
|
});
|
|
}
|
|
|
|
test("stale history exposes explicit recompare and real tools replace old provenance", async (t) => {
|
|
isolateIdentityEnv(t);
|
|
const block = JSON.parse(readFileSync(new URL("./fixtures/rectification-engine-version-cross-midnight-block-golden.json", import.meta.url), "utf8"));
|
|
const range = { start_time: block.request.start_time, end_time: block.request.end_time };
|
|
const raw = dossierFixture({ stage: "block_scan", candidateRange: range, latestResult: null, blockScan: { ...block.response, algorithm_version: PREVIOUS } });
|
|
const accounting = fakeAccounting({
|
|
...receiptHandlers,
|
|
get_agentic_rectification_case_dossier: () => raw,
|
|
get_agentic_rectification_case_compute: () => ({ ...computeFixture(), candidate_range: range }),
|
|
write_agentic_rectification_block_scan: (_fn, args) => { raw.case.block_scan = args.p_block_scan as Record<string, unknown>; return {}; },
|
|
set_agentic_rectification_conversation_focus: (_fn, args) => ({ focus: { id: FOCUS_ID, case_id: CASE_ID, question_id: args.p_question_id, intent: args.p_intent, target_domain: args.p_target_domain, expected_answer_schema: args.p_expected_answer_schema, status: "active" }, idempotent: false }),
|
|
});
|
|
let scans = 0;
|
|
t.mock.method(globalThis, "fetch", async (url: unknown) => {
|
|
if (String(url).endsWith("/versions")) return Response.json(golden.versions);
|
|
assert.ok(String(url).endsWith("/block_scan"));
|
|
scans++;
|
|
return Response.json(block.response);
|
|
});
|
|
const tools = createRectificationV9Tools({ accounting: accounting.client as never, userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID });
|
|
const read = await (tools["rectification-read-case"] as unknown as { execute(input: unknown): Promise<Record<string, unknown>> }).execute({ caseId: CASE_ID });
|
|
assert.equal(read.can_recompare, true);
|
|
assert.equal((read.next_action as Record<string, unknown>).type, "compare_candidates");
|
|
assert.equal(scans, 0, "opening history is not forced recomputation");
|
|
await (tools["rectification-compare-candidates"] as unknown as { execute(input: unknown): Promise<unknown> }).execute({ caseId: CASE_ID });
|
|
assert.equal(scans, 1);
|
|
const refreshed = await dossierResponseWithIdentity(parseV9CaseDossier(raw)!, [], { status: "verified", requiresSkillAdoption: false } as never);
|
|
assert.equal(refreshed.case.result_identity.read_only, false);
|
|
assert.equal(refreshed.case.result_identity.algorithm_version, CURRENT);
|
|
assert.equal(refreshed.case.result_identity.policy_version, golden.versions.decision_policy_version);
|
|
const component = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
|
|
assert.match(component, /resultNotice && canRecompare && !historyReadonly/);
|
|
assert.match(component, /recompareHistoricalResult\(\{ canRecompare, historyReadonly, busy, send \}\)/);
|
|
assert.match(component, /if \(\(action === "message" && !trimmed\) \|\| conversationReadonly\) return/);
|
|
const sent: string[] = [];
|
|
const send = async (action: "message", text: string) => { sent.push(`${action}:${text}`); };
|
|
await recompareHistoricalResult({ canRecompare: true, historyReadonly: false, busy: false, send });
|
|
assert.deepEqual(sent, ["message:请按当前算法重新比较候选。"]);
|
|
for (const state of [{ canRecompare: false, historyReadonly: false, busy: false }, { canRecompare: true, historyReadonly: true, busy: false }, { canRecompare: true, historyReadonly: false, busy: true }]) {
|
|
await recompareHistoricalResult({ ...state, send });
|
|
}
|
|
assert.equal(sent.length, 1, "unknown identity, terminal history and busy clicks cannot dispatch");
|
|
});
|
|
|
|
test("partial conflicts and incomplete native pairs never authorize reuse", async (t) => {
|
|
isolateIdentityEnv(t);
|
|
process.env.RECTIFICATION_ALGORITHM_VERSION = PREVIOUS;
|
|
t.mock.method(globalThis, "fetch", async () => Response.json(golden.versions));
|
|
assert.deepEqual(await readV9EngineScoringIdentity(), { algorithmVersion: null, policyVersion: null });
|
|
delete process.env.RECTIFICATION_ALGORITHM_VERSION;
|
|
t.mock.method(globalThis, "fetch", async () => Response.json({ algorithm_version: CURRENT }));
|
|
const incomplete = await readV9EngineScoringIdentity();
|
|
assert.equal(incomplete.policyVersion, null);
|
|
assert.equal(cachedEngineScoreIsReusable({ algorithmVersion: CURRENT, evidenceLedgerFingerprint: "e", candidateRangeFingerprint: "r" }, { evidenceLedgerFingerprint: "e", candidateRangeFingerprint: "r" }, incomplete), false);
|
|
});
|
|
|
|
for (const stage of ["minute", "block_scan"] as const) {
|
|
for (const failure of ["http", "timeout", "partial"] as const) {
|
|
test(`${stage} ${failure}: historical result stays read-only without recompute; server mutations reject`, async (t) => {
|
|
isolateIdentityEnv(t);
|
|
if (failure === "partial") process.env.RECTIFICATION_DECISION_POLICY_VERSION = golden.versions.decision_policy_version;
|
|
const block = JSON.parse(readFileSync(new URL("./fixtures/rectification-engine-version-cross-midnight-block-golden.json", import.meta.url), "utf8"));
|
|
const range = { start_time: golden.request.start_time, end_time: golden.request.end_time };
|
|
const raw = dossierFixture({ stage, candidateRange: range, latestResult: {
|
|
...golden.score, ...golden.score.decision_receipt,
|
|
result_id: golden.score.result_id, candidates: golden.score.candidate_decisions,
|
|
algorithm_version: PREVIOUS, decision_receipt: golden.score.decision_receipt,
|
|
}, blockScan: { ...block.response, algorithm_version: PREVIOUS } });
|
|
const accounting = fakeAccounting({
|
|
...receiptHandlers,
|
|
get_agentic_rectification_case_dossier: () => raw,
|
|
get_agentic_rectification_case_compute: () => ({ ...computeFixture(), candidate_range: range }),
|
|
});
|
|
const requests: string[] = [];
|
|
t.mock.method(globalThis, "fetch", async (url: unknown) => {
|
|
requests.push(String(url));
|
|
assert.ok(String(url).endsWith("/versions"), "no scoring/oracle call on identity failure");
|
|
if (failure === "timeout") throw new DOMException("fixture timeout", "TimeoutError");
|
|
return Response.json({ error: "fixture_unavailable" }, { status: 503 });
|
|
});
|
|
const input = { accounting: accounting.client, userId: USER_ID, caseId: CASE_ID };
|
|
const result = await scoreAndPersistCurrentEvidence(input);
|
|
assert.equal(result.readOnly, true);
|
|
assert.equal(result.persisted.cached, false);
|
|
assert.equal(result.persisted.algorithmVersion, PREVIOUS);
|
|
assert.equal(result.persisted.selectionAllowed, false);
|
|
assert.equal(result.persisted.confirmationAllowed, false);
|
|
const response = await dossierResponseWithIdentity(parseV9CaseDossier(raw)!, [], {
|
|
status: "verified", requiresSkillAdoption: false,
|
|
} as never);
|
|
assert.match(response.case.result_notice ?? "", /按旧算法产出/);
|
|
assert.equal(response.case.result_identity.algorithm_version, PREVIOUS);
|
|
assert.equal(response.case.result_identity.can_recompare, false);
|
|
assert.equal(response.choice_card, null);
|
|
const candidateId = golden.score.candidate_decisions[0].candidate_id;
|
|
await assert.rejects(acceptV9Candidate(accounting.client, USER_ID, CASE_ID, golden.score.result_id, candidateId, TURN_ID), /result_identity_read_only/);
|
|
await assert.rejects(confirmV9BirthTime(accounting.client, USER_ID, CASE_ID, {
|
|
resultId: golden.score.result_id, candidateId, requestId: TURN_ID, consentQuote: "fixture", sourceTurnId: TURN_ID,
|
|
}), /result_identity_read_only/);
|
|
await assert.rejects(advanceV9CaseFromBlockScan(accounting.client, USER_ID, CASE_ID, range), /result_identity_read_only/);
|
|
const tools = createRectificationV9Tools({ ...input, turnId: TURN_ID, accounting: accounting.client as never });
|
|
const projection = await (tools["rectification-compare-candidates"] as unknown as { execute(input: unknown): Promise<{ read_only: boolean }> }).execute({ caseId: CASE_ID });
|
|
assert.equal(projection.read_only, true);
|
|
const completed = accounting.calls.find((call) => call.fn === "insert_agentic_rectification_tool_receipt" && call.args.p_status === "completed");
|
|
assert.equal(completed?.args.p_engine_version, PREVIOUS);
|
|
const readProjection = await (tools["rectification-read-case"] as unknown as { execute(input: unknown): Promise<Record<string, unknown>> }).execute({ caseId: CASE_ID });
|
|
assert.equal(readProjection.read_only, true);
|
|
assert.equal((readProjection.candidate_summary as Record<string, unknown>).selection_allowed, false);
|
|
const diagnostics = await (tools["rectification-read-diagnostics"] as unknown as { execute(input: unknown): Promise<Record<string, unknown>> }).execute({ caseId: CASE_ID });
|
|
assert.equal(diagnostics.read_only, true);
|
|
assert.match(String(diagnostics.notice), /按旧算法产出/);
|
|
assert.equal(diagnostics.algorithm_version, PREVIOUS);
|
|
assert.equal(diagnostics.can_confirm_exact_minute, false);
|
|
assert.equal(diagnostics.diagnostics, null);
|
|
assert.deepEqual(diagnostics.executed_methods, []);
|
|
const diagnosticsReceipt = accounting.calls.find((call) => call.fn === "insert_agentic_rectification_tool_receipt" && call.args.p_tool_name === "rectification-read-diagnostics" && call.args.p_status === "completed");
|
|
assert.equal(diagnosticsReceipt?.args.p_engine_version, PREVIOUS);
|
|
await assert.rejects((tools["rectification-offer-candidates"] as unknown as { execute(input: unknown): Promise<unknown> }).execute({ caseId: CASE_ID }), /result_identity_read_only/);
|
|
assert.equal((await ensureNonTerminalTurnExit(input)).persisted, false);
|
|
assert.equal((await persistNextInterviewIfIdle(input)).persisted, false);
|
|
assert.ok(accounting.calls.every((call) => call.fn.startsWith("get_") || call.fn === "insert_agentic_rectification_tool_receipt"), "no candidate, focus, inference or validation writes");
|
|
assert.ok(requests.length > 0);
|
|
});
|
|
}
|
|
}
|
|
|
|
test("missing stage source, absent results and nonlatest result IDs cannot authorize candidate writes", async (t) => {
|
|
isolateIdentityEnv(t);
|
|
process.env.RECTIFICATION_ALGORITHM_VERSION = CURRENT;
|
|
process.env.RECTIFICATION_DECISION_POLICY_VERSION = golden.versions.decision_policy_version;
|
|
const latest = { ...golden.score, result_id: golden.score.result_id, candidates: golden.score.candidate_decisions };
|
|
const block = JSON.parse(readFileSync(new URL("./fixtures/rectification-engine-version-cross-midnight-block-golden.json", import.meta.url), "utf8")).response;
|
|
for (const raw of [
|
|
dossierFixture({ stage: "block_scan", latestResult: latest, blockScan: null }),
|
|
dossierFixture({ stage: "minute", latestResult: null, blockScan: block }),
|
|
dossierFixture({ stage: "minute", latestResult: null, blockScan: null }),
|
|
dossierFixture({ stage: "minute", latestResult: latest }),
|
|
]) {
|
|
const accounting = fakeAccounting({ get_agentic_rectification_case_dossier: () => raw });
|
|
await assert.rejects(assertV9ResultWritable(accounting.client, USER_ID, CASE_ID, TURN_ID), /result_identity_read_only/);
|
|
assert.ok(accounting.calls.every((call) => call.fn.startsWith("get_")));
|
|
}
|
|
const empty = dossierFixture({ latestResult: null, blockScan: null });
|
|
const accounting = fakeAccounting({ get_agentic_rectification_case_dossier: () => empty });
|
|
await assertV9ResultWritable(accounting.client, USER_ID, CASE_ID); // ordinary new-case collection remains allowed
|
|
await assert.rejects(assertV9ResultWritable(accounting.client, USER_ID, CASE_ID, null), /result_identity_read_only/);
|
|
assert.equal(resultIdentityView(parseV9CaseDossier(empty)!, { algorithmVersion: null, policyVersion: null }).read_only, false);
|
|
});
|
|
|
|
const historicalGolden = golden;
|
|
async function runGoldenToolSequence(t: test.TestContext, options: {
|
|
compareVersion?: string;
|
|
diagnosticsVersion?: string;
|
|
diagnosticsFailure?: boolean;
|
|
useLiveGolden?: boolean;
|
|
} = {}) {
|
|
isolateIdentityEnv(t);
|
|
// A scoring-9 response needs its real dated contract, never a relabeled scoring-8 fixture.
|
|
const golden = options.useLiveGolden ? {
|
|
request: liveGolden.request,
|
|
score: liveGolden.response,
|
|
versions: {
|
|
algorithm_version: liveGolden.response.algorithm_version,
|
|
decision_policy_version: liveGolden.response.decision_policy_version,
|
|
},
|
|
} : historicalGolden;
|
|
const request = golden.request;
|
|
const candidateRange = {
|
|
start_time: request.start_time, end_time: request.end_time,
|
|
...(request.candidate_intervals ? { candidate_intervals: request.candidate_intervals } : {}),
|
|
};
|
|
const evidence = request.events.map((event: Record<string, string>) => ({
|
|
id: event.id, source_turn_id: TURN_ID, subject: "self", event_kind: event.event_kind,
|
|
domain: event.domain, occurred_from: event.date_start, occurred_to: event.date_end,
|
|
date_precision: event.precision, summary: event.summary, status: "confirmed",
|
|
}));
|
|
const calls: string[] = [];
|
|
t.mock.method(globalThis, "fetch", async (url: unknown) => {
|
|
const path = new URL(String(url)).pathname;
|
|
calls.push(path);
|
|
if (path.endsWith("/versions")) return Response.json(golden.versions);
|
|
if (path.endsWith("/score")) {
|
|
return Response.json({ ...golden.score, algorithm_version: options.compareVersion ?? CURRENT });
|
|
}
|
|
if (path.endsWith("/diagnostics")) {
|
|
if (options.diagnosticsFailure) return Response.json({ error: "fixture_unavailable" }, { status: 503 });
|
|
return Response.json({ ...golden.score, algorithm_version: options.diagnosticsVersion ?? CURRENT });
|
|
}
|
|
throw new Error(`unexpected engine request ${path}`);
|
|
});
|
|
const accounting = fakeAccounting({
|
|
...receiptHandlers,
|
|
get_agentic_rectification_case_dossier: () => dossierFixture({ candidateRange, evidence, latestResult: null }),
|
|
get_agentic_rectification_case_compute: () => ({
|
|
...computeFixture({ baselineBirthSnapshot: {
|
|
birth_date: request.birth_date, latitude: request.lat, longitude: request.lon,
|
|
timezone_offset: request.tz, timezone_id: "Asia/Shanghai", birth_time_source: "approximate",
|
|
} }), candidate_range: candidateRange,
|
|
}),
|
|
persist_agentic_rectification_candidate_v2: (_fn, args) => ({
|
|
result_id: args.p_engine_result_id, cached: false, candidates: args.p_candidates,
|
|
algorithm_version: args.p_algorithm_version, event_contract_version: args.p_event_contract_version,
|
|
decision_policy_version: args.p_decision_policy_version, decision_receipt: args.p_decision_receipt,
|
|
execution_ledger: args.p_execution_ledger, overall_confidence: golden.score.decision_receipt.overall_confidence,
|
|
selection_allowed: golden.score.decision_receipt.selection_allowed, confirmation_allowed: false,
|
|
representative_time: golden.score.decision_receipt.representative_time,
|
|
}),
|
|
set_agentic_rectification_conversation_focus: (_fn, args) => ({
|
|
focus: { id: FOCUS_ID, case_id: CASE_ID, question_id: args.p_question_id,
|
|
intent: args.p_intent, target_evidence_id: args.p_target_evidence_id,
|
|
target_domain: args.p_target_domain, target_kind: args.p_target_kind,
|
|
expected_answer_schema: args.p_expected_answer_schema, status: "active" }, idempotent: false,
|
|
}),
|
|
});
|
|
const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never });
|
|
for (const name of ["rectification-compare-candidates", "rectification-read-diagnostics"] as const) {
|
|
const execute = () => (tools[name] as unknown as { execute(input: unknown): Promise<unknown> }).execute({ caseId: CASE_ID });
|
|
if (options.diagnosticsFailure && name === "rectification-read-diagnostics") await assert.rejects(execute);
|
|
else {
|
|
const projection = await execute() as { read_only?: boolean; algorithm_version?: string; can_confirm_exact_minute?: boolean };
|
|
if (name === "rectification-compare-candidates" && options.compareVersion && options.compareVersion !== golden.versions.algorithm_version) {
|
|
assert.equal(projection.read_only, true, "rolling response mismatch stays read-only after saving true provenance");
|
|
assert.equal(projection.algorithm_version, options.compareVersion);
|
|
}
|
|
if (name === "rectification-read-diagnostics" && options.diagnosticsVersion && options.diagnosticsVersion !== golden.versions.algorithm_version) {
|
|
assert.equal(projection.read_only, true);
|
|
assert.equal(projection.algorithm_version, options.diagnosticsVersion);
|
|
assert.equal(projection.can_confirm_exact_minute, false, "rolling diagnostics cannot claim current validation");
|
|
}
|
|
}
|
|
}
|
|
assert.ok(calls.includes("/api/rectification/v5/score"));
|
|
const persisted = accounting.calls.find((call) => call.fn === "persist_agentic_rectification_candidate_v2");
|
|
assert.equal(persisted?.args.p_algorithm_version, options.compareVersion ?? CURRENT);
|
|
return accounting.calls.filter((call) => call.fn === "insert_agentic_rectification_tool_receipt");
|
|
}
|
|
|
|
test("new Case scoring writes actual identity only on completed receipts", async (t) => {
|
|
const receipts = await runGoldenToolSequence(t);
|
|
assert.deepEqual(receipts.map((call) => [call.args.p_status, call.args.p_engine_version]), [
|
|
// 原值: started=CURRENT;新值: null;原因: BUG-984,开始不是成功结果身份。
|
|
["started", null], ["completed", CURRENT], ["started", null], ["completed", CURRENT],
|
|
]);
|
|
});
|
|
|
|
test("failed diagnostics do not claim an engine result identity", async (t) => {
|
|
const receipts = await runGoldenToolSequence(t, { diagnosticsFailure: true });
|
|
assert.deepEqual(receipts.map((call) => [call.args.p_status, call.args.p_engine_version]), [
|
|
["started", null], ["completed", CURRENT], ["started", null], ["failed", null],
|
|
]);
|
|
});
|
|
|
|
test("mixed completed identities require chronological aggregation (covered by database regression)", async (t) => {
|
|
// Identity-only mutation models rolling backend versions; all response shape and values
|
|
// remain the real native golden. This is not a claim to have run future algorithms.
|
|
const version9 = "rectification-v5-matrix-scoring-9";
|
|
const version10 = "rectification-v5-matrix-scoring-10";
|
|
const receipts = await runGoldenToolSequence(t, { compareVersion: version9, diagnosticsVersion: version10, useLiveGolden: true });
|
|
const completed = receipts.filter((call) => call.args.p_status === "completed");
|
|
assert.equal(new Set(completed.map((call) => call.args.p_turn_id)).size, 1);
|
|
assert.deepEqual(completed.map((call) => call.args.p_engine_version), [version9, version10]);
|
|
// This proves why string max is invalid; database-rectification-block-scan.test.ts
|
|
// executes the authorized aggregate migration and verifies chronological selection.
|
|
assert.equal([version9, version10].sort().at(-1), version9);
|
|
assert.notEqual([version9, version10].sort().at(-1), completed.at(-1)?.args.p_engine_version);
|
|
});
|
|
|
|
test("history opens with its bound Skill and reads old engine receipt values unchanged", async (t) => {
|
|
isolateIdentityEnv(t);
|
|
const bound = resolveSkillPackageVersion("jyotish-birth-time-rectification", "10.0.17");
|
|
for (const oldVersion of ["rectification-v5", PREVIOUS]) {
|
|
const historical = { turn_id: TURN_ID, engine_version: oldVersion, skill_name: bound.name, skill_version: bound.version, status: "completed" };
|
|
const before = structuredClone(historical);
|
|
const open = { disposition: "readonly", case_id: CASE_ID, session_id: SESSION_ID,
|
|
status: "closed", should_start_opening: false, skill_version: bound.version };
|
|
const accounting = fakeAccounting({
|
|
open_agentic_rectification_case: () => open,
|
|
get_agentic_rectification_skill_identity: () => ({ skill_name: bound.name, skill_version: bound.version, skill_sha256: bound.sha256, skill_source_commit: bound.sourceCommit }),
|
|
open_agentic_rectification_case_v2: (_fn, args) => {
|
|
assert.equal(args.p_skill_version, bound.version);
|
|
assert.equal(args.p_skill_sha256, bound.sha256);
|
|
assert.equal("p_engine_version" in args, false);
|
|
return open;
|
|
},
|
|
get_agentic_rectification_turn_receipt: () => historical,
|
|
});
|
|
const opened = await openRectificationCase(accounting.client as never, USER_ID, {
|
|
intent: "session", requestId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", sessionId: SESSION_ID,
|
|
});
|
|
assert.equal(opened.disposition, "readonly");
|
|
const receipt = await loadV9TurnReceipt(accounting.client as never, USER_ID, CASE_ID, TURN_ID);
|
|
assert.equal(v9EngineVersion(), LIVE_CURRENT);
|
|
assert.equal(receipt?.engineVersion, oldVersion);
|
|
assert.deepEqual(historical, before);
|
|
assert.equal(accounting.calls.some((call) => /insert|persist|upgrade/.test(call.fn)), false);
|
|
}
|
|
});
|