import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { createElement, createRef } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { ChatComposer } from "../src/components/chat-composer.tsx"; import { appendQueuedText, COMPOSER_QUEUE_LABEL, COMPOSER_QUEUE_RECALL_LABEL, queuedDraftSettleAction, } from "../src/lib/queued-draft.ts"; import { chatReplyAnnouncer } from "../src/lib/chat-reply-announcement.ts"; import { RECTIFICATION_STOPPED_NOTICE } from "../src/lib/rectification-surface-state.ts"; const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const composer = readFileSync(new URL("../src/components/chat-composer.tsx", import.meta.url), "utf8"); const rectification = readFileSync( new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8", ); const consultationRun = readFileSync( new URL("../src/hooks/use-consultation-run.ts", import.meta.url), "utf8", ); const messageRow = readFileSync( new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8", ); const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); test("queued drafts append onto one card and settle by outcome", () => { assert.equal(appendQueuedText("", " 先问事业 "), "先问事业"); assert.equal(appendQueuedText("先问事业", "再问婚姻"), "先问事业\n再问婚姻"); assert.equal(appendQueuedText("先问事业", " "), "先问事业"); assert.equal(queuedDraftSettleAction("completed"), "send"); assert.equal(queuedDraftSettleAction("succeeded"), "send"); assert.equal(queuedDraftSettleAction("stopped"), "restore"); assert.equal(queuedDraftSettleAction("failed"), "restore"); assert.equal(queuedDraftSettleAction("recovering"), "restore"); assert.equal(queuedDraftSettleAction("readonly"), "restore"); }); test("generating does not disable the textarea; Enter queues instead of dropping", () => { assert.match(composer, /disabled=\{inputDisabled\}/); assert.match(composer, /queued\?: \{ text: string; onRecall: \(\) => void \}/); assert.match(composer, /className="composer-queue"/); assert.match(composer, /COMPOSER_QUEUE_LABEL/); assert.match(composer, /COMPOSER_QUEUE_RECALL_LABEL/); assert.doesNotMatch(styles, /composer-queue[\s\S]{0,400}spinner|composer-queue[\s\S]{0,400}skeleton/i); assert.match( page, /inputDisabled=\{sessionMessagesLoading \|\| rectificationSurfaceOpen \|\| \(!profileComplete && \(onboardingStep !== "name" \|\| !presetMessageFinished \|\| profileSaving\)\)\}/, ); assert.doesNotMatch(page, /inputDisabled=\{isLoading \|\| sessionMessagesLoading \|\| cancellationPending/); assert.match(page, /if \(isLoading \|\| cancellationPending\) return void enqueueQueuedDraft\(composerDraftSnapshot\(\)\);/); assert.match(consultationRun, /queuedDraftSettleAction\(settlePhase\) === "send"/); assert.match(consultationRun, /function enqueueQueuedDraft/); assert.match(consultationRun, /composerInput\.current\?\.focus\(\)/); assert.match(rectification, /inputDisabled=\{readonly\}/); assert.doesNotMatch( rectification.slice(rectification.indexOf(" { const html = renderToStaticMarkup(createElement(ChatComposer, { inputRef: createRef(), value: "还想再问一句", inputLabel: "输入你的问题", placeholder: "例如:未来半年是否适合换工作?", maxLength: 500, inputDisabled: false, submitLabel: "发送", submitBlocked: true, stopVisible: true, stopLabel: "停止回答", stopTitle: "停止回答", queued: { text: "先问事业", onRecall() {} }, onSubmit() {}, onChange() {}, onKeyDown() {}, onStop() {}, })); assert.match(html, /composer-queue/); assert.match(html, new RegExp(COMPOSER_QUEUE_LABEL)); assert.match(html, /先问事业/); assert.match(html, new RegExp(COMPOSER_QUEUE_RECALL_LABEL)); assert.doesNotMatch(html, /]*\sdisabled=/); assert.match(html, /还想再问一句/); }); test("rectification abort settles as stopped, not a failed alert", () => { assert.match(rectification, /stopped: true/); assert.match(rectification, /failed: false,\s*stopped: true/); assert.doesNotMatch(rectification, /if \(raw\.trim\(\)\) setError\(RECTIFICATION_STOPPED_NOTICE\)/); assert.match(rectification, /stoppedNotice=\{message\.stopped \? RECTIFICATION_STOPPED_NOTICE : undefined\}/); assert.match(messageRow, /stoppedNotice/); assert.match(messageRow, /className="message-stopped-notice"/); assert.equal(RECTIFICATION_STOPPED_NOTICE, "已停止,已生成的内容保留;本次不会扣点。"); assert.match(rectification, /signal: abortController\.signal/); const choiceFetch = rectification.slice( rectification.indexOf("const submitStructuredChoice"), rectification.indexOf("const acceptCandidate"), ); assert.match(choiceFetch, /runAbort\.current = abortController/); assert.match(choiceFetch, /signal: abortController\.signal/); const adoptFetch = rectification.slice( rectification.indexOf("const acceptCandidate"), rectification.indexOf("async function copyMessage"), ); assert.match(adoptFetch, /runAbort\.current = abortController/); assert.match(adoptFetch, /signal: abortController\.signal/); assert.equal(chatReplyAnnouncer("stopped"), "chat_notice_toast"); assert.doesNotMatch( rectification.slice(rectification.indexOf("{error &&"), rectification.indexOf("{readonly &&")), /RECTIFICATION_STOPPED_NOTICE/, ); });