Files
Jyotisha/frontend/tests/rectification-stale-compare-fix-20260907.test.ts
T
Jesse_ChenandCursor 4e0db55f03 fix(rectification): keep compare requests valid after style cards (BUG-577–580)
Engine asked_probe_keys no longer include varga split hashes that 400 the scorer, failed compares become visible and retry, user stop can still deliver a range on a stale snapshot, and holdout no longer reasks domains already in the ledger.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 15:46:33 +08:00

332 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test, { afterEach } from "node:test";
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
import {
ADOPT_OUTCOMES,
RECTIFICATION_TERMINATION_COPY,
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import {
RECTIFICATION_USER_COPY,
withCompareFailedRetryNotice,
withLastSuccessfulCompareNotice,
} from "../src/lib/rectification-agentic/user-copy.ts";
import {
applyRectificationChoice,
persistNextInterviewIfIdle,
} from "../src/lib/rectification-agentic/v9/answer-choice.ts";
import { resetStaleMinuteRescoreAttemptsForTests } from "../src/lib/rectification-agentic/v9/block-scan-answer.ts";
import { STOP_ACTION } from "../src/lib/rectification-agentic/v9/choice-action.ts";
import { parseToolActivityDetail } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
CASE_ID,
CANDIDATE_ID,
FOCUS_ID,
RESULT_ID,
SECOND_CANDIDATE_ID,
SESSION_ID,
TURN_ID,
USER_ID,
activeFocusFixture,
candidateSnapshotFixture,
computeFixture,
conversationSummaryFixture,
dossierFixture,
fakeAccounting,
receiptHandlers,
} from "./rectification-v9-test-support.ts";
const ACTION_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const QUESTION_ID = "question-1";
afterEach(() => {
resetStaleMinuteRescoreAttemptsForTests();
});
function scoreableEvidenceRows() {
return [
{
id: "44444444-4444-4444-8444-444444444441",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "education_start",
domain: "education",
occurred_from: "2016-09-01",
occurred_to: "2016-09-30",
date_precision: "month",
summary: "education start",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-09-07T00:00:00.000Z",
},
{
id: "44444444-4444-4444-8444-444444444442",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "career_entry",
domain: "career",
occurred_from: "2018-07-01",
occurred_to: null,
date_precision: "month",
summary: "career entry",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-09-07T00:00:00.000Z",
},
{
id: "44444444-4444-4444-8444-444444444443",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "relationship_start",
domain: "relationship",
occurred_from: "2021-05-01",
occurred_to: null,
date_precision: "month",
summary: "relationship start",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-09-07T00:00:00.000Z",
},
{
id: "44444444-4444-4444-8444-444444444444",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "family_event",
domain: "family",
occurred_from: "2023-03-01",
occurred_to: null,
date_precision: "month",
summary: "family event",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-09-07T00:00:00.000Z",
},
];
}
function staleDossier(extra: { status?: string; activeFocus?: ReturnType<typeof activeFocusFixture> | null } = {}) {
const evidence = scoreableEvidenceRows();
const inference = buildInferenceState({
range_start: "04:45",
range_end: "05:15",
candidates: [
{ id: "05:02", time: "05:02", relative_support: 58 },
{ id: "04:55", time: "04:55", relative_support: 42 },
],
events: [
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2018, precision: "month" },
{ id: "e3", domain: "relationship", year: 2021, precision: "month" },
{ id: "e4", domain: "family", year: 2023, precision: "month" },
],
probes: [],
});
return dossierFixture({
status: extra.status ?? "collecting_evidence",
evidence,
latestResult: candidateSnapshotFixture({
evidenceLedgerFingerprint: "b".repeat(64),
representativeTime: "05:02",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: inference,
},
}),
conversationSummary: conversationSummaryFixture({
activeFocus: extra.activeFocus === undefined
? activeFocusFixture({
questionId: QUESTION_ID,
expectedAnswerSchema: {
choice: {
prompt: "平时相处更接近哪一种?",
option_a: "照顾对方感受",
option_b: "习惯自己拿主意",
option_c: "两种都有",
option_d: "说不好",
options: [
{ key: "A", label: "照顾对方感受", answer_class: "yes" },
{ key: "B", label: "习惯自己拿主意", answer_class: "weak_yes" },
{ key: "C", label: "两种都有", answer_class: "no" },
{ key: "D", label: "说不好", answer_class: "unsure" },
],
},
probe_id: "p-d9",
semantic_key: "varga.d9.style",
scoring: true,
},
})
: extra.activeFocus,
}),
});
}
function scoreEnginePayload() {
return {
success: true,
endpoint: "rectification_v5_score",
result_id: RESULT_ID,
algorithm_version: "rectification-event-contract-v2",
event_contract_version: "rectification-event-contract-v2",
decision_policy_version: "rectification-candidate-policy-v2",
execution_ledger_version: "rectification-execution-ledger-v2",
candidate_decisions: [
{ candidate_id: CANDIDATE_ID, time: "05:02", rank: 1, relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, time: "04:55", rank: 2, relative_support: 42, tied_minute_count: 1 },
],
decision_receipt: {
receipt_version: "candidate-decision-receipt-v2",
contract_version: "v2",
event_contract_version: "rectification-event-contract-v2",
policy_version: "rectification-candidate-policy-v2",
decision_policy_version: "rectification-candidate-policy-v2",
display_allowed: true,
selection_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
accept_allowed: true,
confirm_allowed: false,
representative_candidate_id: CANDIDATE_ID,
representative_time: "05:02",
overall_confidence: "high",
margin_percent: 16,
},
execution_ledger: [
{ ledger_version: "rectification-execution-ledger-v2", stage: "technique_layer", method: "d1-rashi", status: "executed", source: "python-engine" },
],
};
}
test("compare failure copy and receipt detail stay user-visible without PII", () => {
assert.equal(
withCompareFailedRetryNotice("这条记下了。"),
`这条记下了。\n\n${RECTIFICATION_USER_COPY.compareFailedRetry}`,
);
assert.equal(
withLastSuccessfulCompareNotice("目前范围 04:4505:15。"),
`目前范围 04:4505:15。\n\n${RECTIFICATION_USER_COPY.lastSuccessfulCompareRange}`,
);
const detail = parseToolActivityDetail({
result_fingerprint: JSON.stringify({
safe_error_code: "engine_request_failed",
engine_message: "asked_probe_keys[0] must be a non-empty string up to 120 characters",
}),
});
assert.equal(detail?.safe_error_code, "engine_request_failed");
assert.match(String(detail?.engine_message), /asked_probe_keys/);
const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8");
assert.match(agentRun, /withCompareFailedRetryNotice/);
assert.match(agentRun, /rectification-compare-candidates/);
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
assert.match(tools, /engine_message: engineMessageForReceipt/);
});
test("idle persist on a stale snapshot calls candidate score once", async () => {
let scoreCalls = 0;
const previous = globalThis.fetch;
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
if (url.includes("/api/rectification/v5/score")) {
scoreCalls += 1;
return {
ok: true,
status: 200,
json: async () => scoreEnginePayload(),
};
}
throw new Error(`unexpected fetch ${url}`);
}) as typeof fetch;
try {
const raw = staleDossier({ activeFocus: null });
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => raw,
get_agentic_rectification_case_compute: () => computeFixture(),
persist_agentic_rectification_candidate_v2: (_fn, args) => ({
result_id: RESULT_ID,
candidates: args.p_candidates,
overall_confidence: "medium",
selection_allowed: true,
confirmation_allowed: false,
representative_time: "05:02",
evidence_ledger_fingerprint: args.p_evidence_ledger_fingerprint,
candidate_range_fingerprint: args.p_candidate_range_fingerprint,
skill_version: args.p_skill_version,
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,
created_at: "2026-09-07T00:00:00.000Z",
}),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }),
});
await persistNextInterviewIfIdle({
accounting: accounting.client,
userId: USER_ID,
caseId: CASE_ID,
askedTurnId: TURN_ID,
});
assert.equal(scoreCalls, 1);
await persistNextInterviewIfIdle({
accounting: accounting.client,
userId: USER_ID,
caseId: CASE_ID,
askedTurnId: TURN_ID,
});
assert.equal(scoreCalls, 1);
} finally {
globalThis.fetch = previous;
}
});
test("STOP on a stale snapshot rescores then delivers a range", async () => {
const previous = globalThis.fetch;
globalThis.fetch = (async () => {
throw new Error("engine down");
}) as typeof fetch;
try {
const raw = staleDossier();
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => raw,
get_agentic_rectification_case_compute: () => computeFixture(),
apply_agentic_rectification_choice_action: (_fn, args) => ({
action_id: args.p_action_id,
status: "applied",
idempotent: false,
question_id: args.p_question_id,
option_id: args.p_option_id,
probe_id: "p-d9",
revision: Number(args.p_expected_revision) + 1,
source_quote: args.p_source_quote,
derived_context: args.p_derived_context,
narration: args.p_narration,
focus_status: args.p_focus_status,
}),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }),
});
const applied = await applyRectificationChoice(accounting.client, {
userId: USER_ID,
caseId: CASE_ID,
sessionId: SESSION_ID,
actionId: ACTION_ID,
action: STOP_ACTION,
focusId: FOCUS_ID,
questionId: QUESTION_ID,
optionId: "stop",
expectedRevision: 1,
});
assert.ok(ADOPT_OUTCOMES.has(applied.nextAction.session_outcome));
assert.equal(applied.nextAction.can_adopt, true);
assert.match(applied.narration, new RegExp(RECTIFICATION_USER_COPY.lastSuccessfulCompareRange));
assert.ok(applied.narration.includes(RECTIFICATION_TERMINATION_COPY) || applied.narration.includes("范围"));
} finally {
globalThis.fetch = previous;
}
});