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); });