Files
Jyotisha/frontend/tests/rectification-v9-case-service.test.ts
T
Jesse_Chen 5b023e9480
Independent Staging Quality Gate / validate (push) Successful in 12m32s
Independent Staging Quality Gate / publish (push) Successful in 9m9s
fix(rectification): open cases for uncertain birth times
2026-08-15 20:24:43 +08:00

533 lines
18 KiB
TypeScript

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 server-owned radius around reported time", 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.deepEqual(profile.candidateRange, { start_time: "04:45", end_time: "05:15" });
assert.ok(!("password" in profile.baseline));
});
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) => {
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("unknown-time profiles open with a full-day server-owned range", async () => {
const capturedArgs: { value: Record<string, unknown> | null } = { value: null };
const accounting = fakeAccounting({
profile: {
...completeProfile,
reported_birth_time: null,
birth_time_source: "unknown",
birth_time_period: null,
},
rpc: async (_fn, args) => {
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" });
});
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 legacy uncertainty for their fresh range", 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) => {
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,
reported_birth_time: "05:00",
active_birth_time: null,
uncertainty_before_minutes: 10,
uncertainty_after_minutes: 10,
});
}
});
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");
});