Cap planet captions at 30 degrees with a third ring, fall back only within the current person's sessions, and load self when the people catalog fails.
136 lines
7.4 KiB
TypeScript
136 lines
7.4 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/);
|
|
assert.match(people, /title=\{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: [] });
|
|
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();
|
|
}
|
|
});
|