Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
139 lines
7.7 KiB
TypeScript
139 lines
7.7 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
import {
|
||
bindCurrentSubjectAccount, currentSubjectCatalogReady, currentSubjectStorageKey,
|
||
readCurrentSubjectId, resetSubjectCatalogForTests, setCurrentSubject, subscribeCurrentSubject,
|
||
} from "../src/lib/current-subject.ts";
|
||
import { mergeSubjectSessionRows } from "../src/lib/subject-session-merge.ts";
|
||
import { sessionMatchesSubject } from "../src/lib/chat-profile-picker-model.ts";
|
||
import { createSession } from "../src/lib/home-cloud-sync.ts";
|
||
import { loadSubjectBirth } from "../src/lib/subject-birth.ts";
|
||
|
||
import { normalizeReportSubjectRequest } from "../src/lib/report-subject-request.ts";
|
||
import { personalReportCreateRequestSchema } from "../src/lib/personal-report-route-core.ts";
|
||
import { peekReportsPage, refreshReportsPage, resetSecondaryPageDataForTests, writeReportsPage } from "../src/lib/secondary-page-data.ts";
|
||
|
||
const source = (path: string) => readFileSync(new URL(`../src/${path}`, import.meta.url), "utf8");
|
||
|
||
test("account binding never shares an account's selected person and unchanged catalog emits no subject switch", () => {
|
||
const original = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
|
||
const values = new Map<string, string>([[currentSubjectStorageKey("fictional-A"), "person-A"], [currentSubjectStorageKey("fictional-B"), "person-B"]]);
|
||
Object.defineProperty(globalThis, "localStorage", { configurable: true, value: {
|
||
getItem: (key: string) => values.get(key) ?? null,
|
||
setItem: (key: string, value: string) => values.set(key, value),
|
||
removeItem: (key: string) => values.delete(key),
|
||
} });
|
||
resetSubjectCatalogForTests();
|
||
let emitted = 0;
|
||
const unsubscribe = subscribeCurrentSubject(() => { emitted += 1; });
|
||
try {
|
||
bindCurrentSubjectAccount("fictional-A", null);
|
||
assert.equal(readCurrentSubjectId(), "person-A");
|
||
assert.equal(currentSubjectCatalogReady(), false);
|
||
bindCurrentSubjectAccount("fictional-A", ["person-A"]);
|
||
assert.equal(currentSubjectCatalogReady(), true);
|
||
assert.equal(emitted, 1);
|
||
bindCurrentSubjectAccount("fictional-A", ["person-A"]);
|
||
assert.equal(emitted, 1);
|
||
bindCurrentSubjectAccount("fictional-B", null);
|
||
assert.equal(readCurrentSubjectId(), "person-B");
|
||
assert.equal(currentSubjectCatalogReady(), false);
|
||
bindCurrentSubjectAccount("fictional-B", []);
|
||
assert.equal(readCurrentSubjectId(), "self");
|
||
assert.equal(values.get(currentSubjectStorageKey("fictional-A")), "person-A");
|
||
setCurrentSubject("person-B-new");
|
||
assert.equal(values.get(currentSubjectStorageKey("fictional-B")), "person-B-new");
|
||
} finally {
|
||
unsubscribe();
|
||
resetSubjectCatalogForTests();
|
||
if (original) Object.defineProperty(globalThis, "localStorage", original);
|
||
else Reflect.deleteProperty(globalThis, "localStorage");
|
||
}
|
||
});
|
||
|
||
test("metadata refresh preserves hydrated transcript, deep link and later page while sidebar isolates subjects", () => {
|
||
const opened = { ...createSession("test-model"), id: "opened", messagesHydrated: true, chartProfileId: "person-A", messages: [{ role: "user", text: "明确虚构对话" }] } as ReturnType<typeof createSession>;
|
||
const later = { ...createSession("test-model"), id: "later", chartProfileId: "person-A" };
|
||
const deepLink = { ...createSession("test-model"), id: "deep-link", chartProfileId: "person-B" };
|
||
const rows = mergeSubjectSessionRows([opened, later, deepLink], [{ ...opened, title: "新标题", messages: [], messagesHydrated: false }]);
|
||
assert.equal(rows.length, 3);
|
||
assert.equal(rows[0].messages, opened.messages);
|
||
assert.equal(rows[0].messagesHydrated, true);
|
||
assert.equal(rows[0].title, "新标题");
|
||
assert.deepEqual(rows.filter((row) => sessionMatchesSubject(row, "person-B")).map((row) => row.id), ["deep-link"]);
|
||
const removed = { ...later, messagesHydrated: true, messages: [{ role: "user" as const, text: "明确虚构的旧对话" }] };
|
||
const pruned = mergeSubjectSessionRows([opened, removed, deepLink], [{ ...opened, title: "新标题", messages: [], messagesHydrated: false }], {
|
||
subjectId: "person-A",
|
||
complete: true,
|
||
});
|
||
assert.deepEqual(pruned.map((row) => row.id).sort(), ["deep-link", "opened"]);
|
||
});
|
||
|
||
test("malformed subject ids are rejected before PostgreSQL UUID casts", async () => {
|
||
let queried = false;
|
||
await assert.rejects(loadSubjectBirth({ from() { queried = true; throw new Error("must not query"); } }, "fictional-owner", "not-a-uuid"), { code: "subject_not_found" });
|
||
assert.equal(queried, false);
|
||
});
|
||
|
||
test("people delete requires known usage and synastry never switches the global person", () => {
|
||
const people = source("components/people/people-page.tsx");
|
||
assert.match(people, /暂时查不到这个人的对话数量,稍后再试/);
|
||
assert.match(people, /Number\.isInteger\(usage\.sessions\)/);
|
||
assert.match(people, /pendingDelete\?\.id !== selected\.id/);
|
||
// 原值: /title=\{selectedId \? "编辑人物" : "添加一个人"\}/
|
||
// 新值: 同一张表多一种标题:本人「编辑我的资料」,其余不变
|
||
// 原因: BUG-1081——本人在星盘档案可编辑(TASK-self-edit-avatar-menu-20260928 S1)
|
||
assert.match(people, /title=\{editingSelf \? "编辑我的资料" : selectedId \? "编辑人物" : "添加一个人"\}/);
|
||
assert.doesNotMatch(people, /openPerson\(selected\.id,.*synastry/);
|
||
assert.match(source("hooks/use-synastry.ts"), /startNewChat\(\{ subjectId: "self" \}\)/);
|
||
});
|
||
|
||
test("daily card computes eligibility, calendar and fingerprint from selected profile", () => {
|
||
const binder = source("components/daily-starlanguage-binder.tsx");
|
||
assert.match(binder, /useCurrentSubjectProfile\(props\.profile\)/);
|
||
assert.match(binder, /missingOtherProfileStep\(profile\)/);
|
||
assert.match(binder, /calendarDateInTimeZone\(new Date\(\), profile\.timezoneId\)/);
|
||
assert.match(binder, /dailyStarlanguageProfileKey\(profile\)/);
|
||
assert.match(binder, /!subjectReady/);
|
||
});
|
||
|
||
for (const aliasOnly of [true, false]) {
|
||
test(`report subject alias ${aliasOnly ? "alone" : "with matching canonical field"} passes the unchanged strict schema`, () => {
|
||
const id = "66666666-6666-4666-8666-666666666666";
|
||
const normalized = normalizeReportSubjectRequest({ requestId: id, reportType: "personal_full", subjectId: id,
|
||
...(aliasOnly ? {} : { chartProfileId: id }) });
|
||
assert.equal(normalized.mismatch, false);
|
||
assert.equal(personalReportCreateRequestSchema.parse(normalized.body).chartProfileId, id);
|
||
assert.equal("subjectId" in (normalized.body as object), false);
|
||
assert.equal(personalReportCreateRequestSchema.safeParse({ ...normalized.body as object, unexpected: true }).success, false);
|
||
});
|
||
}
|
||
test("report subject conflict is rejected before canonicalization can hide it", () => {
|
||
assert.equal(normalizeReportSubjectRequest({ subjectId: "self", chartProfileId: "66666666-6666-4666-8666-666666666666" }).mismatch, true);
|
||
});
|
||
|
||
test("same self subject in another account cannot reuse cache or publish a late response", async () => {
|
||
resetSecondaryPageDataForTests();
|
||
resetSubjectCatalogForTests();
|
||
bindCurrentSubjectAccount("fictional-A", []);
|
||
writeReportsPage({ kind: "ready", reports: [], nextCursor: null });
|
||
assert.ok(peekReportsPage());
|
||
let finish!: (response: Response) => void;
|
||
const original = globalThis.fetch;
|
||
globalThis.fetch = () => new Promise(resolve => { finish = resolve; });
|
||
try {
|
||
const pending = refreshReportsPage();
|
||
const rejected = assert.rejects(pending, /scope_changed/);
|
||
bindCurrentSubjectAccount("fictional-B", []);
|
||
assert.equal(peekReportsPage(), null);
|
||
finish(Response.json({ reports: [] }));
|
||
await rejected;
|
||
assert.equal(peekReportsPage(), null);
|
||
} finally {
|
||
globalThis.fetch = original;
|
||
resetSecondaryPageDataForTests();
|
||
resetSubjectCatalogForTests();
|
||
}
|
||
});
|