Files
Jyotisha/frontend/tests/people-archive-regressions.test.ts
T
jesse-ux d08ffdc0a6
Independent Staging Quality Gate / validate (push) Failing after 6m46s
Independent Staging Quality Gate / publish (push) Skipped
fix: keep western labels near their planets and chats on the current person
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.
2026-09-25 23:02:17 +08:00

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();
}
});