Files
Jyotisha/frontend/tests/people-archive-regressions.test.ts
T

139 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
});