54269fcfcc
page.tsx still owns the chat main chain, but the first product surfaces now live in their own modules so later splits can land without editing the 4k-line Home. Source-lock tests follow the moved tokens; the orphan user-data contract is aligned and added to the quick gate. Co-authored-by: Cursor <cursoragent@cursor.com>
267 lines
9.8 KiB
TypeScript
267 lines
9.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
SESSION_MISSING_NOTICE,
|
|
SESSION_URL_QUERY_KEY,
|
|
SESSION_URL_RETURN_STORAGE_KEY,
|
|
parseSessionUrlQuery,
|
|
persistLoginSessionReturn,
|
|
readLoginSessionReturn,
|
|
resolveBootstrapSessionSelection,
|
|
sessionHref,
|
|
writeSessionUrl,
|
|
} from "../src/lib/chat-session-url.ts";
|
|
|
|
import { homeSurface as page } from "./home-surface.ts";
|
|
const login = readFileSync(new URL("../src/components/email-otp-login.tsx", import.meta.url), "utf8");
|
|
const loginPage = readFileSync(new URL("../src/app/login/page.tsx", import.meta.url), "utf8");
|
|
const sessionA = "11111111-1111-4111-8111-111111111111";
|
|
const sessionB = "22222222-2222-4222-8222-222222222222";
|
|
const sessionC = "33333333-3333-4333-8333-333333333333";
|
|
|
|
function sourceBetween(source: string, startMarker: string, endMarker: string) {
|
|
const start = source.indexOf(startMarker);
|
|
const end = source.indexOf(endMarker, start);
|
|
assert.notEqual(start, -1, startMarker);
|
|
assert.notEqual(end, -1, endMarker);
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
test("bootstrap reads a listed ?c= session and ignores login storage", () => {
|
|
assert.deepEqual(
|
|
resolveBootstrapSessionSelection({
|
|
listedIds: [sessionA, sessionB],
|
|
defaultSessionId: sessionA,
|
|
search: `?${SESSION_URL_QUERY_KEY}=${sessionB}`,
|
|
storedReturnId: sessionC,
|
|
}),
|
|
{
|
|
sessionId: sessionB,
|
|
urlAction: "keep",
|
|
missing: false,
|
|
clearStoredReturn: true,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("bootstrap restores a stored login return only when the URL has no c", () => {
|
|
assert.deepEqual(
|
|
resolveBootstrapSessionSelection({
|
|
listedIds: [sessionA, sessionB],
|
|
defaultSessionId: sessionA,
|
|
search: "",
|
|
storedReturnId: sessionB,
|
|
}),
|
|
{
|
|
sessionId: sessionB,
|
|
urlAction: "replace-selected",
|
|
missing: false,
|
|
clearStoredReturn: true,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("bootstrap clears an illegal or unknown session query", () => {
|
|
assert.equal(parseSessionUrlQuery("").present, false);
|
|
assert.deepEqual(parseSessionUrlQuery(`?${SESSION_URL_QUERY_KEY}=not-a-uuid`), {
|
|
present: true,
|
|
sessionId: null,
|
|
});
|
|
assert.deepEqual(
|
|
resolveBootstrapSessionSelection({
|
|
listedIds: [sessionA],
|
|
defaultSessionId: sessionA,
|
|
search: `?${SESSION_URL_QUERY_KEY}=not-a-uuid`,
|
|
storedReturnId: sessionB,
|
|
}),
|
|
{
|
|
sessionId: sessionA,
|
|
urlAction: "replace-clear",
|
|
missing: true,
|
|
clearStoredReturn: true,
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
resolveBootstrapSessionSelection({
|
|
listedIds: [sessionA],
|
|
defaultSessionId: sessionA,
|
|
search: `?${SESSION_URL_QUERY_KEY}=${sessionB}`,
|
|
storedReturnId: null,
|
|
}),
|
|
{
|
|
sessionId: sessionA,
|
|
urlAction: "replace-clear",
|
|
missing: true,
|
|
clearStoredReturn: true,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("default bootstrap selection does not write a session URL", () => {
|
|
assert.deepEqual(
|
|
resolveBootstrapSessionSelection({
|
|
listedIds: [sessionA, sessionB],
|
|
defaultSessionId: sessionA,
|
|
search: "",
|
|
storedReturnId: null,
|
|
}),
|
|
{
|
|
sessionId: sessionA,
|
|
urlAction: "none",
|
|
missing: false,
|
|
clearStoredReturn: false,
|
|
},
|
|
);
|
|
|
|
const bootstrap = sourceBetween(page, "async function loadCloudData()", "void loadCloudData();");
|
|
assert.match(bootstrap, /defaultSessionId: nextSessions\[0\]\.id/);
|
|
assert.match(bootstrap, /setActiveSessionId\(bootstrapSelection\.sessionId\)/);
|
|
assert.match(bootstrap, /urlAction === "replace-clear"/);
|
|
assert.match(bootstrap, /urlAction === "replace-selected"/);
|
|
assert.doesNotMatch(bootstrap, /writeSessionUrl\([^)]*, "push"\)/);
|
|
assert.match(bootstrap, /setComposerNotice\(SESSION_MISSING_NOTICE\)/);
|
|
});
|
|
|
|
test("session href keeps sibling query keys and can drop a dead c", () => {
|
|
assert.equal(sessionHref("", sessionA), `/?${SESSION_URL_QUERY_KEY}=${sessionA}`);
|
|
assert.equal(
|
|
sessionHref("?preview=conversation", sessionA),
|
|
`/?preview=conversation&${SESSION_URL_QUERY_KEY}=${sessionA}`,
|
|
);
|
|
assert.equal(sessionHref(`?${SESSION_URL_QUERY_KEY}=${sessionA}`, null), "/");
|
|
assert.equal(
|
|
sessionHref(`?preview=conversation&${SESSION_URL_QUERY_KEY}=${sessionA}`, null),
|
|
"/?preview=conversation",
|
|
);
|
|
});
|
|
|
|
test("user session switches push history; popstate reuses selectSession without a second push", () => {
|
|
const selectSession = sourceBetween(
|
|
page,
|
|
"function selectSession(sessionId: string)",
|
|
"async function selectSessionModel",
|
|
);
|
|
assert.match(selectSession, /sessionSelectionSource\.current === "user"/);
|
|
assert.match(selectSession, /writeSessionUrl\(sessionId, "push"\)/);
|
|
assert.match(selectSession, /sessionSelectionSource\.current = "history";\n selectSession\(/);
|
|
assert.equal((selectSession.match(/writeSessionUrl\([^)]*, "push"\)/g) ?? []).length, 1);
|
|
assert.match(page, /window\.addEventListener\("popstate", onPopState\)/);
|
|
});
|
|
|
|
test("creating and leaving a session keep the address bar in sync", () => {
|
|
const startNewChat = sourceBetween(page, "async function startNewChat()", "function selectSession(");
|
|
assert.match(startNewChat, /writeSessionUrl\(nextSession\.id, "push"\)/);
|
|
assert.match(startNewChat, /window\.history\.replaceState\(null, "", previousHref\)/);
|
|
|
|
const deleteSession = sourceBetween(page, "async function deleteSession(", "function togglePinnedSession");
|
|
assert.match(deleteSession, /writeSessionUrl\(fallbackId \|\| null, "replace"\)/);
|
|
|
|
const archiveSession = sourceBetween(page, "function toggleArchivedSession(", "async function shareSession");
|
|
assert.match(archiveSession, /writeSessionUrl\(fallbackId \|\| null, "replace"\)/);
|
|
|
|
const recovery = sourceBetween(
|
|
page,
|
|
"if (status.status === \"completed\") {",
|
|
"pendingConsultation.current = null;",
|
|
);
|
|
assert.match(recovery, /setActiveSessionId\(\(current\) => current \|\| detailed\.id\)/);
|
|
assert.doesNotMatch(recovery, /writeSessionUrl/);
|
|
});
|
|
|
|
test("401 login redirect stashes the current session id without changing the login page", () => {
|
|
const redirect = sourceBetween(page, "function redirectToLogin(): never {", "function waitForUndoWindow");
|
|
// Former value: the body was only `window.location.replace("/login")`.
|
|
// 401 now stashes a UUID ?c= so returning to `/` can restore the session
|
|
// without adding a `next` query to the login page.
|
|
assert.match(redirect, /persistLoginSessionReturn\(\);/);
|
|
assert.match(redirect, /window\.location\.replace\("\/login"\);/);
|
|
assert.doesNotMatch(login, /sessionStorage|SESSION_URL_RETURN|searchParams\.get\("next"\)/);
|
|
assert.match(login, /successPath\?: "\/" \| "\/admin"/);
|
|
assert.doesNotMatch(loginPage, /next=/);
|
|
assert.equal(SESSION_URL_RETURN_STORAGE_KEY, "jyotisha.session-url-return");
|
|
});
|
|
|
|
test("home stays a client-read query on a static route", () => {
|
|
const lib = readFileSync(new URL("../src/lib/chat-session-url.ts", import.meta.url), "utf8");
|
|
assert.doesNotMatch(page, /useSearchParams/);
|
|
assert.doesNotMatch(page, /export const dynamic/);
|
|
assert.match(page, /writeSessionUrl\(sessionId, "push"\)/);
|
|
assert.match(page, /window\.history\.replaceState/);
|
|
assert.match(lib, /history\.pushState\(null, "", next\)/);
|
|
assert.match(lib, /history\.replaceState\(null, "", next\)/);
|
|
const preview = sourceBetween(
|
|
page,
|
|
"if (previewMode) {",
|
|
"const [nextAccount, modelCatalogResult, sessionsPayload]",
|
|
);
|
|
assert.doesNotMatch(preview, /writeSessionUrl|persistLoginSessionReturn|readLoginSessionReturn/);
|
|
assert.match(page, /SESSION_MISSING_NOTICE/);
|
|
assert.equal(SESSION_MISSING_NOTICE, "该对话不存在或已被删除");
|
|
});
|
|
|
|
test("login return storage only accepts a UUID session id", () => {
|
|
const memory = new Map<string, string>();
|
|
const previousWindow = globalThis.window;
|
|
Object.defineProperty(globalThis, "window", {
|
|
configurable: true,
|
|
value: {
|
|
location: { search: `?${SESSION_URL_QUERY_KEY}=${sessionA}` },
|
|
},
|
|
});
|
|
Object.defineProperty(globalThis, "sessionStorage", {
|
|
configurable: true,
|
|
value: {
|
|
setItem(key: string, value: string) { memory.set(key, value); },
|
|
getItem(key: string) { return memory.get(key) ?? null; },
|
|
removeItem(key: string) { memory.delete(key); },
|
|
},
|
|
});
|
|
try {
|
|
persistLoginSessionReturn();
|
|
assert.equal(readLoginSessionReturn(), sessionA);
|
|
memory.set(SESSION_URL_RETURN_STORAGE_KEY, "not-a-uuid");
|
|
assert.equal(readLoginSessionReturn(), null);
|
|
} finally {
|
|
if (previousWindow === undefined) {
|
|
Reflect.deleteProperty(globalThis, "window");
|
|
} else {
|
|
Object.defineProperty(globalThis, "window", { configurable: true, value: previousWindow });
|
|
}
|
|
Reflect.deleteProperty(globalThis, "sessionStorage");
|
|
}
|
|
});
|
|
|
|
test("writeSessionUrl no-ops when the address is already the target", () => {
|
|
const calls: string[] = [];
|
|
const previousWindow = globalThis.window;
|
|
Object.defineProperty(globalThis, "window", {
|
|
configurable: true,
|
|
value: {
|
|
location: { pathname: "/", search: `?${SESSION_URL_QUERY_KEY}=${sessionA}` },
|
|
history: {
|
|
pushState(_state: unknown, _unused: string, url: string) { calls.push(`push:${url}`); },
|
|
replaceState(_state: unknown, _unused: string, url: string) { calls.push(`replace:${url}`); },
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
writeSessionUrl(sessionA, "push");
|
|
assert.deepEqual(calls, []);
|
|
writeSessionUrl(sessionB, "push");
|
|
assert.deepEqual(calls, [`push:/?${SESSION_URL_QUERY_KEY}=${sessionB}`]);
|
|
writeSessionUrl(null, "replace");
|
|
assert.deepEqual(calls, [
|
|
`push:/?${SESSION_URL_QUERY_KEY}=${sessionB}`,
|
|
"replace:/",
|
|
]);
|
|
} finally {
|
|
if (previousWindow === undefined) {
|
|
Reflect.deleteProperty(globalThis, "window");
|
|
} else {
|
|
Object.defineProperty(globalThis, "window", { configurable: true, value: previousWindow });
|
|
}
|
|
}
|
|
});
|