59559d4b24
Thinking disappeared on failure and never reached session storage. Keep the sanitized chain on disk and on errors, and regroup the sidebar around reports, charts, favorites, and dated history titles. Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
11 KiB
TypeScript
147 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
|
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 persists the optimistic user message before generation starts", () => {
|
|
const undo = sendSource.indexOf("await waitForUndoWindow(controller.signal)");
|
|
const persist = sendSource.indexOf("await persistSession(userSession)");
|
|
const consult = sendSource.indexOf('fetch("/api/consult"');
|
|
|
|
assert.ok(undo >= 0 && undo < persist && persist < consult);
|
|
assert.match(sendSource, /问题保存失败,未开始生成;问题已放回输入框。[\s\S]*?return false;[\s\S]*?fetch\("\/api\/consult"/);
|
|
});
|
|
|
|
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 persist = partialStop.indexOf("await persistSession(stoppedSession)");
|
|
const conflict = partialStop.indexOf("error.status === 409");
|
|
const recoveryPersist = partialStop.slice(conflict, persist);
|
|
|
|
assert.ok(cancel >= 0 && cancel < persist);
|
|
assert.ok(conflict >= 0 && conflict < persist);
|
|
assert.doesNotMatch(recoveryPersist, /persistSession\(stoppedSession\)/);
|
|
assert.match(recoveryPersist, /fetchConsultationStatus\(pending.sessionId, pending.requestId\)/);
|
|
assert.match(recoveryPersist, /status.status === "completed"[\s\S]*?fetchSessions\(\)[\s\S]*?readSessions\(payload, 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]*?fetchSessions\(controller\.signal\)[\s\S]*?readSessions\(payload, modelCatalog\)/);
|
|
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/);
|
|
assert.ok(sendSource.indexOf("await persistSession(userSession)") < sendSource.indexOf('fetch("/api/consult"'));
|
|
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/);
|
|
assert.match(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/);
|
|
});
|