814c924e4a
Keep askable cards after exhaustion, explain each probe, read the adopted credible range in reports and chat, and compare declared periods before the minute grid when the clock is unknown. Co-authored-by: Cursor <cursoragent@cursor.com>
721 lines
25 KiB
TypeScript
721 lines
25 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.equal(profile.baseline.ayanamsa, "raman");
|
|
assert.deepEqual(profile.candidateRange, { start_time: "04:45", end_time: "05:15" });
|
|
assert.ok(!("password" in profile.baseline));
|
|
});
|
|
|
|
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) => {
|
|
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) => {
|
|
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.deepEqual(tight.candidateRange, { start_time: "14:30", end_time: "15:00" });
|
|
});
|
|
|
|
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) => {
|
|
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 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,
|
|
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");
|
|
});
|