f708edf365
SQL kinds now match the TypeScript ledger so batch ingest can confirm dated events. Confirm no longer burns the opening focus, recap uses server date labels, and the Agent sees indistinguishable width instead of a fake unique minute. Co-authored-by: Cursor <cursoragent@cursor.com>
214 lines
8.9 KiB
TypeScript
214 lines
8.9 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 {
|
|
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.1");
|
|
assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m);
|
|
assert.match(skill, /^version: 10\.0\.1$/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", "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 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);
|
|
});
|