Files
Jyotisha/frontend/tests/rectification-v9-case-service.test.ts
T
Jesse_Chen 8e31680b45 fix(rectification): honor declared birth-time uncertainty and split windows over two hours (BUG-571–573)
Intake stores how sure the user is; rectification now searches that range, offers a one-click widen when event fit is low at the edge, and trisects windows longer than two hours before the minute grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 12:18:50 +08:00

748 lines
26 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 test from "node:test";
import {
RectificationCaseServiceError,
adoptLegacyRectificationSkill,
closeRectificationCase,
getRectificationCase,
getRectificationSkillIdentityStatus,
getRectificationEntrySummary,
loadV9RectificationProfile,
mapRectificationRpcError,
openRectificationCase,
upgradeRectificationSkill,
} from "../src/lib/rectification-agentic/v9/case-service.ts";
import type { OpenRectificationCaseRequest } from "../src/lib/rectification-agentic/v9/open-request.ts";
type RpcHandler = (fn: string, args: Record<string, unknown>) => Promise<{
data: unknown;
error: { message: string } | null;
}>;
function fakeAccounting(overrides: {
profile?: Record<string, unknown> | null;
rpc?: RpcHandler;
}) {
const rpc =
overrides.rpc ??
(async () => ({ data: null, error: { message: "agentic_rectification_case_not_found" } }));
return {
from: (table: string) => {
if (table !== "profiles") throw new Error(`unexpected table ${table}`);
const builder = {
select: () => builder,
eq: () => builder,
single: async () => {
if (!overrides.profile) return { data: null, error: { message: "not found" } };
return { data: overrides.profile, error: null };
},
};
return builder;
},
rpc,
} as never;
}
const completeProfile = {
birth_date: "1997-08-08",
birth_place_label: "河北省邯郸市",
reported_birth_time: "05:00",
active_birth_time: "05:08",
birth_time_source: "family_exact",
birth_time_period: null,
uncertainty_before_minutes: 10,
uncertainty_after_minutes: 10,
latitude: 36.420487,
longitude: 114.209936,
timezone_id: "Asia/Shanghai",
timezone_offset: 8,
};
const requestId = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee";
const caseId = "11111111-1111-4111-8111-111111111111";
const sessionId = "22222222-2222-4222-8222-222222222222";
function openRpc(data: unknown): RpcHandler {
return async () => ({ data, error: null });
}
test("loadV9RectificationProfile uses a ±15 engine boundary for family_exact", async () => {
const accounting = fakeAccounting({ profile: completeProfile });
const profile = await loadV9RectificationProfile(accounting, "user-1");
assert.equal(profile.userId, "user-1");
assert.equal(profile.baseline.birth_date, "1997-08-08");
assert.equal(profile.baseline.birth_place_label, "河北省邯郸市");
assert.equal(profile.baseline.timezone_id, "Asia/Shanghai");
assert.equal(profile.baseline.reported_birth_time, "05:00");
assert.equal(profile.baseline.active_birth_time, null);
assert.equal(profile.baselineFingerprint.length, 64);
assert.equal(profile.baseline.uncertainty_before_minutes, 10);
assert.equal(profile.baseline.uncertainty_after_minutes, 10);
assert.equal(profile.baseline.ayanamsa, "raman");
assert.deepEqual(profile.candidateRange, { start_time: "04:45", end_time: "05:15" });
assert.equal(profile.stage, "minute");
assert.ok(!("password" in profile.baseline));
});
test("approximate ±60 opens 04:0006:00 as minute; ±120 opens 03:0007:00 as block_scan", async () => {
const hour = await loadV9RectificationProfile(fakeAccounting({
profile: {
...completeProfile,
birth_time_source: "approximate",
uncertainty_before_minutes: 60,
uncertainty_after_minutes: 60,
},
}), "user-1");
assert.deepEqual(hour.candidateRange, { start_time: "04:00", end_time: "06:00" });
assert.equal(hour.stage, "minute");
const twoHours = await loadV9RectificationProfile(fakeAccounting({
profile: {
...completeProfile,
birth_time_source: "approximate",
uncertainty_before_minutes: 120,
uncertainty_after_minutes: 120,
},
}), "user-1");
assert.deepEqual(twoHours.candidateRange, { start_time: "03:00", end_time: "07:00" });
assert.equal(twoHours.stage, "block_scan");
});
test("profile fingerprint ignores display labels and prior accepted minutes", async () => {
const first = await loadV9RectificationProfile(fakeAccounting({
profile: completeProfile,
}), "user-1");
const presentationOnly = await loadV9RectificationProfile(fakeAccounting({
profile: {
...completeProfile,
birth_place_label: "邯郸(用户备注)",
active_birth_time: "06:30",
},
}), "user-1");
assert.equal(first.baselineFingerprint, presentationOnly.baselineFingerprint);
});
test("ayanamsa is stored on new snapshots but does not change the baseline fingerprint", async () => {
const raman = await loadV9RectificationProfile(fakeAccounting({
profile: completeProfile,
}), "user-1");
const lahiri = await loadV9RectificationProfile(fakeAccounting({
profile: { ...completeProfile, ayanamsa: "lahiri" },
}), "user-1");
assert.equal(raman.baseline.ayanamsa, "raman");
assert.equal(lahiri.baseline.ayanamsa, "lahiri");
assert.equal(raman.baselineFingerprint, lahiri.baselineFingerprint);
});
test("loadV9RectificationProfile normalizes PostgreSQL Date birth dates", async () => {
const dateProfile = {
...completeProfile,
birth_date: new Date("1997-08-08T00:00:00.000Z"),
};
const accounting = fakeAccounting({ profile: dateProfile });
const profile = await loadV9RectificationProfile(accounting, "user-1");
assert.equal(profile.baseline.birth_date, "1997-08-08");
});
test("family exact 0/0 remains profile truth while the Case keeps a movable search window", async () => {
const accounting = fakeAccounting({
profile: {
...completeProfile,
uncertainty_before_minutes: 0,
uncertainty_after_minutes: 0,
},
});
const profile = await loadV9RectificationProfile(accounting, "user-1");
assert.equal(profile.baseline.uncertainty_before_minutes, 0);
assert.equal(profile.baseline.uncertainty_after_minutes, 0);
assert.deepEqual(profile.candidateRange, { start_time: "04:45", end_time: "05:15" });
});
test("loadV9RectificationProfile rejects incomplete profiles without creating a case", async () => {
for (const incomplete of [
{ ...completeProfile, latitude: null, longitude: null, timezone_offset: null },
{ ...completeProfile, birth_place_label: "" },
{ ...completeProfile, timezone_id: "" },
]) {
const accounting = fakeAccounting({ profile: incomplete });
await assert.rejects(
() => loadV9RectificationProfile(accounting, "user-1"),
(error: unknown) =>
error instanceof RectificationCaseServiceError && error.code === "profile_incomplete",
);
}
});
test("period-only profiles open with their selected server-owned range", async () => {
const cases = [
{ intent: "homepage", period: "morning", range: { start_time: "08:00", end_time: "11:59" } },
{ intent: "new", period: "late_night", range: { start_time: "23:00", end_time: "03:59" } },
] as const;
for (const item of cases) {
const capturedArgs: { value: Record<string, unknown> | null } = { value: null };
const accounting = fakeAccounting({
profile: {
...completeProfile,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: item.period,
},
rpc: async (fn, args) => {
if (fn === "open_agentic_rectification_case_v2") capturedArgs.value = args;
return {
data: {
disposition: "created",
case_id: caseId,
session_id: sessionId,
status: "draft",
should_start_opening: true,
skill_version: "9.0.0",
},
error: null,
};
},
});
const response = await openRectificationCase(accounting, "user-1", {
intent: item.intent,
requestId,
});
assert.equal(response.disposition, "created");
assert.deepEqual(capturedArgs.value?.p_candidate_range, item.range);
assert.equal(
(capturedArgs.value?.p_baseline_birth_snapshot as Record<string, unknown>).reported_birth_time,
null,
);
}
});
test("a custom declared window opens the Case with those clocks, not the leftover afternoon bucket", async () => {
const capturedArgs: { value: Record<string, unknown> | null } = { value: null };
const accounting = fakeAccounting({
profile: {
...completeProfile,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: "afternoon",
declared_window_start: "14:30",
declared_window_end: "15:00",
},
rpc: async (fn, args) => {
if (fn === "open_agentic_rectification_case_v2") capturedArgs.value = args;
return {
data: {
disposition: "created",
case_id: caseId,
session_id: sessionId,
status: "draft",
should_start_opening: true,
skill_version: "9.0.0",
},
error: null,
};
},
});
const response = await openRectificationCase(accounting, "user-1", {
intent: "homepage",
requestId,
});
assert.equal(response.disposition, "created");
assert.deepEqual(capturedArgs.value?.p_candidate_range, { start_time: "14:30", end_time: "15:00" });
assert.equal(
(capturedArgs.value?.p_baseline_birth_snapshot as Record<string, unknown>).declared_window_start,
"14:30",
);
assert.equal(
(capturedArgs.value?.p_baseline_birth_snapshot as Record<string, unknown>).declared_window_end,
"15:00",
);
});
test("tightening a declared window changes the Case fingerprint", async () => {
const afternoon = await loadV9RectificationProfile(fakeAccounting({
profile: {
...completeProfile,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: "afternoon",
declared_window_start: "12:00",
declared_window_end: "17:59",
},
}), "user-1");
const tight = await loadV9RectificationProfile(fakeAccounting({
profile: {
...completeProfile,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: "afternoon",
declared_window_start: "14:30",
declared_window_end: "15:00",
},
}), "user-1");
assert.notEqual(afternoon.baselineFingerprint, tight.baselineFingerprint);
assert.deepEqual(afternoon.candidateRange, { start_time: "12:00", end_time: "17:59" });
assert.equal(afternoon.stage, "block_scan");
assert.deepEqual(tight.candidateRange, { start_time: "14:30", end_time: "15:00" });
assert.equal(tight.stage, "minute");
});
test("period-only profiles with an IANA timezone can open when the cached offset is missing", async () => {
const originalFetch = globalThis.fetch;
const capturedArgs: { value: Record<string, unknown> | null } = { value: null };
let timezoneLookupCalled = false;
globalThis.fetch = async (input, init) => {
timezoneLookupCalled = true;
assert.equal(String(input), "http://127.0.0.1:5200/api/location/timezone");
assert.deepEqual(JSON.parse(String(init?.body)), {
latitude: 36.420487,
longitude: 114.209936,
birthDate: "1997-08-08",
birthTime: "20:30",
});
return new Response(JSON.stringify({
available: true,
timezoneId: "Asia/Shanghai",
timezoneOffset: 8,
}), { status: 200 });
};
try {
const accounting = fakeAccounting({
profile: {
...completeProfile,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: "evening",
timezone_offset: null,
},
rpc: async (fn, args) => {
if (fn === "open_agentic_rectification_case_v2") capturedArgs.value = args;
return {
data: {
disposition: "created",
case_id: caseId,
session_id: sessionId,
status: "draft",
should_start_opening: true,
skill_version: "9.0.0",
},
error: null,
};
},
});
const response = await openRectificationCase(accounting, "user-1", {
intent: "homepage",
requestId,
});
assert.equal(response.disposition, "created");
assert.equal(timezoneLookupCalled, true);
assert.deepEqual(capturedArgs.value?.p_candidate_range, { start_time: "18:00", end_time: "22:59" });
assert.equal(
(capturedArgs.value?.p_baseline_birth_snapshot as Record<string, unknown>).timezone_offset,
8,
);
} finally {
globalThis.fetch = originalFetch;
}
});
test("unknown-time profiles open with a full-day server-owned range", async () => {
const capturedArgs: { value: Record<string, unknown> | null } = { value: null };
const rpcNames: string[] = [];
const accounting = fakeAccounting({
profile: {
...completeProfile,
reported_birth_time: null,
birth_time_source: "unknown",
birth_time_period: null,
},
rpc: async (fn, args) => {
rpcNames.push(fn);
if (fn === "open_agentic_rectification_case_v2") capturedArgs.value = args;
return {
data: {
disposition: "created",
case_id: caseId,
session_id: sessionId,
status: "draft",
should_start_opening: true,
skill_version: "9.0.0",
},
error: null,
};
},
});
const response = await openRectificationCase(accounting, "user-1", {
intent: "homepage",
requestId,
});
assert.equal(response.disposition, "created");
assert.deepEqual(capturedArgs.value?.p_candidate_range, { start_time: "00:00", end_time: "23:59" });
assert.ok(rpcNames.includes("set_agentic_rectification_case_stage"));
});
test("fresh profiles still fail closed when their birth-time declaration is incomplete", async () => {
const cases = [
{ source: "period_only", period: null },
{ source: "period_only", period: "not_a_period" },
{ source: "family_exact", period: null },
{ source: "approximate", period: null },
] as const;
for (const item of cases) {
let rpcCalled = false;
const accounting = fakeAccounting({
profile: {
...completeProfile,
reported_birth_time: null,
birth_time_source: item.source,
birth_time_period: item.period,
},
rpc: async () => {
rpcCalled = true;
return { data: null, error: null };
},
});
await assert.rejects(
() => openRectificationCase(accounting, "user-1", { intent: "homepage", requestId }),
(error: unknown) =>
error instanceof RectificationCaseServiceError && error.code === "profile_incomplete",
);
assert.equal(rpcCalled, false);
}
});
test("homepage and new opens ignore active time and keep family_exact at ±15", async () => {
for (const intent of ["homepage", "new"] as const) {
const capturedArgs: { value: Record<string, unknown> | null } = { value: null };
const accounting = fakeAccounting({
profile: completeProfile,
rpc: async (fn, args) => {
if (fn === "open_agentic_rectification_case_v2") capturedArgs.value = args;
return {
data: {
disposition: "created",
case_id: caseId,
session_id: sessionId,
status: "draft",
should_start_opening: true,
skill_version: "9.0.0",
},
error: null,
};
},
});
const request: OpenRectificationCaseRequest = { intent, requestId };
const response = await openRectificationCase(accounting, "user-1", request);
assert.equal(response.disposition, "created");
assert.equal(response.caseId, caseId);
assert.equal(response.sessionId, sessionId);
assert.equal(response.shouldStartOpening, true);
assert.deepEqual(capturedArgs.value?.p_candidate_range, { start_time: "04:45", end_time: "05:15" });
assert.deepEqual(capturedArgs.value?.p_baseline_birth_snapshot, {
birth_date: "1997-08-08",
birth_place_label: "河北省邯郸市",
latitude: 36.420487,
longitude: 114.209936,
timezone_id: "Asia/Shanghai",
timezone_offset: 8,
birth_time_source: "family_exact",
birth_time_period: null,
birth_time_clue: null,
declared_window_start: null,
declared_window_end: null,
reported_birth_time: "05:00",
active_birth_time: null,
uncertainty_before_minutes: 10,
uncertainty_after_minutes: 10,
ayanamsa: "raman",
});
}
});
test("homepage open may create a new case even when resumable history exists", async () => {
const accounting = fakeAccounting({
profile: completeProfile,
rpc: openRpc({
disposition: "created",
case_id: caseId,
session_id: sessionId,
status: "draft",
should_start_opening: true,
skill_version: "9.0.0",
}),
});
const response = await openRectificationCase(accounting, "user-1", {
intent: "homepage",
requestId,
});
assert.equal(response.disposition, "created");
assert.equal(response.shouldStartOpening, true);
});
test("session intent passes the exact sessionId and never the profile snapshot", async () => {
const capturedArgs: { value: Record<string, unknown> | null } = { value: null };
const accounting = fakeAccounting({
profile: null,
rpc: async (fn, args) => {
capturedArgs.value = args;
return {
data: {
disposition: "readonly",
case_id: caseId,
session_id: sessionId,
status: "closed",
should_start_opening: false,
skill_version: "9.0.0",
},
error: null,
};
},
});
const response = await openRectificationCase(accounting, "user-1", {
intent: "session",
requestId,
sessionId,
});
assert.equal(response.disposition, "readonly");
assert.equal(capturedArgs.value?.p_session_id, sessionId);
assert.deepEqual(capturedArgs.value?.p_baseline_birth_snapshot, {});
});
test("non-owner session opens surface a safe not-found error", async () => {
const accounting = fakeAccounting({
profile: null,
rpc: async () => ({ data: null, error: { message: "agentic_rectification_session_not_found" } }),
});
await assert.rejects(
() =>
openRectificationCase(accounting, "user-1", {
intent: "session",
requestId,
sessionId: "99999999-9999-4999-8999-999999999999",
}),
(error: unknown) =>
error instanceof RectificationCaseServiceError && error.code === "case_session_not_found",
);
});
test("legacy active-case conflicts still map to a safe public error", async () => {
const accounting = fakeAccounting({
profile: completeProfile,
rpc: async () => ({ data: null, error: { message: "agentic_rectification_active_case_conflict" } }),
});
await assert.rejects(
() => openRectificationCase(accounting, "user-1", { intent: "new", requestId }),
(error: unknown) =>
error instanceof RectificationCaseServiceError && error.code === "active_case_conflict",
);
});
test("entry summary projects resumable history for homepage context", async () => {
const accounting = fakeAccounting({
rpc: async (fn) => {
assert.equal(fn, "get_agentic_rectification_entry_summary");
return {
data: {
has_resumable_case: true,
has_terminal_case_with_time: false,
latest_resumable: {
case_id: caseId,
status: "collecting_evidence",
last_activity_at: "2026-08-12T00:00:00.000Z",
},
latest_terminal: null,
},
error: null,
};
},
});
const summary = await getRectificationEntrySummary(accounting, "user-1");
assert.equal(summary.hasResumableCase, true);
assert.equal(summary.hasTerminalCaseWithTime, false);
assert.equal(summary.latestResumable?.caseId, caseId);
});
test("get case returns a sanitized projection without the birth snapshot", async () => {
const accounting = fakeAccounting({
rpc: async () => ({
data: {
case_id: caseId,
session_id: sessionId,
status: "candidate_ready",
skill_name: "jyotish-birth-time-rectification",
skill_version: "9.0.0",
candidate_range: { start_time: "04:50", end_time: "05:10" },
accepted_time: null,
confirmed_time: null,
created_at: "2026-08-12T00:00:00.000Z",
last_activity_at: "2026-08-12T00:00:00.000Z",
completed_at: null,
closed_reason: null,
evidence_count: 3,
turn_count: 4,
latest_result: null,
},
error: null,
}),
});
const view = await getRectificationCase(accounting, "user-1", caseId);
assert.equal(view.caseId, caseId);
assert.equal(view.evidenceCount, 3);
assert.deepEqual(view.candidateRange, { start_time: "04:50", end_time: "05:10" });
assert.ok(!("baseline_birth_snapshot" in view));
assert.ok(!("birth_date" in view));
});
test("close is idempotent and never wraps as engine confirmed", async () => {
const accounting = fakeAccounting({
rpc: async (fn, args) => {
assert.equal(args.p_reason, "completed_by_user");
return { data: { success: true, case_id: caseId, status: "closed", idempotent: false }, error: null };
},
});
const result = await closeRectificationCase(accounting, "user-1", caseId, "completed_by_user");
assert.equal(result.status, "closed");
assert.equal(result.idempotent, false);
});
test("skill identity status preserves legacy adoption state", async () => {
const accounting = fakeAccounting({
rpc: async (fn) => {
assert.equal(fn, "get_agentic_rectification_skill_identity_status");
return {
data: {
skill_name: "jyotish-birth-time-rectification",
skill_version: "8.9.0",
skill_sha256: null,
skill_source_commit: null,
skill_identity_status: "legacy_unverifiable",
requires_skill_adoption: true,
},
error: null,
};
},
});
const status = await getRectificationSkillIdentityStatus(accounting, "user-1", caseId);
assert.equal(status.status, "legacy_unverifiable");
assert.equal(status.skillSha256, null);
assert.equal(status.requiresSkillAdoption, true);
});
test("legacy adoption calls the explicit RPC and keeps a null previous hash", async () => {
const accounting = fakeAccounting({
rpc: async (fn, args) => {
assert.equal(fn, "adopt_agentic_rectification_skill_v1");
assert.equal(args.p_skill_name, "jyotish-birth-time-rectification");
assert.equal(args.p_skill_version, "9.0.0");
assert.equal(args.p_skill_sha256, "5acb3103e80993ea611b93d2c1746e70b74aff8f8636bcffc80a837b954b470d");
return {
data: {
success: true,
case_id: caseId,
previous_skill_name: "jyotish-birth-time-rectification",
previous_skill_version: "8.9.0",
previous_skill_sha256: null,
previous_source_commit: null,
previous_identity_status: "legacy_unverifiable",
upgrade_kind: "legacy_adoption",
skill_name: "jyotish-birth-time-rectification",
skill_version: "9.0.0",
skill_sha256: "5acb3103e80993ea611b93d2c1746e70b74aff8f8636bcffc80a837b954b470d",
source_commit: "0fd111d16b45796086a6c1d0945dbd3de6755d8a",
receipt_id: "33333333-3333-4333-8333-333333333333",
idempotent: false,
},
error: null,
};
},
});
const result = await adoptLegacyRectificationSkill(accounting, "user-1", caseId, "9.0.0");
assert.equal(result.previousSkillSha256, null);
assert.equal(result.idempotent, false);
});
test("upgrade skill resolves and passes the exact registered identity", async () => {
const accounting = fakeAccounting({
rpc: async (fn, args) => {
assert.equal(fn, "upgrade_agentic_rectification_skill_v2");
assert.equal(args.p_skill_name, "jyotish-birth-time-rectification");
assert.equal(args.p_skill_version, "9.0.0");
assert.equal(
args.p_skill_sha256,
"5acb3103e80993ea611b93d2c1746e70b74aff8f8636bcffc80a837b954b470d",
);
assert.equal(
args.p_skill_source_commit,
"0fd111d16b45796086a6c1d0945dbd3de6755d8a",
);
return {
data: {
success: true,
case_id: caseId,
previous_skill_version: "8.9.0",
skill_version: "9.0.0",
idempotent: false,
},
error: null,
};
},
});
const result = await upgradeRectificationSkill(accounting, "user-1", caseId, "9.0.0");
assert.equal(result.skillVersion, "9.0.0");
});
test("rpc errors map to safe public views without leaking database text", async () => {
const generic = mapRectificationRpcError(new Error("syntax error at or near \"profiles\""));
assert.equal(generic.status, 500);
assert.equal(generic.code, "rectification_service_failed");
assert.doesNotMatch(generic.message, /syntax error/);
const conflict = mapRectificationRpcError(new Error("agentic_rectification_active_case_conflict"));
assert.equal(conflict.status, 409);
assert.equal(conflict.code, "active_case_conflict");
assert.doesNotMatch(conflict.message, /agentic_rectification/);
const notFound = mapRectificationRpcError(new Error("agentic_rectification_case_not_found"));
assert.equal(notFound.status, 404);
assert.equal(notFound.code, "case_not_found");
const publicLegacy = mapRectificationRpcError(new Error("agentic_rectification_skill_identity_unverifiable"));
assert.equal(publicLegacy.status, 409);
assert.equal(publicLegacy.code, "skill_identity_unverifiable");
const staleProbe = mapRectificationRpcError(new Error("agentic_rectification_stale_probe"));
assert.equal(staleProbe.status, 409);
assert.equal(staleProbe.code, "stale_probe");
const revisionConflict = mapRectificationRpcError(new Error("agentic_rectification_revision_conflict"));
assert.equal(revisionConflict.status, 409);
assert.equal(revisionConflict.code, "revision_conflict");
const staleQuestion = mapRectificationRpcError(new Error("agentic_rectification_stale_question"));
assert.equal(staleQuestion.status, 409);
assert.equal(staleQuestion.code, "stale_question");
const focusInactive = mapRectificationRpcError(new Error("agentic_rectification_focus_not_active"));
assert.equal(focusInactive.status, 409);
assert.equal(focusInactive.code, "focus_not_active");
const patchRetired = mapRectificationRpcError(new Error("agentic_rectification_inference_patch_retired"));
assert.equal(patchRetired.status, 409);
assert.equal(patchRetired.code, "inference_patch_retired");
});