Files
Jyotisha/frontend/tests/rectification-agentic-entry.test.ts
T
Jesse_Chen 4e247c112e fix(web): keep thinking off the spoken consult and rectification answer
Enumerate evidence kinds so education cannot be proposed as a kind, and stream Chinese thinking on a separate channel that collapses when the reply arrives.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 21:30:55 +08:00

629 lines
32 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const component = readFileSync(
new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url),
"utf8",
);
const chat = readFileSync(
new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url),
"utf8",
);
const messageRow = readFileSync(
new URL("../src/components/chat-message-row.tsx", import.meta.url),
"utf8",
);
const messageActions = readFileSync(
new URL("../src/components/chat-message-actions.tsx", import.meta.url),
"utf8",
);
const route = readFileSync(
new URL("../src/app/api/rectification/agent/route.ts", import.meta.url),
"utf8",
);
const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const agent = readFileSync(
new URL("../src/mastra/agentic-rectification.ts", import.meta.url),
"utf8",
);
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const board = readFileSync(
new URL("../src/components/rectification-board.tsx", import.meta.url),
"utf8",
);
const boardModel = readFileSync(
new URL("../src/lib/rectification-board-model.ts", import.meta.url),
"utf8",
);
const houseTable = readFileSync(
new URL("../src/components/rectification-house-table.tsx", import.meta.url),
"utf8",
);
const progressLabels = readFileSync(
new URL("../src/lib/rectification-activity-labels.ts", import.meta.url),
"utf8",
);
const completedActivityReceipt = readFileSync(
new URL("../src/components/completed-activity-receipt.tsx", import.meta.url),
"utf8",
);
const choiceCardComponent = readFileSync(
new URL("../src/components/rectification-choice-card.tsx", import.meta.url),
"utf8",
);
const caseRoute = readFileSync(
new URL("../src/app/api/rectification/cases/[caseId]/route.ts", import.meta.url),
"utf8",
);
const adoptSkillRoute = readFileSync(
new URL("../src/app/api/rectification/cases/[caseId]/adopt-skill/route.ts", import.meta.url),
"utf8",
);
const upgradeSkillRoute = readFileSync(
new URL("../src/app/api/rectification/cases/[caseId]/upgrade-skill/route.ts", import.meta.url),
"utf8",
);
const regenerateRoute = readFileSync(
new URL(
"../src/app/api/rectification/cases/[caseId]/turns/[turnId]/regenerate/route.ts",
import.meta.url,
),
"utf8",
);
test("birth-time rectification entry mounts the V9 case-ref chat", () => {
assert.match(component, /return <RectificationAgenticChat \{\.\.\.props\} \/>/);
assert.match(component, /caseId: string;/);
assert.match(component, /readonly: boolean;/);
assert.match(component, /shouldStartOpening: boolean;/);
assert.match(component, /initialTurns:/);
});
test("persisted rectification turns hydrate after the async Case refresh", () => {
assert.match(chat, /function messagesFromTurns\(initialTurns:/);
assert.match(chat, /useState<RenderMessage\[\]>\(\(\) => messagesFromTurns\(initialTurns\)\)/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.at\(-1\)\?\.id \?\? "loading"\}`\}/);
assert.match(page, /methods: Array\.isArray\(\(turn\.receipt as \{ methods\?: unknown \}\)\.methods\)/);
assert.match(component, /methods\?: readonly string\[\]/);
});
test("opening is server-owned: shouldStartOpening drives the first turn, never client history", () => {
assert.doesNotMatch(chat, /initialMessages\.length > 0 \|\| openingStarted/);
assert.doesNotMatch(chat, /agenticOpeningInstruction|用户刚进入生时校正会话/);
assert.match(chat, /shouldStartOpening/);
assert.match(chat, /if \(readonly \|\| openingStarted\.current \|\| !shouldStartOpening\) return/);
assert.match(chat, /void send\("opening", ""\)/);
assert.match(route, /action: z\.enum\(\["opening", "message", "read_only"\]\)/);
assert.match(page, /shouldStartOpening=\{rectificationShouldStartOpening\}/);
assert.match(page, /setRectificationShouldStartOpening\(opened\.shouldStartOpening\)/);
});
test("incomplete profiles stay in the shared onboarding flow before any open request", () => {
const opening = page.slice(
page.indexOf("async function openRectificationCase"),
page.indexOf("async function openRectificationFromHomepage"),
);
assert.match(opening, /const missingStep = missingProfileStep\(profile\)/);
assert.match(opening, /setOnboardingStep\(missingStep\)/);
assert.ok(
opening.indexOf("const missingStep = missingProfileStep(profile)")
< opening.indexOf("/api/rectification/cases/open"),
);
assert.match(chat, /payload\?\.code === "profile_incomplete"/);
});
test("server profile failures pause automatic rectification resume", () => {
const resumeEffect = page.slice(
page.indexOf('useEffect(() => {\n if (!hydrated'),
page.indexOf('useEffect(() => {\n if (!hydrated || !accountId'),
);
const incompleteHandler = page.slice(
page.indexOf("function handleRectificationProfileIncomplete"),
page.indexOf("async function draftSynastryQuestionFromChart"),
);
assert.match(resumeEffect, /\|\| rectificationError\) return/);
assert.match(incompleteHandler, /setRectificationError\("profile_incomplete"\)/);
assert.ok(
incompleteHandler.indexOf('setRectificationError("profile_incomplete")')
< incompleteHandler.indexOf("setRectificationSessionId(null)"),
);
});
test("homepage rectification open failures always surface a visible recovery message", () => {
const opening = page.slice(
page.indexOf("async function openRectificationCase"),
page.indexOf("async function openRectificationFromHomepage"),
);
assert.match(
opening,
/code === "profile_incomplete"[\s\S]*?handleRectificationProfileIncomplete\(\)/,
);
assert.match(
opening,
/code === "invalid_open_request"[\s\S]*?setRectificationError\(/,
);
assert.match(
opening,
/catch \{[\s\S]*?setRectificationError\("生时校正会话暂时无法打开,请稍后重试。"\)/,
);
assert.match(
page,
/rectificationError && !rectificationSurfaceOpen && \([\s\S]*?role="alert"[\s\S]*?rectificationErrorMessage/,
);
});
test("account rehydration normalizes persisted ISO birth dates before completeness checks", () => {
const profileReader = page.slice(
page.indexOf("function readProfile"),
page.indexOf("function readSessions"),
);
assert.match(profileReader, /const date = normalizePersistedBirthDate\(/);
});
test("candidate acceptance refreshes the profile result without overwriting an open draft", () => {
const refresh = page.slice(
page.indexOf("async function refreshAccount"),
page.indexOf("function updateSession"),
);
assert.match(refresh, /const nextProfile = readProfile\(latest\.profile\)/);
assert.match(
refresh,
/setProfile\(\(current\) => preserveShallowEqual\(current, nextProfile\)\)/,
);
assert.doesNotMatch(refresh, /setProfileDraft/);
});
test("agent tool calls never end silently; the runner owns completion and failure", () => {
assert.doesNotMatch(route, /agenticRectificationMaxSteps = 8/);
assert.match(agent, /RECTIFICATION_AGENT_STEP_BUDGETS/);
assert.match(agent, /RECTIFICATION_AGENT_HARD_STEP_LIMIT/);
assert.match(route, /send\(\{ type: "done", emitted: true \}\)/);
assert.doesNotMatch(route, /send\(\{ type: "done", emitted: false \}\)/);
});
test("persisted turns survive remounts; duplicate openings are suppressed by the server", () => {
assert.match(chat, /initialTurns/);
assert.match(chat, /const openingStarted = useRef\(false\)/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.at\(-1\)\?\.id \?\? "loading"\}`\}/);
assert.match(page, /initialTurns=\{rectificationTurns\}/);
assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/);
});
test("the agent route verifies the exact Case/Session binding before any turn", () => {
assert.match(route, /caseId: z\.string\(\)\.uuid\(\)/);
assert.match(route, /sessionId: z\.string\(\)\.uuid\(\)/);
assert.match(route, /requestId: z\.string\(\)\.uuid\(\)/);
assert.match(route, /boundSessionId !== sessionId/);
assert.match(route, /chatSession\.agentic_rectification_case_id !== caseId/);
assert.doesNotMatch(route, /conversation\.action === "opening" && persistedMessages\.length > 0/);
assert.doesNotMatch(route, /\.update\(\{ messages: nextMessages, updated_at:/);
});
test("candidate results restore through the durable Candidate Snapshot API, never agent text", () => {
assert.match(chat, /\/api\/rectification\/cases\/\$\{encodeURIComponent\(caseId\)\}\?sessionId=/);
assert.doesNotMatch(chat, /savedSentinel|AYANAM_RECTIFICATION_SAVED|hidden block/);
assert.doesNotMatch(chat, /<!--AYANAM_SUGGESTIONS/);
});
test("candidate acceptance is non-billable and happens through the durable case endpoint", () => {
assert.match(chat, /\/candidates\/accept/);
assert.match(chat, /resultId: candidateResult\.resultId/);
assert.doesNotMatch(chat, /action: "accept_candidate"/);
const acceptRoute = readFileSync(
new URL("../src/app/api/rectification/cases/[caseId]/candidates/accept/route.ts", import.meta.url),
"utf8",
);
assert.match(acceptRoute, /accept_agentic_rectification_candidate_for_case/);
assert.doesNotMatch(acceptRoute, /authorizeUsage|completeUsage|begin_consultation_credit/);
});
test("usage completes or releases without hiding settlement failures", () => {
const run = readFileSync(
new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url),
"utf8",
);
assert.match(run, /billing\.complete\(/);
assert.match(run, /billing\.release\(/);
assert.match(run, /usage_settlement_failed/);
assert.match(run, /thinking: "enabled"/);
assert.match(run, /toPublicThinkingDelta/);
assert.match(route, /featureKey: "rectification"/);
assert.match(route, /rectification:case:\$\{caseId\}/);
});
test("opening and message retries reuse the caller-owned request id as the usage event key", () => {
assert.match(route, /requestId,\s*action\s*\} = parsed\.data/);
assert.match(route, /eventKey: requestId/);
assert.doesNotMatch(route, /eventKey:\s*(?:globalThis\.)?crypto\.randomUUID\(\)/);
});
test("rectification uses one case-level entitlement and the session-pinned model version", () => {
assert.match(route, /select\("id,messages,session_type,model_id,model_config_version,agentic_rectification_case_id"\)/);
assert.match(route, /resolveSessionLanguageModel\(\s*chatSession\.model_id,\s*chatSession\.model_config_version,?\s*\)/);
assert.match(route, /const billingRequestPrefix = `rectification:case:\$\{caseId\}`/);
assert.match(route, /requestId: billingRequestId/);
assert.match(route, /modelConfigVersion: selectedModel\.configVersion/);
assert.match(route, /generationModel: selectedModel\.model/);
assert.doesNotMatch(route, /loadLanguageModelCatalog|resolveLanguageModelFromCatalog|\bresolveLanguageModel\(|\bdefaultLanguageModel\(/);
});
test("Agentic rectification scrolls the conversation container as streamed messages grow", () => {
assert.match(chat, /const conversation = useRef<HTMLElement>\(null\)/);
assert.match(chat, /<section ref=\{conversation\} className="conversation is-rectification"/);
assert.match(chat, /const container = conversation\.current/);
assert.match(chat, /top: container\.scrollHeight/);
assert.match(chat, /\}, \[busy, candidateResult, choiceCard, error, messages, savedTime\]\);/);
assert.doesNotMatch(chat, /conversationEnd|scrollIntoView/);
});
test("rectification keeps receipts for the varga sentence and shows live tool progress", () => {
const activityHelper = chat.slice(
chat.indexOf("function completedReceiptFromPersisted"),
chat.indexOf("export function RectificationAgenticChat"),
);
assert.match(activityHelper, /receipt\.tools/);
assert.doesNotMatch(activityHelper, /receipt\.phases/);
assert.match(activityHelper, /filter\(isPublicRectificationTool\)/);
assert.match(activityHelper, /receipt\.methods/);
assert.match(activityHelper, /filter\(isPublicRectificationMethod\)/);
assert.match(chat, /completedReceipt\?: CompletedActivityReceiptView/);
assert.match(chat, /showActivity=\{displayedMessage\.state !== "settled"\}/);
assert.match(chat, /vargaSentenceFromMethods/);
assert.match(chat, /vargaSentence=\{vargaSentence\}/);
assert.match(board, /RectificationHouseTableView/);
assert.doesNotMatch(chat, /activeActivity/);
assert.match(chat, /RECTIFICATION_TOOL_PROGRESS_LABELS/);
assert.match(chat, /rectificationCompletedTrail\(activityReceiptState\.completedSteps\)/);
assert.match(progressLabels, /正在读取校正记录/);
assert.match(progressLabels, /正在比较候选时间/);
assert.doesNotMatch(chat, /<CompletedActivityReceipt/);
assert.match(chat, /event\.type === "tool\.activity"/);
assert.match(chat, /event\.status === "started"/);
assert.match(chat, /event\.status === "completed"/);
assert.match(chat, /event\.status !== "completed" && event\.status !== "failed"/);
const messageRender = chat.slice(
chat.indexOf("{messages.map((message) => {"),
chat.indexOf("{savedTime &&"),
);
const replyIndex = messageRender.indexOf("<ChatMessageRow");
assert.ok(replyIndex >= 0);
assert.doesNotMatch(messageRender, /<RectificationHouseTableView/);
assert.match(completedActivityReceipt, /<details className=/);
assert.doesNotMatch(completedActivityReceipt, /<details[^>]*\sopen/);
assert.match(completedActivityReceipt, /本轮完成 · \$\{stepLabels\.length\} 个步骤 · \$\{receipt\.methods\.length\} 项计算依据/);
for (const genericCopy of ["开始本轮执行", "正在加载专用方法", "专用方法已加载", "本轮做了什么"]) {
assert.doesNotMatch(chat, new RegExp(genericCopy));
}
assert.match(chat, /回答未完成,已保留现有内容;本次不会扣点/);
assert.doesNotMatch(chat, /reasoning-delta|chain-of-thought/);
assert.match(chat, /event.type === "thinking.delta"/);
assert.match(messageRow, /className="message-thinking"/);
assert.match(messageRow, /思考过程/);
assert.match(messageRow, /userOpen \?\? !hasAnswer/);
assert.match(styles, /\.message-thinking-body/);
});
test("completed Agent replies restore feedback, copy and safe in-place regeneration actions", () => {
for (const label of ["赞", "踩", "复制回答", "重新生成回答"]) {
assert.match(messageActions, new RegExp(`aria-label="${label}"`));
}
assert.match(chat, /<ChatMessageActions/);
assert.match(chat, /toggleRectificationFeedback/);
assert.match(chat, /navigator\.clipboard\.writeText\(message\.text\)/);
assert.match(chat, /\/turns\/\$\{encodeURIComponent\(message\.turnId\)\}\/regenerate/);
assert.match(chat, /requestId: globalThis\.crypto\.randomUUID\(\)/);
const messageRender = chat.slice(
chat.indexOf("{messages.map((message) => {"),
chat.indexOf("{savedTime &&"),
);
const replyIndex = messageRender.indexOf("<ChatMessageRow");
const actionsIndex = messageRender.indexOf("<ChatMessageActions");
assert.ok(replyIndex >= 0 && actionsIndex >= 0);
assert.ok(replyIndex < actionsIndex);
assert.doesNotMatch(messageRender, /<CompletedActivityReceipt/);
assert.doesNotMatch(chat, /send\("message",\s*(?:oldText|previousText)/);
assert.doesNotMatch(regenerateRoute, /consultation-billing|reserveConsultation|billing\./);
assert.match(regenerateRoute, /loadV9CaseSkillIdentity\(accounting, user\.id, caseId\)/);
assert.match(regenerateRoute, /resolveExactSkillPackage\(/);
assert.match(regenerateRoute, /skillPackage\.sourceCommit !== boundIdentity\.sourceCommit/);
assert.match(regenerateRoute, /getRectificationV9RegenerationAgent\([\s\S]*skillPackage\)/);
assert.match(regenerateRoute, /regenerateV9AssistantTurn/);
assert.match(regenerateRoute, /skillPackage,/);
});
test("candidate state renders from the snapshot API and never from sentinels", () => {
assert.match(chat, /当前可能的出生时间/);
assert.match(chat, /还不能确认唯一分钟/);
assert.match(chat, /可以先采用,也可以继续补充事件或改选/);
assert.match(chat, /相对支持度不是统计概率/);
assert.match(chat, /改选为此时间/);
assert.doesNotMatch(chat, /disabled=\{Boolean\(candidateResult\.selectedTime\)/);
assert.match(styles, /\.rectification-candidate-list \{[\s\S]*grid-template-columns: repeat\(3, minmax\(0, 1fr\)\)/);
assert.match(styles, /\.rectification-candidate-list \{[\s\S]*min-width: 0/);
assert.doesNotMatch(chat, /relativeSupport}\%/);
assert.match(chat, /相对支持度 {candidate.relativeSupport}/);
assert.match(chat, /isRecommendedRectificationCandidate/);
assert.match(chat, /已采用/);
assert.match(chat, /正在采用…/);
assert.match(chat, /RectificationBoard/);
assert.match(board, /className="rectification-snapshot"/);
assert.match(styles, /\.rectification-snapshot \{/);
assert.match(styles, /\.rectification-house-table \{[^}]*border:\s*1px solid var\(--color-border\)/);
assert.match(styles, /\.rectification-house-table \{[^}]*background:\s*var\(--color-canvas-soft\)/);
assert.match(styles, /\.message-list \{[\s\S]*--assistant-content-inset: calc\(32px \+ var\(--space-3\)\)/);
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*width: calc\(100% - var\(--assistant-content-inset\)\)/);
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/);
assert.match(styles, /\.rectification-workspace \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) minmax\(18rem, 22\.5rem\)/);
assert.match(styles, /\.rectification-workspace\.is-compact \{[\s\S]*grid-template-columns: minmax\(0, 1fr\)/);
assert.match(chat, /matchMedia\(`\(max-width: \$\{RECTIFICATION_BOARD_SPLIT_MIN_PX - 1\}px\)`\)/);
assert.match(chat, /nextCompact = query\.matches/);
assert.doesNotMatch(chat, /clientWidth < RECTIFICATION_BOARD_SPLIT_MIN_PX/);
assert.match(boardModel, /RECTIFICATION_BOARD_SPLIT_MIN_PX = 768/);
assert.match(board, /if \(!compact\) return sheet;/);
assert.doesNotMatch(styles, /\.rectification-snapshot \{[^}]*--assistant-content-inset/);
assert.doesNotMatch(styles, /\.rectification-house-table \{[^}]*padding-inline-start:\s*var\(--assistant-content-inset\)/);
assert.doesNotMatch(styles, /\.rectification-house-table \{[^}]*--assistant-content-inset/);
assert.doesNotMatch(page, /rectification-workspace/);
assert.match(page, /chat-panel\$\{rectificationSurfaceOpen \? " is-rectification" : ""\}/);
});
test("the natal house table is a live board beside the chat, not a message", () => {
const messageLoop = chat.slice(
chat.indexOf("{messages.map((message) => {"),
chat.indexOf("{savedTime &&"),
);
const snapshot = board.slice(
board.indexOf("className=\"rectification-snapshot\""),
board.indexOf("{scoringMinutes.length > 0 || displayMinutes.length > 0"),
);
const rowIndex = messageLoop.indexOf("<ChatMessageRow");
const snapshotIndex = snapshot.indexOf("className=\"rectification-snapshot\"");
const houseIndex = snapshot.indexOf("<RectificationHouseTableView");
assert.ok(rowIndex >= 0);
assert.ok(snapshotIndex >= 0 && houseIndex > snapshotIndex);
assert.doesNotMatch(snapshot, /<RectificationCandidateCards/);
assert.doesNotMatch(snapshot, /当前可能的出生时间/);
assert.doesNotMatch(messageLoop, /className="rectification-snapshot"/);
assert.doesNotMatch(messageLoop, /<RectificationHouseTableView/);
assert.match(chat, /className="conversation is-rectification"/);
assert.match(chat, /<RectificationBoard/);
assert.match(chat, /await loadCaseSnapshot\(\)/);
assert.doesNotMatch(chat, /void loadCaseSnapshot\(\)/);
assert.match(chat, /signal: controller\.signal/);
assert.match(board, /aria-live="polite"/);
assert.match(board, /groupWindowTransitions/);
assert.match(board, /<aside/);
assert.doesNotMatch(board, /<dialog/);
assert.doesNotMatch(board, /showModal/);
assert.doesNotMatch(board, /onCloseRef\.current = onClose/);
assert.match(board, /event\.key === "Escape"/);
assert.match(board, /aria-label="关闭盘面"/);
assert.match(board, /className="rectification-board__handle"/);
assert.match(board, /className="rectification-board-overlay"/);
assert.match(board, /className="rectification-board-scrim"/);
assert.match(board, /role=\{compact \? "dialog" : undefined\}/);
assert.match(board, /aria-modal=\{compact \? true : undefined\}/);
assert.match(chat, /RectificationBoardPeek/);
assert.match(chat, /onOpen=\{toggleBoard\}/);
assert.match(chat, /inert=\{compactBoard && boardOpen \? true : undefined\}/);
assert.match(houseTable, /当前本命宫位/);
assert.match(houseTable, /补充经历后会按新线索重算/);
assert.doesNotMatch(houseTable, /<ChatMessageRow/);
});
test("compact board overlays chat as a bottom sheet above the composer", () => {
assert.doesNotMatch(styles, /\.rectification-workspace\.is-compact\.is-board-open \{[\s\S]*grid-template-rows: minmax\(8rem, 1fr\) minmax\(13rem, 46%\)/);
assert.match(styles, /\.rectification-workspace \{[\s\S]*isolation: isolate/);
assert.match(styles, /\.rectification-workspace \{[\s\S]*z-index: 0/);
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[\s\S]*position: absolute/);
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[\s\S]*inset: 0/);
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[^}]*z-index: 50/);
assert.doesNotMatch(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[^}]*z-index: 20/);
assert.match(styles, /\.rectification-workspace\.is-compact\.is-board-open \.rectification-workspace__chat \.composer-wrap \{[\s\S]*visibility: hidden/);
assert.match(styles, /\.rectification-workspace\.is-compact\.is-board-open \.rectification-workspace__chat \.composer-wrap \{[\s\S]*backdrop-filter: none/);
assert.match(styles, /\.rectification-workspace__chat \.composer-wrap \{[\s\S]*z-index: 2/);
assert.match(styles, /\.rectification-board\.is-sheet \{[^}]*height: 72%/);
assert.doesNotMatch(styles, /\.rectification-board\.is-sheet \{[^}]*height: 92%/);
assert.match(styles, /\.rectification-board\.is-sheet \{[\s\S]*background: var\(--color-canvas\)/);
assert.match(styles, /\.rectification-board\.is-sheet \{[\s\S]*border-block-start: 1px solid var\(--color-border\)/);
assert.match(styles, /@keyframes rectification-sheet-enter \{[\s\S]*translateY\(18%\)/);
assert.doesNotMatch(styles, /dialog\.rectification-board/);
assert.match(styles, /\.rectification-board__close \{[\s\S]*width: 44px/);
assert.match(styles, /\.rectification-board__close \{[\s\S]*min-height: 44px/);
assert.match(board, /<X aria-hidden="true"/);
assert.match(chat, /compactBoard && !boardOpen \? \(/);
assert.match(chat, /headerSlot && boardPeek \? createPortal\(boardPeek, headerSlot\)/);
assert.doesNotMatch(chat, /querySelector\(/);
assert.doesNotMatch(chat, /setHeaderSlot/);
assert.match(page, /className="chat-header-rectification"/);
assert.match(page, /data-rectification-header-slot=""/);
assert.match(page, /ref=\{setRectificationHeaderSlot\}/);
assert.match(page, /headerSlot=\{rectificationHeaderSlot\}/);
assert.match(styles, /\.chat-header-rectification \.rectification-board-peek \{[\s\S]*width: auto/);
assert.doesNotMatch(chat, /rectification-workspace__board-trigger/);
assert.doesNotMatch(chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("<form className=\"composer\"")), /RectificationBoardPeek/);
assert.match(chat, /className=\{`rectification-workspace\$\{compactBoard \? " is-compact" : ""\}\$\{boardOpen \? " is-board-open" : ""\}`\}/);
});
test("rectification composer can stop a live agent run", () => {
assert.match(chat, /signal: abortController\.signal/);
assert.match(chat, /runAbort\.current\?\.abort\(\)/);
assert.match(chat, /className="composer-stop"/);
assert.match(chat, /aria-label="停止回答"/);
assert.match(chat, /event\.type === "attempt\.reset"/);
assert.match(chat, /caught\.name === "AbortError"/);
assert.match(chat, /showActivity=\{displayedMessage\.state !== "settled"\}/);
});
test("conversation and house board reuse the quiet overlay scrollbar", () => {
assert.match(styles, /\[data-sidebar="content"\],\s*\n\.conversation,\s*\n\.rectification-board__body \{/);
assert.match(styles, /\.conversation \{[^}]*overflow-y:\s*auto/);
assert.doesNotMatch(styles, /\.conversation \{[^}]*scrollbar-gutter/);
assert.doesNotMatch(styles, /\.conversation:not\(\.is-empty\):not\(\.is-rectification\) \{[^}]*scrollbar-gutter/);
});
test("time-selection cards appear under the latest settled agent bubble only after offer-candidates", () => {
const messageLoop = chat.slice(
chat.indexOf("{messages.map((message) => {"),
chat.indexOf("{savedTime &&"),
);
const actionsIndex = messageLoop.indexOf("<ChatMessageActions");
const cardsIndex = messageLoop.indexOf("<RectificationCandidateCards");
assert.ok(actionsIndex >= 0 && cardsIndex > actionsIndex);
assert.match(chat, /candidateResult\?\.selectionAllowed/);
assert.match(chat, /turnOfferedSelection/);
assert.match(chat, /rectification-offer-candidates/);
assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*turnOfferedSelection\(latestOfferMessage\)[\s\S]*!busy/);
assert.match(
chat,
/selectionCardMessageKey = showSelectionCards && latestOfferMessage\s*\? latestOfferMessage\.renderKey\s*: undefined/,
);
assert.match(messageLoop, /showSelectionCards && message\.renderKey === selectionCardMessageKey/);
assert.match(messageLoop, /<RectificationChoiceCard/);
assert.match(messageLoop, /key=\{choiceCard\.question_id\}/);
assert.doesNotMatch(choiceCardComponent, /setSelectedKey\(""\)/);
assert.match(chat, /showChoiceCards = Boolean\(/);
assert.match(chat, /choiceCardUserMessage/);
assert.match(chat, /CHOICE_STOP_MESSAGE/);
assert.match(caseRoute, /choice_card: choiceCardFromCaseDossier/);
assert.doesNotMatch(messageLoop, /className="rectification-snapshot"/);
});
test("rectification keeps the composer but never renders generated suggestion chips", () => {
assert.match(chat, /<form className="composer"/);
assert.doesNotMatch(chat, /composer-suggestions/);
assert.doesNotMatch(chat, /setSuggestions/);
assert.doesNotMatch(chat, /suggestions/);
assert.match(chat, /点上面的选项即可/);
});
test("rectification Agent output stays natural and keeps tool execution silent", () => {
const skill = readFileSync(
new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url),
"utf8",
);
const strategy = readFileSync(
new URL("../../skills/jyotish-birth-time-rectification/references/conversation-strategy.md", import.meta.url),
"utf8",
);
assert.match(agent, /工具执行过程保持静默/);
assert.match(agent, /思考过程必须用简体中文/);
assert.match(agent, /本轮做了什么/);
assert.match(agent, /完成凭证完全由服务端公开 Activity\/receipt 展示/);
assert.match(agent, /禁止只说记下了、会话会保留、以后再继续/);
assert.match(agent, /暂时想不到了 \/ 没有更多 \/ 先这样/);
assert.match(agent, /on_user_stop/);
assert.match(agent, /skill_verification_report/);
assert.match(agent, /D9\/D10 类型对照/);
assert.doesNotMatch(agent, /分盘句和宫位表由界面展示/);
assert.match(skill, /不得叙述读取 Skill/);
assert.match(skill, /完整回复可以(?:是)?零(?:个)?问题/);
assert.match(skill, /同一用户可以保留多个可恢复 Case/);
assert.doesNotMatch(skill, /同一用户最多一个 resumable Case/);
assert.match(strategy, /没有更多事件/);
assert.match(strategy, /(?:无需|不要求)结束、暂停或保存进度/);
});
test("clear current-turn events go through the batch evidence service", () => {
const tools = readFileSync(
new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url),
"utf8",
);
assert.match(agent, /当前轮新事件一律走 rectification-record-evidence-batch/);
assert.match(agent, /rectification-confirm-evidence 只用于用户对已有 pending 明确说“对\/是”/);
assert.match(agent, /不得要求用户把已说清的事件再发一遍/);
assert.doesNotMatch(agent, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
assert.doesNotMatch(agent, /“是\/对”只能确认当前 pending draft/);
assert.match(tools, /同一轮有两件及以上可拆分事件时必须改用 rectification-record-evidence-batch/);
assert.doesNotMatch(tools, /只有用户明确确认后才进入评分账本/);
});
test("the Agent prompt cannot offer candidates while asking for more evidence", () => {
// The hard boundary lives in the prompt; no tool input carries an
// offer_selection boolean anymore.
assert.match(agent, /不得在同一回复中一边要求继续补证据,一边提供候选采用/);
assert.match(agent, /id 不是 adopt_representative 时不得调用 rectification-offer-candidates/);
assert.match(agent, /selection_allowed 只表示可以采用代表性时间/);
const tools = readFileSync(
new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url),
"utf8",
);
assert.doesNotMatch(tools, /offer_selection/);
});
test("adopted time offers a consultation handoff without unique-minute copy", () => {
assert.match(chat, /用这个时间看盘/);
assert.match(chat, /onStartConsultation/);
assert.match(board, /换升时刻/);
assert.match(board, /经历与大运对照/);
assert.match(chat, /<RectificationChoiceCard/);
assert.match(chat, /choiceCardUserMessage/);
assert.match(page, /startConsultationAfterRectification/);
assert.match(page, /createSession\(modelCatalog\.defaultModelId\)/);
assert.match(agent, /start_consultation/);
assert.match(agent, /还不能确认唯一分钟/);
});
test("stream failures remove empty assistant placeholders", () => {
assert.match(chat, /streamFailed = true/);
assert.match(chat, /const succeeded = completed && !streamFailed && Boolean\(parsed\.text\)/);
assert.match(chat, /current\.filter\(\(message\) => message\.renderKey !== assistantRenderKey\)/);
assert.match(chat, /if \(succeeded\)/);
});
test("rectification sessions are opened by the server Case API; the browser never creates them", () => {
assert.match(page, /\/api\/rectification\/cases\/open/);
assert.match(page, /openRectificationRequestBody\(intent, exactSessionId\)/);
assert.match(page, /openResponseFromPayload\(payload\)/);
assert.doesNotMatch(page, /sessions\.find\(\(session\) => session\.sessionType === "birth_time_rectification"\)/);
assert.doesNotMatch(page, /hasRectificationSession/);
assert.match(page, /rectificationCardAction = resolveRectificationEntryAction/);
});
test("sidebar selection passes the exact sessionId to the server open API", () => {
const selectSession = page.slice(
page.indexOf("function selectSession("),
page.indexOf("async function selectSessionModel", page.indexOf("function selectSession(")),
);
assert.match(selectSession, /nextSession\?\.sessionType === "birth_time_rectification"/);
assert.match(selectSession, /void openRectificationSession\(nextSession\.id\)/);
assert.doesNotMatch(selectSession, /resumeRectificationSession\.current\(nextSession\)/);
});
test("homepage CTA is server-driven from the entry summary, not the session list", () => {
const entryLib = readFileSync(
new URL("../src/lib/rectification-entry.ts", import.meta.url),
"utf8",
);
assert.doesNotMatch(page, /hasRectificationSession/);
assert.doesNotMatch(page, /sessions\.some\([\s\S]{0,120}birth_time_rectification/);
assert.match(page, /resolveRectificationEntryAction/);
assert.match(page, /rectificationCardAction === "restart"/);
assert.match(entryLib, /开始新的生时校正/);
assert.match(entryLib, /再次校正/);
});
test("readonly terminal sessions expose a 再次校正 action instead of appending", () => {
assert.match(chat, /readonly/);
assert.match(chat, /再次校正/);
assert.match(chat, /onRestart/);
});
test("legacy Skill adoption is an explicit server-owned route and case refresh exposes status", () => {
assert.match(adoptSkillRoute, /adoptLegacyRectificationSkill/);
assert.match(adoptSkillRoute, /invalid_adoption_request/);
assert.doesNotMatch(adoptSkillRoute, /upgradeRectificationSkill/);
assert.match(upgradeSkillRoute, /upgradeRectificationSkill/);
assert.doesNotMatch(upgradeSkillRoute, /adoptLegacyRectificationSkill/);
assert.match(caseRoute, /loadV9CaseSkillIdentityStatus/);
assert.match(caseRoute, /skill_identity_status/);
assert.match(caseRoute, /requires_skill_adoption/);
});