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>
154 lines
11 KiB
TypeScript
154 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { homeSurface as source } from "./home-surface.ts";
|
|
const sendSource = source.slice(source.indexOf(" async function send("), source.indexOf("\n\n consultationReplay.current"));
|
|
const stopSource = source.slice(source.indexOf(" async function stopResponse("), source.indexOf("\n\n function completeConsultationInterface"));
|
|
|
|
test("consultation does not persist the optimistic user message before generation starts", () => {
|
|
// Former value: `await persistSession(userSession)` ran after the undo window and
|
|
// before `/api/consult`, locking the client full-replace write. The server now
|
|
// appends the question after a successful reserve.
|
|
const undo = sendSource.indexOf("await waitForUndoWindow(controller.signal)");
|
|
const consult = sendSource.indexOf('fetch("/api/consult"');
|
|
|
|
assert.ok(undo >= 0 && undo < consult);
|
|
assert.equal(sendSource.indexOf("await persistSession(userSession)"), -1);
|
|
assert.doesNotMatch(sendSource, /问题保存失败,未开始生成;问题已放回输入框。/);
|
|
});
|
|
|
|
test("only explicit stop can request consultation cancellation", () => {
|
|
const streamCatch = sendSource.indexOf(" } catch (caught) {", sendSource.indexOf('fetch("/api/consult"'));
|
|
|
|
assert.equal(source.match(/confirmCancellation\(/g)?.length, 2);
|
|
assert.match(stopSource, /await confirmCancellation\(/);
|
|
assert.ok(streamCatch >= 0);
|
|
assert.doesNotMatch(sendSource.slice(streamCatch), /confirmCancellation\(|\/api\/consult\/cancel/);
|
|
});
|
|
|
|
test("durable partial stop cancels before preserving content and recovers completed conflicts", () => {
|
|
const partialStop = stopSource.slice(
|
|
stopSource.indexOf(" if (pending.partialReply)"),
|
|
stopSource.indexOf("\n updateSession(pending.sessionId, () => pending.previousSession)"),
|
|
);
|
|
const cancel = partialStop.indexOf("await requestCancellation(pending.requestId)");
|
|
const conflict = partialStop.indexOf("error.status === 409");
|
|
const recoveryPersist = partialStop.slice(conflict);
|
|
|
|
assert.ok(cancel >= 0 && conflict > cancel);
|
|
// Former value: persistSession(stoppedSession) after cancel. Refunded partial
|
|
// answers stay in page memory only and must not overwrite the server transcript.
|
|
assert.equal(partialStop.indexOf("await persistSession(stoppedSession)"), -1);
|
|
assert.doesNotMatch(recoveryPersist, /persistSession\(stoppedSession\)/);
|
|
assert.match(recoveryPersist, /fetchConsultationStatus\(pending.sessionId, pending.requestId\)/);
|
|
assert.match(recoveryPersist, /status.status === "completed"[\s\S]*?fetchSessionDetail\(pending.sessionId, modelCatalog\)/);
|
|
assert.match(partialStop, /已停止回答,现有内容已保留,本次点数已退回。/);
|
|
assert.match(source, /停止回答,保留已生成内容并退回本次点数/);
|
|
assert.match(source, /停止回答,保留现有内容并申请退回本次点数/);
|
|
});
|
|
|
|
test("explicit consultation HTTP failures unlock instead of entering recovery", () => {
|
|
assert.match(sendSource, /!response\.ok[\s\S]*throw new ConsultationResponseError\([\s\S]*response\.status/);
|
|
const explicitFailure = sendSource.slice(
|
|
sendSource.indexOf("caught instanceof ConsultationResponseError"),
|
|
sendSource.indexOf("if (!cancelled && ownsInterface && pendingConsultation.current)"),
|
|
);
|
|
assert.match(explicitFailure, /caught\.message === "request_conflict"[\s\S]*setConsultationPhase\("recovering"\)/);
|
|
const genericFailure = explicitFailure.slice(explicitFailure.indexOf("setRequestError({ sessionId, message: caught.message })"));
|
|
assert.match(genericFailure, /setRequestError\(\{ sessionId, message: caught\.message \}\)/);
|
|
assert.match(genericFailure, /completeConsultationInterface\(requestId\)/);
|
|
assert.doesNotMatch(genericFailure, /phase: "recovering"|setConsultationPhase\("recovering"\)/);
|
|
});
|
|
|
|
test("reserved consultations recover through the status endpoint", () => {
|
|
assert.match(source, /\/api\/consult\/status\?sessionId=\$\{encodeURIComponent\(sessionId\)\}&requestId=\$\{encodeURIComponent\(requestId\)\}/);
|
|
assert.match(source, /fetch\("\/api\/consult\/status", \{ signal, cache: "no-store" \}\)/);
|
|
assert.match(source, /reservedConsultation\?\.status === "reserved"[\s\S]*nextSessions\.find\(\(session\) => session\.id === reservedConsultation\.sessionId\)/);
|
|
assert.match(source, /status\?\.status !== "reserved"[\s\S]*sessions\.find\(\(item\) => item\.id === status\.sessionId\)/);
|
|
assert.match(source, /status: "reserved" \| "completed" \| "cancelled"/);
|
|
assert.match(source, /readonly responseMessage\?: unknown/);
|
|
assert.match(source, /phase: "recovering"/);
|
|
assert.match(source, /window\.setTimeout\(\(\) => void poll\(\), 1_750\)/);
|
|
assert.match(source, /status\.status === "completed"[\s\S]*?fetchSessionDetail\(pendingSessionId, modelCatalog, controller\.signal\)/);
|
|
assert.match(source, /window\.addEventListener\("online", onOnline\)/);
|
|
assert.match(source, /window\.addEventListener\("pageshow", onPageShow\)/);
|
|
assert.match(source, /网络已断开,回答仍在后台生成;联网后会自动恢复。/);
|
|
});
|
|
|
|
test("a missing reservation is replayed once instead of being polled until the question is lost", () => {
|
|
assert.match(source, /class ConsultationStatusError extends Error[\s\S]*readonly status: number/);
|
|
assert.match(source, /throw new ConsultationStatusError\([\s\S]*response\.status/);
|
|
const recoveryEffect = source.slice(
|
|
source.indexOf('if (consultationPhase !== "recovering"'),
|
|
source.indexOf("}, [consultationPhase, modelCatalog, pendingRequestId, pendingSessionId])"),
|
|
);
|
|
assert.match(source, /const consultationStatusMissingCount = useRef\(0\)/);
|
|
assert.match(source, /const consultationReplayStarted = useRef<string \| null>\(null\)/);
|
|
assert.match(recoveryEffect, /status\.status === "reserved"[\s\S]*consultationStatusMissingCount\.current = 0/);
|
|
assert.match(recoveryEffect, /caught instanceof ConsultationStatusError && caught\.status === 404[\s\S]*consultationStatusMissingCount\.current \+= 1/);
|
|
assert.match(recoveryEffect, /consultationStatusMissingCount\.current === 1[\s\S]*正在确认本次咨询请求是否已开始/);
|
|
assert.match(recoveryEffect, /consultationReplayStarted\.current !== pendingRequestId[\s\S]*consultationReplay\.current\(\)/);
|
|
assert.match(recoveryEffect, /consultationStatusMissingCount\.current >= 4[\s\S]*setPendingSessionId\(null\)[\s\S]*setPendingRequestId\(null\)[\s\S]*setConsultationPhase\(null\)/);
|
|
assert.match(recoveryEffect, /后台未找到本次咨询请求,已停止恢复,请重新发送。/);
|
|
assert.match(recoveryEffect, /consultationStatusMissingCount\.current = 0;[\s\S]*回答仍在后台生成,正在自动恢复。/);
|
|
assert.match(recoveryEffect, /consultationStatusMissingCount\.current > 0[\s\S]*window\.setTimeout\(\(\) => void poll\(\), 1_750\)[\s\S]*else \{[\s\S]*void poll\(\)/);
|
|
});
|
|
|
|
test("tab-local pending ids drive strict bootstrap recovery before the global fallback", () => {
|
|
const bootstrap = source.slice(
|
|
source.indexOf("let reservedConsultation: ConsultationStatus | null = null"),
|
|
source.indexOf("if (controller.signal.aborted) return;", source.indexOf("let reservedConsultation: ConsultationStatus | null = null")),
|
|
);
|
|
const storageSync = source.slice(
|
|
source.indexOf("if (!hydrated || uiPreview.current) return;", source.indexOf("}, []);")),
|
|
source.indexOf('if (consultationPhase !== "recovering"'),
|
|
);
|
|
|
|
assert.match(bootstrap, /readStoredPendingConsultation\([\s\S]*pendingConsultationStorageKey[\s\S]*nextSessions\.map\(\(session\) => session\.id\)/);
|
|
assert.match(bootstrap, /if \(!storedPending && sessionStorage\.getItem\(pendingConsultationStorageKey\)\) \{\s*sessionStorage\.removeItem\(pendingConsultationStorageKey\)/);
|
|
assert.ok(bootstrap.indexOf("if (storedPending)") < bootstrap.indexOf("fetchActiveConsultationStatus(controller.signal)"));
|
|
assert.match(bootstrap, /if \(storedPending\) \{[\s\S]*fetchConsultationStatus\([\s\S]*storedPending\.sessionId,[\s\S]*storedPending\.requestId,[\s\S]*\} else \{[\s\S]*fetchActiveConsultationStatus/);
|
|
assert.match(bootstrap, /status\.status === "reserved"[\s\S]*reservedConsultation = status;[\s\S]*else \{[\s\S]*sessionStorage\.removeItem\(pendingConsultationStorageKey\)/);
|
|
assert.match(bootstrap, /caught instanceof ConsultationStatusError && caught\.status === 404 \? 1 : 0/);
|
|
assert.match(storageSync, /pendingSessionId && pendingRequestId[\s\S]*sessionStorage\.setItem\(pendingConsultationStorageKey,[\s\S]*sessionId: pendingSessionId,[\s\S]*requestId: pendingRequestId,[\s\S]*question:/);
|
|
assert.match(storageSync, /else \{[\s\S]*sessionStorage\.removeItem\(pendingConsultationStorageKey\)/);
|
|
});
|
|
|
|
test("pending storage keeps the question so a refresh during the undo window can replay", () => {
|
|
assert.match(source, /function readStoredPendingConsultation\(/);
|
|
assert.match(source, /typeof parsedPending\.question === "string" \? parsedPending\.question : ""/);
|
|
assert.match(sendSource, /sessionStorage\.setItem\(pendingConsultationStorageKey, JSON\.stringify\(\{[\s\S]*sessionId,[\s\S]*requestId,[\s\S]*question: originalQuestion/);
|
|
assert.match(source, /restoreConsultationRecovery\(recoverySession, reservedConsultation\.requestId, storedPending\)/);
|
|
assert.match(source, /stored\?\.question\?\.trim\(\)/);
|
|
assert.match(sendSource, /resumeRequestId/);
|
|
assert.match(sendSource, /questionAlreadyPresent/);
|
|
assert.match(source, /consultationReplay\.current = \(\) => \{[\s\S]*resumeRequestId: pending\.requestId/);
|
|
});
|
|
|
|
test("the first default consultation title is persisted with the user question", () => {
|
|
const userSessionBlock = sendSource.slice(
|
|
sendSource.indexOf("const userSession: ChatSession"),
|
|
sendSource.indexOf("const requestId = resumeRequestId ?? globalThis.crypto.randomUUID()"),
|
|
);
|
|
assert.match(userSessionBlock, /currentSession\.messages\.length === 0 && isGenericSessionTitle\(currentSession\.title\)[\s\S]*resolveSessionTitle\(question/);
|
|
// Former value: persistSession(userSession) before consult wrote the title by
|
|
// replacing the whole messages array. Title now comes from SQL append, and the
|
|
// completed metadata patch only updates title/theme/model/chart binding.
|
|
assert.equal(sendSource.indexOf("await persistSession(userSession)"), -1);
|
|
assert.match(sendSource, /await persistSession\(completedSession\)/);
|
|
assert.match(sendSource, /const completedTitle = reply\.title && !isGenericSessionTitle\(reply\.title\)/);
|
|
assert.match(sendSource, /resolveSessionTitle\(question, reply\.title/);
|
|
});
|
|
|
|
test("a truncated generation keeps the partial answer and does not wait for a successful run", () => {
|
|
const stream = sendSource.slice(sendSource.indexOf('fetch("/api/consult"'));
|
|
assert.match(stream, /event\.code === "answer_truncated"/);
|
|
assert.match(stream, /truncatedFailure = event/);
|
|
assert.match(stream, /const truncatedSession: ChatSession = \{[\s\S]*role: "assistant"[\s\S]*text: reply\.text/);
|
|
// Former value: persistSession(truncatedSession) saved a refunded partial answer.
|
|
assert.doesNotMatch(stream, /await persistSession\(truncatedSession\)/);
|
|
assert.match(stream, /setComposerNotice\(truncatedFailure\.message\)/);
|
|
assert.match(stream, /if \(!runCompleted && !truncatedFailure\) \{\s*throw new ConsultationResponseError/);
|
|
assert.doesNotMatch(stream.slice(stream.indexOf("if (truncatedFailure)")), /runCompleted = true/);
|
|
});
|