Files
Jyotisha/frontend/tests/rectification-session-composer-guard.test.ts
T
jesse-ux db5b0a214b
Independent Staging Quality Gate / validate (push) Failing after 6m30s
Independent Staging Quality Gate / publish (push) Skipped
fix(web): 会话列表合同跟着外壳搬家,无活跃会话不再静默吞发送
四条源码合同改到 (app)/layout 与注册载荷两端对断。列表只剩校正会话时普通输入框锁定,发送会打开校正或给出提示。
2026-09-17 23:06:01 +08:00

140 lines
5.7 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { homeSurface as page } from "./home-surface.ts";
import {
composerLocksAsRectification,
consultSendBlockedByRectificationSession,
dropRectificationStoredPending,
fallbackSessionId,
missingActiveSessionSendAction,
RECTIFICATION_OPENING_COMPOSER_PLACEHOLDER,
RECTIFICATION_REOPEN_LABEL,
rectificationOrdinaryComposerReopen,
SESSION_NOT_CONSULTATION_CODE,
} from "../src/lib/rectification-session-composer-guard.ts";
const read = (relativePath: string) => readFileSync(new URL(relativePath, import.meta.url), "utf8");
const run = read("../src/hooks/use-consultation-run.ts");
const sessions = read("../src/hooks/use-session-management.ts");
const consultRoute = read("../src/app/api/consult/route.ts");
const pageSource = read("../src/app/(app)/page.tsx");
const sendFn = run.slice(run.indexOf("async function send("), run.indexOf('await fetch("/api/consult"'));
test("a rectification session blocks consult send and keeps the draft", () => {
assert.equal(consultSendBlockedByRectificationSession({ sessionType: "birth_time_rectification" }), true);
assert.equal(consultSendBlockedByRectificationSession({ sessionType: "consultation" }), false);
assert.match(sendFn, /consultSendBlockedByRectificationSession\(currentSession\)/);
assert.match(sendFn, /setDraft\(originalQuestion\)/);
assert.match(sendFn, /openRectificationSession\(currentSession\.id\)/);
assert.doesNotMatch(sendFn, /fetch\("\/api\/consult"/);
});
test("pending consult replay pointing at a rectification session is dropped", () => {
assert.equal(
dropRectificationStoredPending(
{ sessionId: "r1" },
[{ id: "r1", sessionType: "birth_time_rectification" }],
),
null,
);
assert.deepEqual(
dropRectificationStoredPending(
{ sessionId: "c1" },
[{ id: "c1", sessionType: "consultation" }],
),
{ sessionId: "c1" },
);
assert.match(run, /if \(consultSendBlockedByRectificationSession\(session\)\) \{[\s\S]*sessionStorage\.removeItem\(pendingConsultationStorageKey\)/);
});
test("consult POST splits missing session from wrong session type", () => {
assert.match(consultRoute, /if \(!chatSession\) \{[\s\S]*咨询会话不存在[\s\S]*status: 404/);
assert.match(
consultRoute,
/if \(chatSession\.session_type !== "consultation"\) \{[\s\S]*session_not_consultation[\s\S]*status: 409/,
);
assert.match(consultRoute, /这是生时校正会话/);
assert.match(consultRoute, /请在生时校正里继续。/);
assert.equal(SESSION_NOT_CONSULTATION_CODE, "session_not_consultation");
assert.match(run, /caught\.code === SESSION_NOT_CONSULTATION_CODE/);
});
test("ordinary composer is locked on a rectification session until the surface opens", () => {
assert.equal(RECTIFICATION_OPENING_COMPOSER_PLACEHOLDER, "正在打开生时校正…");
assert.equal(RECTIFICATION_REOPEN_LABEL, "重新打开生时校正");
assert.match(pageSource, /inputDisabled=\{[^}]*activeRectificationSession/);
assert.match(pageSource, /submitBlocked=\{[^}]*activeRectificationSession/);
assert.match(pageSource, /ordinaryComposerPlaceholder\(/);
assert.match(pageSource, /RectificationComposerReopen/);
assert.equal(
rectificationOrdinaryComposerReopen({
activeRectificationSession: true,
error: "生时校正会话暂时无法打开,请稍后重试。",
errorSessionId: "s1",
sessionId: "s1",
}),
true,
);
assert.equal(
rectificationOrdinaryComposerReopen({
activeRectificationSession: true,
error: "",
errorSessionId: null,
sessionId: "s1",
}),
false,
);
});
test("delete and popstate fallbacks do not setActiveSessionId onto a rectification session", () => {
assert.equal(
fallbackSessionId([
{ id: "r1", sessionType: "birth_time_rectification" },
{ id: "c1", sessionType: "consultation" },
]),
"c1",
);
assert.equal(
fallbackSessionId([{ id: "r1", sessionType: "birth_time_rectification" }]),
"r1",
);
assert.match(sessions, /function activateFallbackSession/);
assert.match(sessions, /if \(fallback\?\.sessionType === "birth_time_rectification"\) \{\s*selectSession\(fallbackId\);/);
assert.match(sessions, /if \(activeSessionId === session.id\) \{\s*activateFallbackSession\(nextSessions\);/);
assert.match(sessions, /const fallbackId = fallbackSessionId\(listed\);/);
assert.match(page, /sessions.find\(\(session\) => session.sessionType !== "birth_time_rectification"\)/);
});
test("an empty active session still locks the composer when the list is only rectification", () => {
assert.equal(
composerLocksAsRectification(undefined, [{ sessionType: "birth_time_rectification" }]),
true,
);
assert.equal(
composerLocksAsRectification(undefined, [{ sessionType: "consultation" }]),
false,
);
assert.equal(
composerLocksAsRectification({ sessionType: "consultation" }, [{ sessionType: "birth_time_rectification" }]),
false,
);
assert.deepEqual(
missingActiveSessionSendAction([{ id: "r1", sessionType: "birth_time_rectification" }]),
{ openSessionId: "r1" },
);
assert.deepEqual(
missingActiveSessionSendAction([]),
{ notice: "请先选一条对话,或点上方「新建对话」。" },
);
assert.match(pageSource, /composerLocksAsRectification\(activeSession, sessions\)/);
assert.match(sendFn, /missingActiveSessionSendAction\(sessions\)/);
assert.match(sendFn, /openRectificationSession\(action\.openSessionId\)/);
assert.match(sendFn, /setComposerNotice\(action\.notice\)/);
assert.doesNotMatch(
sendFn,
/if \(!question \|\| !currentSession \|\| !modelCatalog \|\| !account\) return false;/,
);
});