Files
Jyotisha/frontend/tests/rectification-v9-contracts.test.ts
T
Jesse_Chen 4e247c112e fix(web): keep thinking off the spoken consult and rectification answer
Enumerate evidence kinds so education cannot be proposed as a kind, and stream Chinese thinking on a separate channel that collapses when the reply arrives.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 21:30:55 +08:00

230 lines
9.8 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import test from "node:test";
import {
MAX_RESUMABLE_CASES_PER_USER,
RECTIFICATION_CASE_STATUSES,
RECTIFICATION_SKILL_NAME,
RECTIFICATION_SKILL_VERSION,
RESUMABLE_CASE_STATUSES,
TERMINAL_CASE_STATUSES,
canTransitToTerminal,
evidenceWritesAllowed,
isRectificationCaseStatus,
isResumableStatus,
isTerminalStatus,
} from "../src/lib/rectification-agentic/v9/case-status.ts";
import {
evidenceKindSchema,
evidenceDomainSchema,
BACKGROUND_ONLY_KINDS,
DISTINCT_KIND_GROUPS,
EVIDENCE_KINDS,
canTransitEvidenceStatus,
evidenceSubjectForDomain,
isBackgroundEvidenceKind,
isDatePrecision,
isEvidenceDomain,
isEvidenceKind,
isEvidenceStatus,
normalizeQuote,
quoteIsGroundedInMessage,
} from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import {
PUBLIC_ACTIVITY_EVENTS,
PUBLIC_RECTIFICATION_PHASES,
PUBLIC_RECTIFICATION_TOOLS,
safeActivityEvent,
} from "../src/lib/rectification-agentic/v9/public-receipt.ts";
import {
openRectificationCaseRequestSchema,
shouldStartOpening,
} from "../src/lib/rectification-agentic/v9/open-request.ts";
const skillDirectory = fileURLToPath(
new URL("../../skills/jyotish-birth-time-rectification", import.meta.url),
);
const skill = readFileSync(`${skillDirectory}/SKILL.md`, "utf8");
const references = [
"evidence-model.md",
"conversation-strategy.md",
"candidate-comparison.md",
"technique-routing.md",
"truth-consent-boundaries.md",
];
test("v9 case status machine separates resumable from terminal statuses", () => {
assert.deepEqual([...RESUMABLE_CASE_STATUSES], [
"draft",
"collecting_evidence",
"candidate_ready",
"candidate_accepted",
"needs_rebaseline",
"paused",
]);
assert.deepEqual([...TERMINAL_CASE_STATUSES], [
"confirmed",
"closed",
"abandoned",
"superseded",
]);
for (const status of RECTIFICATION_CASE_STATUSES) {
assert.equal(isRectificationCaseStatus(status), true);
assert.equal(isResumableStatus(status), RESUMABLE_CASE_STATUSES.includes(status));
assert.equal(isTerminalStatus(status), TERMINAL_CASE_STATUSES.includes(status));
}
assert.equal(isRectificationCaseStatus("mystery"), false);
});
test("terminal transitions are one-way and evidence writes stop at terminal", () => {
assert.equal(canTransitToTerminal("candidate_accepted", "confirmed"), true);
assert.equal(canTransitToTerminal("collecting_evidence", "closed"), true);
assert.equal(canTransitToTerminal("confirmed", "closed"), false);
assert.equal(canTransitToTerminal("closed", "candidate_ready"), false);
assert.equal(evidenceWritesAllowed("collecting_evidence"), true);
assert.equal(evidenceWritesAllowed("candidate_accepted"), true);
assert.equal(evidenceWritesAllowed("needs_rebaseline"), true);
assert.equal(evidenceWritesAllowed("confirmed"), false);
assert.equal(evidenceWritesAllowed("closed"), false);
assert.equal(evidenceWritesAllowed("superseded"), false);
assert.equal(MAX_RESUMABLE_CASES_PER_USER, 1);
});
test("the active rectification skill pins the v10 identity and lives in the right directory", () => {
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.10");
assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m);
assert.match(skill, /^version: 10\.0\.10$/m);
for (const reference of references) {
const content = readFileSync(`${skillDirectory}/references/${reference}`, "utf8");
assert.ok(content.length > 0, `${reference} must be non-empty`);
}
assert.ok(skill.split("\n").length <= 200, "SKILL.md must stay within 200 lines");
});
test("skill keeps Path C A/B/C/D questions and forbids unique-minute claims", () => {
assert.match(skill, /不再有固定 10[–-]15 个事件/);
assert.match(skill, /A\/B\/C\/D 主题问卷/);
assert.match(skill, /80%\/60%/);
assert.match(skill, /事件吻合率/);
assert.match(skill, /D9\/D10 类型表/);
assert.match(skill, /不得伪造出生分钟/);
assert.match(skill, /日期精度真实保留/);
assert.match(skill, /candidate[\s\S]*accepted[\s\S]*confirmed/);
assert.match(skill, /candidate_range_not_birth_time_truth/);
assert.doesNotMatch(skill, /固定 A\/B\/C\/D 问卷、D9\/D10 类型表贴标签/);
});
test("evidence model exposes the full kind/domain/precision/status sets", () => {
for (const kind of EVIDENCE_KINDS) assert.equal(isEvidenceKind(kind), true);
assert.equal(isEvidenceKind("education"), false);
assert.equal(evidenceKindSchema.safeParse("education").success, false);
assert.equal(evidenceKindSchema.safeParse("education_start").success, true);
assert.equal(evidenceDomainSchema.safeParse("education").success, true);
assert.equal(isEvidenceKind("career"), false);
assert.equal(isEvidenceDomain("career"), true);
assert.equal(isEvidenceDomain("nope"), false);
for (const precision of ["year", "month", "quarter", "day", "range", "unknown"]) {
assert.equal(isDatePrecision(precision), true);
}
assert.equal(isDatePrecision("exact_minute"), false);
for (const status of ["draft", "pending_confirmation", "confirmed", "superseded", "rejected"]) {
assert.equal(isEvidenceStatus(status), true);
}
assert.equal(isEvidenceStatus("collected"), false);
});
test("semantically distinct kinds are never folded together", () => {
const flat = DISTINCT_KIND_GROUPS.flat();
assert.ok(flat.includes("career_entry") && flat.includes("career_pressure") && flat.includes("career_exit"));
assert.ok(flat.includes("relationship_start") && flat.includes("relationship_commitment") && flat.includes("relationship_separation"));
assert.equal(new Set(flat).size, flat.length);
});
test("only the server confirmation path may produce confirmed evidence", () => {
assert.equal(canTransitEvidenceStatus("draft", "pending_confirmation"), true);
assert.equal(canTransitEvidenceStatus("pending_confirmation", "confirmed"), true);
assert.equal(canTransitEvidenceStatus("confirmed", "superseded"), true);
assert.equal(canTransitEvidenceStatus("superseded", "confirmed"), false);
assert.equal(canTransitEvidenceStatus("rejected", "confirmed"), false);
assert.equal(canTransitEvidenceStatus("draft", "confirmed"), true);
});
test("quote grounding normalizes whitespace and punctuation", () => {
assert.equal(
normalizeQuote("2016 年 9 月,我离开家去北京开始工作。"),
"2016年9月,我离开家去北京开始工作。".replace(/[\s\u3000,。!?、;:“”‘’()《》·—…,!.;:?]/g, ""),
);
assert.equal(quoteIsGroundedInMessage("2016年6月高考结束", "2016年6月高考结束,"), true);
assert.equal(quoteIsGroundedInMessage("2016年9月离开家去北京工作", "离开家去北京"), true);
assert.equal(quoteIsGroundedInMessage("我去了上海", "去了北京"), false);
assert.equal(quoteIsGroundedInMessage("", "任意"), false);
});
test("family/other background kinds: only other and horary stay off the scoring path", () => {
assert.equal(isBackgroundEvidenceKind("family_event"), false);
assert.equal(isBackgroundEvidenceKind("other"), true);
assert.equal(isBackgroundEvidenceKind("horary_query"), true);
assert.equal(isBackgroundEvidenceKind("occupation_note"), false);
assert.equal(isBackgroundEvidenceKind("career_entry"), false);
assert.equal(isBackgroundEvidenceKind("appearance_note"), false);
assert.equal(BACKGROUND_ONLY_KINDS.size, 2);
assert.equal(evidenceSubjectForDomain("family", "self"), "family");
assert.equal(evidenceSubjectForDomain("career", undefined), "self");
assert.equal(evidenceSubjectForDomain("appearance"), "self");
});
test("public receipt allowlists are exact and deny unknown values", () => {
for (const phase of PUBLIC_RECTIFICATION_PHASES) {
assert.equal(safeActivityEvent(phase), phase);
}
assert.equal(safeActivityEvent("provider.reasoning"), null);
assert.equal(safeActivityEvent("tool.payload"), null);
assert.ok((PUBLIC_RECTIFICATION_PHASES as readonly string[]).includes("thinking.delta"));
assert.equal(PUBLIC_ACTIVITY_EVENTS.length, PUBLIC_RECTIFICATION_PHASES.length);
assert.ok(PUBLIC_RECTIFICATION_TOOLS.includes("rectification-read-case"));
assert.ok(!(PUBLIC_RECTIFICATION_TOOLS as readonly string[]).includes("rectification-scan"));
});
test("open request schemas reject userId, birth data and range from the browser", () => {
const homepage = openRectificationCaseRequestSchema.safeParse({
intent: "homepage",
requestId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
});
assert.equal(homepage.success, true);
const withUserId = openRectificationCaseRequestSchema.safeParse({
intent: "homepage",
requestId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
userId: "ffffffff-0000-4aaa-9bbb-cccccccccccc",
});
assert.equal(withUserId.success, false);
const withBirthData = openRectificationCaseRequestSchema.safeParse({
intent: "homepage",
requestId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
birth_date: "1997-08-08",
});
assert.equal(withBirthData.success, false);
const withRange = openRectificationCaseRequestSchema.safeParse({
intent: "homepage",
requestId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
candidate_range: { start_time: "04:00", end_time: "06:00" },
});
assert.equal(withRange.success, false);
const supersedeTrue = openRectificationCaseRequestSchema.safeParse({
intent: "new",
requestId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
supersedeActive: true,
});
assert.equal(supersedeTrue.success, false);
});
test("shouldStartOpening is server-owned: only freshly created never-started cases", () => {
assert.equal(shouldStartOpening("created", 0), true);
assert.equal(shouldStartOpening("created", 1), false);
assert.equal(shouldStartOpening("resumed", 0), false);
assert.equal(shouldStartOpening("resumed", 5), false);
assert.equal(shouldStartOpening("readonly", 0), false);
});