fix(consult): resolve chat subject from server-owned profile binding
This commit is contained in:
@@ -73,7 +73,10 @@ test("standard consultation resolves and settles the session-pinned model versio
|
||||
assert.match(consultRoute, /sessionId: z\.string\(\)\.uuid\(\)/);
|
||||
// Former value: select("id,model_id,model_config_version,session_type,messages").
|
||||
// First-round session titles need the current title, theme, and chart role.
|
||||
assert.match(consultRoute, /select\("id,model_id,model_config_version,session_type,messages,title,theme,chart_profile_role,context_summary"\)/);
|
||||
// 原值: select includes chart_profile_role but not chart_profile_id or chart_profile_name.
|
||||
// 新值: the same select also reads chart_profile_id and chart_profile_name.
|
||||
// 原因: ordinary chat resolves the subject from the stored binding, not from chart_profile_role alone.
|
||||
assert.match(consultRoute, /select\("id,model_id,model_config_version,session_type,messages,title,theme,chart_profile_id,chart_profile_name,chart_profile_role,context_summary"\)/);
|
||||
assert.match(consultRoute, /resolveSessionLanguageModel\(\s*chatSession\.model_id,\s*chatSession\.model_config_version,?\s*\)/);
|
||||
assert.match(consultRoute, /actualModelId: selectedModel\.id/);
|
||||
assert.match(consultRoute, /modelConfigVersion: selectedModel\.configVersion/);
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
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");
|
||||
});
|
||||
Reference in New Issue
Block a user