Move the home startup sequence and consultation recovery poll into plain functions, and the error and onboarding screens into components. Landing rules, the 20s fatal screen, retry, and the chart settings deep link stay the same.
129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { runConsultationRecoveryPoll, type ConsultationRecoveryPollDeps } from "../src/lib/consultation-recovery-poll.ts";
|
|
import { ConsultationStatusError } from "../src/lib/home-cloud-sync.ts";
|
|
import { previewModelCatalog } from "../src/lib/home-types.ts";
|
|
import { withFakeTimers } from "./home-bootstrap-fake-timers.ts";
|
|
|
|
const sessionId = "11111111-1111-4111-8111-111111111111";
|
|
const requestId = "22222222-2222-4222-8222-222222222222";
|
|
|
|
async function flush() {
|
|
for (let step = 0; step < 8; step += 1) await Promise.resolve();
|
|
}
|
|
|
|
function createPoll(overrides: {
|
|
fetchConsultationStatus: ConsultationRecoveryPollDeps["fetchConsultationStatus"];
|
|
missing?: number;
|
|
}) {
|
|
const state = {
|
|
notice: "",
|
|
phase: "recovering" as "recovering" | null,
|
|
pendingSession: sessionId as string | null,
|
|
pendingRequest: requestId as string | null,
|
|
replay: 0,
|
|
error: "",
|
|
refreshed: 0,
|
|
};
|
|
const deps: ConsultationRecoveryPollDeps = {
|
|
pendingSessionId: sessionId,
|
|
pendingRequestId: requestId,
|
|
modelCatalog: previewModelCatalog,
|
|
consultationStatusMissingCount: { current: overrides.missing ?? 0 },
|
|
consultationReplayStarted: { current: null },
|
|
consultationReplay: {
|
|
current: () => {
|
|
state.replay += 1;
|
|
deps.consultationReplayStarted.current = requestId;
|
|
},
|
|
},
|
|
pendingConsultation: { current: { phase: "recovering" } },
|
|
setComposerNotice: (notice) => { state.notice = notice; },
|
|
setSessions: () => undefined,
|
|
setActiveSessionId: () => undefined,
|
|
setPendingSessionId: (id) => { state.pendingSession = id; },
|
|
setPendingRequestId: (id) => { state.pendingRequest = id; },
|
|
setConsultationPhase: (phase) => { state.phase = phase === "recovering" ? "recovering" : null; },
|
|
setStreamingReply: () => undefined,
|
|
setRequestError: (value) => { state.error = value?.message ?? ""; },
|
|
refreshAccount: () => { state.refreshed += 1; },
|
|
online: () => true,
|
|
fetchConsultationStatus: overrides.fetchConsultationStatus,
|
|
fetchSessionDetail: async () => null,
|
|
};
|
|
return { deps, state };
|
|
}
|
|
|
|
test("the first 404 confirms the request and the next 404 replays it once", async () => {
|
|
await withFakeTimers(async (tick) => {
|
|
const { deps, state } = createPoll({
|
|
fetchConsultationStatus: async () => { throw new ConsultationStatusError(404, "missing"); },
|
|
});
|
|
runConsultationRecoveryPoll(deps, new AbortController().signal);
|
|
await flush();
|
|
assert.equal(deps.consultationStatusMissingCount.current, 1);
|
|
assert.equal(state.notice, "正在确认本次咨询请求是否已开始…");
|
|
assert.equal(state.replay, 0);
|
|
assert.equal(state.phase, "recovering");
|
|
|
|
tick(1_750);
|
|
await flush();
|
|
assert.equal(deps.consultationStatusMissingCount.current, 2);
|
|
assert.equal(state.replay, 1);
|
|
assert.equal(state.phase, "recovering");
|
|
});
|
|
});
|
|
|
|
test("the fourth missing status gives up and stops polling", async () => {
|
|
await withFakeTimers(async (tick) => {
|
|
const { deps, state } = createPoll({
|
|
fetchConsultationStatus: async () => { throw new ConsultationStatusError(404, "missing"); },
|
|
});
|
|
runConsultationRecoveryPoll(deps, new AbortController().signal);
|
|
await flush();
|
|
tick(1_750);
|
|
await flush();
|
|
tick(1_750);
|
|
await flush();
|
|
assert.equal(deps.consultationStatusMissingCount.current, 3);
|
|
assert.equal(state.notice, "正在重新发起本次咨询…");
|
|
assert.equal(state.replay, 1);
|
|
|
|
tick(1_750);
|
|
await flush();
|
|
assert.equal(deps.consultationStatusMissingCount.current, 4);
|
|
assert.equal(state.pendingSession, null);
|
|
assert.equal(state.pendingRequest, null);
|
|
assert.equal(state.phase, null);
|
|
assert.equal(state.error, "后台未找到本次咨询请求,请重新发送。");
|
|
assert.equal(state.notice, "后台未找到本次咨询请求,已停止恢复,请重新发送。");
|
|
const timersBefore = state.notice;
|
|
tick(1_750);
|
|
await flush();
|
|
assert.equal(state.notice, timersBefore);
|
|
assert.equal(deps.consultationStatusMissingCount.current, 4);
|
|
});
|
|
});
|
|
|
|
test("a reserved status clears the missing count", async () => {
|
|
await withFakeTimers(async (tick) => {
|
|
const { deps, state } = createPoll({
|
|
missing: 2,
|
|
fetchConsultationStatus: async () => ({
|
|
sessionId,
|
|
requestId,
|
|
status: "reserved",
|
|
}),
|
|
});
|
|
runConsultationRecoveryPoll(deps, new AbortController().signal);
|
|
await flush();
|
|
assert.equal(deps.consultationStatusMissingCount.current, 2);
|
|
tick(1_750);
|
|
await flush();
|
|
assert.equal(deps.consultationStatusMissingCount.current, 0);
|
|
assert.equal(state.notice, "回答仍在后台生成,正在自动恢复。");
|
|
assert.equal(state.phase, "recovering");
|
|
});
|
|
});
|