import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { ConsultationProfileTruthError, prepareConsultationRoute, } from "../src/lib/consultation-route-service.ts"; import { authoritativeSessionBinding, ConsultationSubjectError, consultationSubjectBindingFromSession, consultationSubjectFailureResponse, resolveConsultationSubject, type OwnedChartProfile, } from "../src/lib/consultation-subject-resolver.ts"; const selfProfile = Object.freeze({ name: "Synthetic Self", birth_date: "1990-01-02", reported_birth_time: "08:15:00", active_birth_time: "08:15:00", birth_time_source: "family_exact", birth_time_status: "reported", birth_place_label: "Synthetic City", latitude: 31.2, longitude: 121.5, timezone_offset: 8, country_code: "CN", province_code: "310000", city_code: "310100", district_code: "", }); const otherProfile = Object.freeze({ name: "Synthetic Other", date: "1988-03-04", reportedTime: "09:20", time: "", birthTimeSource: "family_exact", birthTimeStatus: "reported", birthPlaceLabel: "Synthetic Harbor", latitude: 22.3, longitude: 114.2, timezoneOffset: 8, countryCode: "CN", ayanamsa: "lahiri", }); const clientBirth = Object.freeze({ name: "Client Name", year: 2001, month: 2, day: 3, hour: 4, minute: 5, city: "Client City", lat: 1, lon: 2, tz: 3, }); function ownedOther(profile: unknown = otherProfile, userId = "user-self"): OwnedChartProfile { return { id: "chart-other", userId, role: "other", profile }; } test("self resolves from the authoritative profile and other resolves from the owned chart profile", async () => { let selfLoads = 0; let otherLoads = 0; const self = await resolveConsultationSubject({ userId: "user-self", binding: { chartProfileId: "self", chartProfileRole: "self", chartProfileName: "Client Name" }, clientBirth, loadSelfProfile: async (userId) => { assert.equal(userId, "user-self"); selfLoads += 1; return selfProfile; }, loadOwnedChartProfile: async () => { otherLoads += 1; return ownedOther(); }, }); assert.equal(selfLoads, 1); assert.equal(otherLoads, 0); assert.equal(self.role, "self"); assert.equal(self.name, "Synthetic Self"); assert.equal((self.profile as { birth_date: string }).birth_date, "1990-01-02"); const legacy = await resolveConsultationSubject({ userId: "user-self", binding: { chartProfileId: null, chartProfileRole: null, chartProfileName: null }, clientBirth, loadSelfProfile: async () => selfProfile, loadOwnedChartProfile: async () => { otherLoads += 1; return ownedOther(); }, }); assert.equal(otherLoads, 0); assert.equal(legacy.role, "self"); assert.equal((legacy.profile as { birth_date: string }).birth_date, "1990-01-02"); selfLoads = 0; const other = await resolveConsultationSubject({ userId: "user-self", binding: { chartProfileId: "chart-other", chartProfileRole: "other", chartProfileName: "Stale Label" }, clientBirth, loadSelfProfile: async () => { selfLoads += 1; return selfProfile; }, loadOwnedChartProfile: async (chartProfileId) => { assert.equal(chartProfileId, "chart-other"); return ownedOther(); }, }); assert.equal(selfLoads, 0); assert.equal(other.role, "other"); assert.equal(other.name, "Synthetic Other"); assert.equal(other.chartProfileId, "chart-other"); const row = other.profile as { birth_date: string; latitude: number; reported_birth_time: string }; assert.equal(row.birth_date, "1988-03-04"); assert.equal(row.latitude, 22.3); assert.equal(row.reported_birth_time, "09:20"); assert.equal("year" in row, false); assert.equal("lat" in row, false); }); test("another user's id, a random id, a role mismatch, and a deleted or incomplete profile fail closed", async () => { const cases: Array<{ name: string; owned: OwnedChartProfile | null | Error; code: string }> = [ { name: "other user", owned: ownedOther(otherProfile, "user-other"), code: "subject_not_found" }, { name: "random id", owned: null, code: "subject_not_found" }, { name: "deleted", owned: null, code: "subject_not_found" }, { name: "role mismatch", owned: { id: "chart-other", userId: "user-self", role: "self", profile: otherProfile }, code: "subject_role_mismatch", }, { name: "missing", owned: null, code: "subject_not_found", }, { name: "incomplete", owned: ownedOther({ name: "Synthetic Other", year: 2001, lat: 1, city: "Client City" }), code: "subject_incomplete", }, ]; for (const item of cases) { let selfLoads = 0; const error = await resolveConsultationSubject({ userId: "user-self", binding: { chartProfileId: "chart-other", chartProfileRole: "other", chartProfileName: "Client Name" }, clientBirth, loadSelfProfile: async () => { selfLoads += 1; return selfProfile; }, loadOwnedChartProfile: async () => { if (item.owned instanceof Error) throw item.owned; return item.owned; }, }).then(() => null, (caught: unknown) => caught); assert.ok(error instanceof ConsultationSubjectError, item.name); assert.equal(error.code, item.code, item.name); assert.equal(error.message.includes("Client Name"), false, item.name); assert.equal(error.message.includes("Synthetic"), false, item.name); assert.equal(selfLoads, 0, item.name); } }); test("a forged display name cannot complete or replace the server profile", async () => { let selfLoads = 0; const missingName = await resolveConsultationSubject({ userId: "user-self", binding: { chartProfileId: "chart-other", chartProfileRole: "other", chartProfileName: "Client Name" }, clientBirth, loadSelfProfile: async () => { selfLoads += 1; return selfProfile; }, loadOwnedChartProfile: async () => ownedOther({ ...otherProfile, name: "" }), }).then(() => null, (caught: unknown) => caught); assert.ok(missingName instanceof ConsultationSubjectError); assert.equal(missingName.code, "subject_incomplete"); assert.equal(missingName.message.includes("Client Name"), false); assert.equal(selfLoads, 0); const claimedSelf = await resolveConsultationSubject({ userId: "user-self", binding: { chartProfileId: "chart-other", chartProfileRole: "self", chartProfileName: "Synthetic Self" }, clientBirth, loadSelfProfile: async () => { selfLoads += 1; return selfProfile; }, loadOwnedChartProfile: async () => ownedOther(), }).then(() => null, (caught: unknown) => caught); assert.ok(claimedSelf instanceof ConsultationSubjectError); assert.equal(claimedSelf.code, "subject_role_mismatch"); assert.equal(selfLoads, 0); }); test("database failures stay closed and do not leak internals", async () => { const error = await resolveConsultationSubject({ userId: "user-self", binding: { chartProfileId: "chart-other", chartProfileRole: "other", chartProfileName: null }, loadSelfProfile: async () => selfProfile, loadOwnedChartProfile: async () => { throw new Error("duplicate key value violates unique constraint for user-other@example"); }, }).then(() => null, (caught: unknown) => caught); assert.ok(error instanceof ConsultationSubjectError); assert.equal(error.code, "subject_unavailable"); assert.equal(error.message.includes("duplicate"), false); assert.equal(error.message.includes("example"), false); const response = consultationSubjectFailureResponse(error); const encoded = JSON.stringify(response); assert.equal(encoded.includes("duplicate"), false); assert.equal(encoded.includes("user-other"), false); assert.equal(encoded.includes("@"), false); assert.match(response.body.message, /不会扣点/); }); test("conflicting client birth fields still lose to the server profile", async () => { let reserves = 0; let selfLoads = 0; const prepared = await prepareConsultationRoute({ userId: "user-self", mode: "unverified_birth_time", subject: { binding: { chartProfileId: "chart-other", chartProfileRole: "other", chartProfileName: "Client Name" }, clientBirth, loadOwnedChartProfile: async () => ownedOther(), }, loadProfile: async () => { selfLoads += 1; return selfProfile; }, beforeReserve({ serverChart, consultationMode }) { assert.equal(consultationMode, "unverified_birth_time"); assert.equal(serverChart?.name, "Synthetic Other"); assert.equal(serverChart?.toolInput.year, 1988); assert.equal(serverChart?.toolInput.month, 3); assert.equal(serverChart?.toolInput.day, 4); assert.equal(serverChart?.toolInput.hour, 9); assert.equal(serverChart?.toolInput.minute, 20); assert.equal(serverChart?.toolInput.city, "Synthetic Harbor"); assert.equal(serverChart?.toolInput.lat, 22.3); assert.equal(serverChart?.toolInput.lon, 114.2); assert.equal(serverChart?.toolInput.tz, 8); assert.equal(serverChart?.toolInput.ayanamsa, "lahiri"); }, reserve: async () => { reserves += 1; return { charged: true }; }, }); assert.equal(selfLoads, 0); assert.equal(reserves, 1); assert.equal(prepared.subject?.role, "other"); assert.equal(prepared.subject?.name, "Synthetic Other"); assert.notEqual(prepared.serverChart?.toolInput.year, clientBirth.year); }); test("consult preparation receives the other profile and a resolver failure does not charge or prepare a model call", async () => { const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8"); assert.match(route, /chart_profile_id,chart_profile_name,chart_profile_role/); assert.match(route, /clientBirth: parsed\.data/); assert.match(route, /loadOwnedChartProfile/); assert.match(route, /consultationSubjectBindingFromSession\(chatSession\)/); assert.doesNotMatch(route, /parsed\.data\.name/); const failureHandler = route.slice( route.indexOf("instanceof ConsultationSubjectError"), route.indexOf("const modelSelection"), ); assert.match(failureHandler, /consultationSubjectFailureResponse/); assert.doesNotMatch(failureHandler, /getJyotishAgent|streamAgentResponse|streamTextResponse/); let reserves = 0; let selfLoads = 0; let preparations = 0; const error = await prepareConsultationRoute({ userId: "user-self", mode: "unverified_birth_time", subject: { binding: consultationSubjectBindingFromSession({ chart_profile_id: "chart-missing", chart_profile_role: "other", chart_profile_name: "Synthetic Other", }), clientBirth, loadOwnedChartProfile: async () => null, }, loadProfile: async () => { selfLoads += 1; return selfProfile; }, beforeReserve() { preparations += 1; }, reserve: async () => { reserves += 1; return { charged: true }; }, }).then(() => null, (caught: unknown) => caught); assert.ok(error instanceof ConsultationSubjectError); assert.equal(error.code, "subject_not_found"); assert.equal(selfLoads, 0); assert.equal(preparations, 0); assert.equal(reserves, 0); assert.equal(error instanceof ConsultationProfileTruthError, false); }); test("an other chart does not inherit the account candidate range", async () => { let rangeLoads = 0; const prepared = await prepareConsultationRoute({ userId: "user-self", mode: "verified_chart", subject: { binding: { chartProfileId: "chart-other", chartProfileRole: "other", chartProfileName: null }, loadOwnedChartProfile: async () => ownedOther({ ...otherProfile, time: "09:20", birthTimeStatus: "confirmed", }), }, loadProfile: async () => selfProfile, loadCandidateRange: async () => { rangeLoads += 1; return { startTime: "08:00", endTime: "08:30" }; }, reserve: async () => "reserved", }); assert.equal(rangeLoads, 0); assert.equal(prepared.serverChart?.toolInput.hour, 9); assert.equal("candidate_range" in (prepared.serverChart?.toolInput ?? {}), false); }); test("a session with messages rejects rebinding and an empty session stores the server name", async () => { const stored = consultationSubjectBindingFromSession({ chart_profile_id: "chart-other", chart_profile_role: "other", chart_profile_name: "Synthetic Other", }); let selfLoads = 0; const locked = await authoritativeSessionBinding({ userId: "user-self", requested: { chart_profile_id: "chart-next", chart_profile_role: "other", chart_profile_name: "Client Name", }, existingMessages: [{ role: "user", text: "synthetic question" }], existingBinding: stored, loadSelfProfile: async () => { selfLoads += 1; return selfProfile; }, loadOwnedChartProfile: async () => ownedOther(), }).then(() => null, (caught: unknown) => caught); assert.ok(locked instanceof ConsultationSubjectError); assert.equal(locked.code, "subject_locked"); assert.equal(selfLoads, 0); const lockedResponse = consultationSubjectFailureResponse(locked); assert.equal(lockedResponse.status, 409); assert.equal(JSON.stringify(lockedResponse).includes("Client Name"), false); const forgedRole = await authoritativeSessionBinding({ userId: "user-self", requested: { chart_profile_id: "chart-other", chart_profile_role: "self", chart_profile_name: "Synthetic Self", }, existingMessages: [], existingBinding: null, loadSelfProfile: async () => { selfLoads += 1; return selfProfile; }, loadOwnedChartProfile: async () => ownedOther(), }).then(() => null, (caught: unknown) => caught); assert.ok(forgedRole instanceof ConsultationSubjectError); assert.equal(forgedRole.code, "subject_role_mismatch"); assert.equal(selfLoads, 0); const created = await authoritativeSessionBinding({ userId: "user-self", requested: { chart_profile_id: "chart-other", chart_profile_role: "other", chart_profile_name: "Client Name", }, existingMessages: [], existingBinding: null, loadSelfProfile: async () => { selfLoads += 1; return selfProfile; }, loadOwnedChartProfile: async () => ownedOther(), }); assert.equal(selfLoads, 0); assert.deepEqual(created, { chart_profile_id: "chart-other", chart_profile_name: "Synthetic Other", chart_profile_role: "other", }); }); test("refresh and deep link keep the stored binding instead of a global active chart", async () => { const activeChartId = "self"; const stored = consultationSubjectBindingFromSession({ chart_profile_id: "chart-other", chart_profile_name: "Synthetic Other", chart_profile_role: "other", }); assert.notEqual(activeChartId, stored.chartProfileId); let selfLoads = 0; const resolved = await resolveConsultationSubject({ userId: "user-self", binding: stored, loadSelfProfile: async () => { selfLoads += 1; return selfProfile; }, loadOwnedChartProfile: async () => ownedOther(), }); assert.equal(selfLoads, 0); assert.equal((resolved.profile as { birth_date: string }).birth_date, "1988-03-04"); const refreshed = await authoritativeSessionBinding({ userId: "user-self", requested: { title: "synthetic title", chart_profile_id: stored.chartProfileId, chart_profile_name: stored.chartProfileName, chart_profile_role: stored.chartProfileRole, }, existingMessages: [{ role: "assistant", text: "synthetic reply" }], existingBinding: stored, loadSelfProfile: async () => { throw new Error("refresh must not read profiles"); }, loadOwnedChartProfile: async () => { throw new Error("refresh must not reread chart_profiles"); }, }); assert.equal(refreshed?.chart_profile_id, "chart-other"); assert.equal(refreshed?.chart_profile_role, "other"); assert.equal(refreshed?.chart_profile_name, "Synthetic Other"); const sessions = readFileSync(new URL("../src/app/api/sessions/route.ts", import.meta.url), "utf8"); const sessionItem = readFileSync(new URL("../src/app/api/sessions/[id]/route.ts", import.meta.url), "utf8"); assert.match(sessions, /authoritativeSessionBinding/); assert.match(sessionItem, /authoritativeSessionBinding/); assert.match(sessionItem, /sessionSelect = "id,title,theme,model_id,messages,session_type,rectification_case_id,chart_profile_id,chart_profile_name,chart_profile_role/); assert.doesNotMatch(sessionItem, /activeChartId/); assert.doesNotMatch(sessions, /activeChartId/); }); test("a concurrent delete observed by the send fails closed and does not fall back to self", async () => { let visible: OwnedChartProfile | null = ownedOther(); let selfLoads = 0; let reserves = 0; let preparations = 0; const pending = prepareConsultationRoute({ userId: "user-self", mode: "unverified_birth_time", subject: { binding: { chartProfileId: "chart-other", chartProfileRole: "other", chartProfileName: "Synthetic Other" }, clientBirth, loadOwnedChartProfile: async () => { await Promise.resolve(); return visible; }, }, loadProfile: async () => { selfLoads += 1; return selfProfile; }, beforeReserve() { preparations += 1; }, reserve: async () => { reserves += 1; return { charged: true }; }, }); visible = null; const error = await pending.then(() => null, (caught: unknown) => caught); assert.ok(error instanceof ConsultationSubjectError); assert.equal(error.code, "subject_not_found"); assert.equal(selfLoads, 0); assert.equal(preparations, 0); assert.equal(reserves, 0); const source: { date: string; name: string } = { ...otherProfile }; const resolved = await resolveConsultationSubject({ userId: "user-self", binding: { chartProfileId: "chart-other", chartProfileRole: "other", chartProfileName: null }, loadSelfProfile: async () => selfProfile, loadOwnedChartProfile: async () => ownedOther(source), }); source.date = "2000-01-01"; source.name = "Changed After Read"; assert.equal((resolved.profile as { birth_date?: string }).birth_date, "1988-03-04"); assert.equal(resolved.name, "Synthetic Other"); });