Files
Jyotisha/frontend/tests/rectification-ingest-p0.test.ts
T
Jesse_ChenandClaude Opus 5.5 ea21743b09 fix(rectification): stop spoken collect once the training gate opens (BUG-1084..1087)
Once the discriminator training gate is open, only choice cards are asked
and the range card goes out when they are exhausted; targeted lines, their
re-ask and guided windows no longer hold the card or invite more events.
Delivery body says how many choice questions were used instead of the event
fit percent; narration names an excluded cluster instead of "range
unchanged"; a delivered turn no longer carries a collect question.

Offline replay (v4, 3 radii x 2 directions): truth in range 20/20 in every
cell; guided-window injections give the same width in truth and opposite
directions, so red line 1 was revised by product to truth-in-range only.
Skill 10.0.31 -> 10.0.32 (10.0.31 kept as deprecated for pinned cases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-29 10:06:57 +08:00

287 lines
13 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 { existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import test from "node:test";
import {
DATE_PRECISIONS,
EVIDENCE_DOMAINS,
EVIDENCE_KINDS,
displayDateLabel,
isEvidenceDomain,
isEvidenceKind,
quoteIsGroundedInMessage,
} from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import {
confirmationAllowedForWidth,
indistinguishableWidthMinutes,
} from "../src/lib/rectification-agentic/v9/candidate-plateau.ts";
import { RECTIFICATION_POLICY } from "../src/lib/rectification-policy.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CASE_ID,
CANDIDATE_ID,
EVIDENCE_ID,
FOCUS_ID,
RESULT_ID,
SECOND_CANDIDATE_ID,
TURN_ID,
USER_ID,
candidateSnapshotFixture,
computeFixture,
dossierFixture,
fakeAccounting,
receiptHandlers,
} from "./rectification-v9-test-support.ts";
const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883";
const ingestMigration = readFileSync(
new URL("../supabase/migrations/20260819010000_rectification_ingest_precision_plateau.sql", import.meta.url),
"utf8",
);
const occupationHoraryMigration = readFileSync(
new URL("../supabase/migrations/20260820040000_rectification_occupation_horary.sql", import.meta.url),
"utf8",
);
const agentSource = readFileSync(
new URL("../src/mastra/agentic-rectification.ts", import.meta.url),
"utf8",
);
const skill = readFileSync(
new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url),
"utf8",
);
const evidenceModel = readFileSync(
new URL("../../skills/jyotish-birth-time-rectification/references/evidence-model.md", import.meta.url),
"utf8",
);
const candidateComparison = readFileSync(
new URL("../../skills/jyotish-birth-time-rectification/references/candidate-comparison.md", import.meta.url),
"utf8",
);
function quotedSqlValues(source: string, pattern: RegExp): string[] {
const match = source.match(pattern);
assert.ok(match?.[1], `missing SQL allowlist: ${pattern}`);
return [...match[1].matchAll(/'([^']+)'/g)].map((value) => value[1]);
}
test("SQL kind and domain helpers cover the TypeScript evidence allowlists", () => {
const sqlKinds = quotedSqlValues(
occupationHoraryMigration,
/create or replace function public\.agentic_rectification_evidence_kinds\(\)[\s\S]*?select array\[([\s\S]*?)\]::text\[\]/,
);
const sqlDomains = quotedSqlValues(
occupationHoraryMigration,
/create or replace function public\.agentic_rectification_evidence_domains\(\)[\s\S]*?select array\[([\s\S]*?)\]::text\[\]/,
);
const sqlPrecisions = quotedSqlValues(
ingestMigration,
/create or replace function public\.agentic_rectification_date_precisions\(\)[\s\S]*?select array\[([\s\S]*?)\]::text\[\]/,
);
assert.deepEqual(sqlKinds, [...EVIDENCE_KINDS]);
assert.deepEqual(sqlDomains, [...EVIDENCE_DOMAINS]);
assert.deepEqual(sqlPrecisions, [...DATE_PRECISIONS]);
assert.equal(isEvidenceKind("education_milestone"), true);
assert.equal(isEvidenceKind("occupation_note"), true);
assert.equal(isEvidenceKind("horary_query"), true);
assert.equal(isEvidenceDomain("education"), true);
assert.equal(isEvidenceDomain("occupation"), true);
assert.equal(isEvidenceDomain("horary"), true);
assert.equal(isEvidenceDomain("health_pressure"), true);
});
test("day precision labels stay ISO dates and never collapse to a year sentence", () => {
assert.equal(displayDateLabel("day", "2024-08-08", null), "2024-08-08");
assert.equal(displayDateLabel("month", "2024-05-01", null), "2024-05");
assert.equal(displayDateLabel("year", "2024-01-01", null), "2024年");
assert.equal(displayDateLabel("range", "2024-01-01", "2024-03-31"), "2024-01-01–2024-03-31");
assert.doesNotMatch(displayDateLabel("day", "2024-08-08", null), /年份|年$/);
assert.match(skill, /display_date_label/);
assert.match(evidenceModel, /禁止把日级格式化成“年份已确定为 YYYY”/);
});
test("ASCII punctuation is stripped for quote grounding without allowing paraphrase", () => {
assert.equal(quoteIsGroundedInMessage("2016年6月高考结束", "2016年6月高考结束,"), true);
assert.equal(quoteIsGroundedInMessage("2016年6月高考结束", "那年夏天考完了"), false);
});
test("a 25-minute tied plateau projects width and forbids unique-minute confirmation", () => {
const candidates = [
{ candidateId: CANDIDATE_ID, time: "04:45", rank: 1, relativeSupport: 40, tiedMinuteCount: 25 },
{ candidateId: SECOND_CANDIDATE_ID, time: "04:46", rank: 2, relativeSupport: 35, tiedMinuteCount: 25 },
{ candidateId: THIRD_CANDIDATE_ID, time: "04:47", rank: 3, relativeSupport: 25, tiedMinuteCount: 25 },
];
const width = indistinguishableWidthMinutes(candidates);
assert.ok(width >= 25);
assert.equal(confirmationAllowedForWidth(true, width), false);
assert.equal(RECTIFICATION_POLICY.maxConfirmationWidthMinutes, 5);
const projection = latestResultToolProjection({
resultId: RESULT_ID,
candidates,
selectionAllowed: true,
confirmationAllowed: true,
representativeTime: "04:45",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
}));
assert.equal(projection.engine_indistinguishable_width_minutes, width);
assert.equal(projection.confirmation_allowed, false);
assert.equal(
(projection.confirmation_gate as { confirmation_allowed: boolean }).confirmation_allowed,
false,
);
assert.match(candidateComparison, /一段不可分区间/);
assert.match(candidateComparison, /代表性候选/);
assert.match(agentSource, /不得宣称唯一出生分钟/);
});
test("read-case evidence context keeps day labels and confirm does not rewrite dates", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [{
id: EVIDENCE_ID,
source_turn_id: TURN_ID,
subject: "self",
event_kind: "relationship_end",
domain: "relationship",
occurred_from: "2024-08-08",
occurred_to: null,
date_precision: "day",
summary: "2024-08-08 的一件关系变化",
status: "pending_confirmation",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:06.000Z",
}],
latestResult: {
...candidateSnapshotFixture({
confirmationAllowed: true,
representativeTime: "04:45",
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:45", relative_support: 40, tied_minute_count: 25 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:46", relative_support: 35, tied_minute_count: 25 },
{ candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "04:47", relative_support: 25, tied_minute_count: 25 },
],
}),
selection_allowed: true,
confirmation_allowed: true,
},
}),
get_agentic_rectification_case_compute: () => computeFixture(),
confirm_agentic_rectification_evidence_v10: (_fn, args) => ({
focus_id: args.p_focus_id,
evidence_id: args.p_evidence_id,
status: "confirmed",
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
const projection = await (tools["rectification-read-case"] as unknown as {
execute(input: unknown): Promise<{
evidence_context: Array<{ date_precision: string; display_date_label: string }>;
latest_result: { confirmation_allowed: boolean; engine_indistinguishable_width_minutes: number };
}>;
}).execute({ caseId: CASE_ID, projection: "full_diagnostics" });
assert.equal(projection.evidence_context[0]?.date_precision, "day");
assert.equal(projection.evidence_context[0]?.display_date_label, "2024-08-08");
assert.doesNotMatch(projection.evidence_context[0]?.display_date_label ?? "", /年份已确定为/);
assert.ok(projection.latest_result.engine_indistinguishable_width_minutes >= 25);
assert.equal(projection.latest_result.confirmation_allowed, false);
await (tools["rectification-confirm-evidence"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({ caseId: CASE_ID, evidenceId: EVIDENCE_ID });
const confirmCall = accounting.calls.find((call) => call.fn === "confirm_agentic_rectification_evidence_v10");
assert.ok(confirmCall);
assert.equal(confirmCall.args.p_focus_id, null);
assert.equal("p_date_precision" in confirmCall.args, false);
assert.equal("p_occurred_from" in confirmCall.args, false);
});
test("new-case skill identity is 10.0.26 and the prompt prefers batch ingest", () => {
// 原值: "10.0.25"
// 原值: "10.0.26"
// 新值: "10.0.27"
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
// 原值: "10.0.30" / 新值: "10.0.31" / 原因: 开场改大白话、题干带例子和示例回答写进 Skill OpeningPolicy(BUG-1049,2026-09-26)
// 原值: "10.0.31" / 新值: "10.0.32" / 原因: 训练门开后只问点选卡、交付正文去吻合率写进 Skill(BUG-1084/1085,2026-09-29)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.32");
// 原值: "10.0.25"
// 原值: "10.0.26"
// 新值: "10.0.27"
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: /^version: 10\.0\.28$/ / 新值: /^version: 10\.0\.29$/ / 原因: 年月阶段改口述并禁止追问本人
// 原值: /^version: 10\.0\.29$/ / 新值: /^version: 10\.0\.30$/ / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
// 原值: /^version: 10\.0\.30$/ / 新值: /^version: 10\.0\.31$/ / 原因: 开场改大白话、题干带例子和示例回答写进 Skill OpeningPolicy(BUG-1049,2026-09-26)
// 原值: /^version: 10\.0\.31$/ / 新值: /^version: 10\.0\.32$/ / 原因: 训练门开后只问点选卡、交付正文去吻合率写进 Skill(BUG-1084/1085,2026-09-29)
assert.match(skill, /^version: 10\.0\.32$/m);
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
assert.match(agentSource, /新事件走 rectification-record-evidence-batch/);
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
});
test("receipt allowlist includes D5, D7 and D3 public methods", () => {
const d5d7Migration = readFileSync(
new URL("../supabase/migrations/20260820020000_rectification_d5_d7_precision.sql", import.meta.url),
"utf8",
);
assert.match(d5d7Migration, /'d5-panchamsha'/);
assert.match(d5d7Migration, /'d7-saptamsha'/);
assert.match(d5d7Migration, /'d12-dwadashamsha'/);
const d3Migration = readFileSync(
new URL("../supabase/migrations/20260820030000_rectification_d3_family.sql", import.meta.url),
"utf8",
);
assert.match(d3Migration, /'d3-drekkana'/);
});
test("dateless occupation_note confirms on write and backfills existing drafts", () => {
const migration = readFileSync(
new URL("../supabase/migrations/20260823010000_rectification_occupation_dateless.sql", import.meta.url),
"utf8",
);
assert.match(migration, /^begin;[\s\S]*^commit;$/m);
assert.match(
migration,
/create or replace function public\.agentic_rectification_allows_dateless_confirm\(p_kind text\)/,
);
assert.match(migration, /p_kind in \('occupation_note', 'appearance_note', 'birthmark_or_scar'\)/);
assert.match(
migration,
/if \(v_precision = 'unknown' or v_from is null\)\s+and not public\.agentic_rectification_allows_dateless_confirm\(v_kind\) then/,
);
assert.match(
migration,
/elsif public\.agentic_rectification_allows_dateless_confirm\(v_kind\)\s+and \(v_precision = 'unknown' or v_from is null\) then/,
);
assert.match(
migration,
/where event_kind in \('occupation_note', 'appearance_note', 'birthmark_or_scar'\)[\s\S]*status in \('draft', 'pending_confirmation'\)/,
);
assert.match(migration, /grant execute on function public\.record_agentic_rectification_evidence_batch/);
assert.equal(
existsSync(new URL("../db/migrations/20260823010000_rectification_occupation_dateless.sql", import.meta.url)),
false,
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
);
});