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 { BACKGROUND_ONLY_KINDS, DISTINCT_KIND_GROUPS, EVIDENCE_KINDS, canTransitEvidenceStatus, 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.0"); assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m); assert.match(skill, /^version: 10\.0\.0$/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 the method in the skill, not a hard-coded questionnaire", () => { // The skill may only mention the old questionnaire as an explicit // prohibition, never as a requirement. assert.match(skill, /不再有固定 10[–-]15 个事件/); assert.match(skill, /固定 80%\/60% 匹配率/); assert.match(skill, /固定 A\/B\/C\/D 问卷/); assert.match(skill, /不得伪造出生分钟/); assert.match(skill, /日期精度真实保留/); assert.match(skill, /candidate[\s\S]*accepted[\s\S]*confirmed/); }); 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("career"), false); assert.equal(isEvidenceDomain("career"), true); assert.equal(isEvidenceDomain("nope"), false); for (const precision of ["year", "month", "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年9月离开家去北京工作", "离开家去北京"), true); assert.equal(quoteIsGroundedInMessage("我去了上海", "去了北京"), false); assert.equal(quoteIsGroundedInMessage("", "任意"), false); }); test("family/other are background-only kinds that never advance scoring", () => { assert.equal(isBackgroundEvidenceKind("family_event"), true); assert.equal(isBackgroundEvidenceKind("other"), true); assert.equal(isBackgroundEvidenceKind("career_entry"), false); assert.equal(BACKGROUND_ONLY_KINDS.size, 2); }); 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.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); });