Files
Jyotisha/frontend/tests/rectification-agentic-entry.test.ts
T
Jesse_Chen 54269fcfcc
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
refactor(chat): extract home helpers, chart library, and starter surfaces
page.tsx still owns the chat main chain, but the first product surfaces now
live in their own modules so later splits can land without editing the 4k-line
Home. Source-lock tests follow the moved tokens; the orphan user-data contract
is aligned and added to the quick gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 22:58:02 +08:00

746 lines
39 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";
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 activityStatus = readFileSync(
new URL("../src/components/agent-activity-status.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 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.doesNotMatch(chat, /spoken-answer/);
assert.doesNotMatch(chat, /splitRectificationSpokenAndThinking|settleRectificationSpokenAndThinking|finalizeRectificationSpokenAndThinking/);
assert.match(chat, /text: raw,/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.length > 0 \? "ready" : "loading"\}`\}/);
assert.doesNotMatch(page, /rectificationTurns\.at\(-1\)\?\.id/);
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 \(initialTurns\.length > 0\) \{/);
assert.match(chat, /if \(readonly \|\| openingStarted\.current \|\| !shouldStartOpening\) return/);
assert.match(chat, /onOpeningConsumed\?\.\(\)/);
assert.match(chat, /void send\("opening", ""\)/);
assert.match(route, /import \{ createServerSupabaseClient \} from "@\/lib\/supabase\/server"/);
assert.match(route, /action: z\.enum\(\["opening", "message", "read_only", "answer_choice", "stop_and_review"\]\)/);
assert.match(page, /shouldStartOpening=\{rectificationShouldStartOpening\}/);
assert.match(page, /setRectificationShouldStartOpening\(opened\.shouldStartOpening\)/);
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
});
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(chat, /if \(initialTurns\.length > 0\) \{/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.length > 0 \? "ready" : "loading"\}`\}/);
assert.match(page, /initialTurns=\{rectificationTurns\}/);
assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/);
assert.match(page, /onOpeningConsumed=\{\(\) => setRectificationShouldStartOpening\(false\)\}/);
});
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, mutually exclusive, and continues through the durable case endpoint", () => {
assert.match(chat, /\/candidates\/accept/);
assert.match(chat, /resultId: candidateResult\.resultId/);
assert.match(chat, /if \(!candidateResult \|\| acceptingCandidateId \|\| busy \|\| readonly\) return;/);
assert.match(chat, /setAcceptingCandidateId\(candidateId\);[\s\S]*setPending\(true\);/);
assert.match(chat, /await loadCaseSnapshot\(\);[\s\S]*choiceContinuationPending\.current = true;/);
assert.match(chat, /finally \{[\s\S]*setAcceptingCandidateId\(null\);[\s\S]*setPending\(false\);/);
assert.match(chat, /if \(!choiceContinuationPending\.current \|\| busy \|\| readonly\) return;[\s\S]*void send\("read_only", ""\);/);
assert.doesNotMatch(chat, /await loadCaseSnapshot\(\);[\s\S]*await send\("read_only", ""\);/);
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, /answerTokens: 8_192/);
assert.match(run, /thinkingTokens: 8_192/);
assert.match(run, /activity.changed/);
assert.match(run, /if \(!answerText\.trim\(\)\) return failedAttempt\(attemptId, "empty_stream"\)/);
assert.doesNotMatch(run, /bindSpokenToOpenQuestion|CHOICE_CARD_CONTINUATION_ACK|openQuestionPromptFromToolResult/);
assert.doesNotMatch(run, /heldSpoken/);
assert.match(run, /replace: true/);
const narration = readFileSync(
new URL("../src/lib/rectification-agentic/v9/turn-narration.ts", import.meta.url),
"utf8",
);
assert.match(narration, /payload\?: unknown/);
assert.doesNotMatch(run, /splitRectificationSpokenAndThinking/);
assert.match(run, /emit\(event: PublicStreamEvent\): Promise<void> \| void;/);
assert.match(run, /agentGenerationSettings\(options\.generationModel, \{[\s\S]*thinking: "enabled"/);
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", () => {
// Former value: select included unused `messages` and pulled up to 500KB per turn.
assert.match(route, /select\("id,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 follows the conversation tail only while the reader stays near the bottom", () => {
assert.match(chat, /const conversation = useRef<HTMLElement>\(null\)/);
assert.match(chat, /<section[\s\S]*ref=\{conversation\}[\s\S]*className="conversation is-rectification"/);
assert.match(chat, /followTailRef/);
assert.match(chat, /isNearBottom/);
assert.match(chat, /showJumpToLatest/);
assert.match(chat, /回到最新/);
assert.match(chat, /followLatestContent/);
assert.match(chat, /const container = conversation\.current/);
assert.match(chat, /top: viewport\.scrollHeight/);
assert.match(chat, /shouldShowJumpToLatest/);
assert.match(chat, /choiceCardsOpen/);
assert.match(
chat,
/useLayoutEffect\(\(\) => \{\s*choiceCardsOpen\.current = showLiveChoiceCard;\s*updateFollowState\(\);\s*\}, \[showLiveChoiceCard, updateFollowState\]\)/,
);
assert.doesNotMatch(chat, /choiceCardsOpen\.current = showLiveChoiceCard;\s*useLayoutEffect/);
assert.match(chat, /updateFollowState/);
assert.match(chat, /\}, \[busy, candidateResult, choiceCard, error, messages, savedTime, followLatestContent\]\);/);
const jumpLatestRule = styles.match(/\.rectification-jump-latest \{[^}]+\}/)?.[0] ?? "";
assert.match(jumpLatestRule, /justify-content: center/);
assert.doesNotMatch(jumpLatestRule, /flex-end/);
assert.match(styles, /--rectification-jump-clearance/);
assert.match(styles, /\.message-stage-and-answer \{[\s\S]*gap: var\(--space-2\)/);
assert.doesNotMatch(chat, /conversationEnd|scrollIntoView/);
assert.doesNotMatch(styles, /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(chat, /activityTraceFromReceipt/);
assert.match(activityStatus, /vargaSentence/);
assert.match(activityStatus, /VargaTraceStep/);
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.doesNotMatch(chat, /event.type === "thinking.delta"/);
assert.doesNotMatch(chat, /thinkingText: settled\.thinking/);
assert.doesNotMatch(chat, /\.\.\.\(thinkingText \? \{ thinkingText \}/);
assert.match(chat, /event.type === "activity.changed"/);
assert.match(chat, /userFacingRunFailure/);
assert.doesNotMatch(chat, /本轮处理未完成,已保留服务端记录的执行进度/);
assert.match(chat, /startActivityTraceStep/);
assert.match(chat, /completeActivityTraceStep/);
assert.match(activityStatus, /activityTrace/);
assert.match(activityStatus, /className="message-thinking"/);
assert.match(activityStatus, />思考</);
assert.match(activityStatus, /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.doesNotMatch(chat, /本会话以代表性时间收口,不确认唯一分钟/);
assert.doesNotMatch(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 use server adoption state and stay mutually exclusive with choice cards", () => {
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, /showLiveChoiceCard = Boolean\([\s\S]*currentQuestion\?\.kind === "choice"[\s\S]*choiceCard[\s\S]*answeredQuestionIds[\s\S]*!busy/);
assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*candidateResult\?\.canAdopt[\s\S]*!showLiveChoiceCard[\s\S]*!busy[\s\S]*!readonly/);
assert.doesNotMatch(
chat.slice(chat.indexOf("const showSelectionCards"), chat.indexOf("const selectionCardMessageKey")),
/offeredSelectionOnce/,
);
assert.match(
chat,
/selectionCardMessageKey = showSelectionCards && latestSettledAssistant\s*\? latestSettledAssistant\.renderKey\s*: undefined/,
);
assert.match(messageLoop, /showSelectionCards && message\.renderKey === selectionCardMessageKey/);
assert.match(messageLoop, /<RectificationChoiceCard/);
assert.match(messageLoop, /choiceAttachment/);
assert.match(messageLoop, /choiceNonce/);
assert.match(messageLoop, /answered:\$\{settledChoice\.selectedKey\}/);
assert.doesNotMatch(choiceCardComponent, /setSelectedKey\(""\)/);
assert.match(choiceCardComponent, /selectedKey/);
assert.match(choiceCardComponent, /is-answered/);
assert.match(chat, /showLiveChoiceCard = Boolean\(/);
assert.match(chat, /isStructuredChoiceUserText/);
assert.match(chat, /submitStructuredChoice\(CHOICE_ACTION/);
assert.match(chat, /submitStructuredChoice\(STOP_ACTION/);
assert.match(route, /answer_choice/);
assert.match(route, /stop_and_review/);
assert.match(route, /applyRectificationChoice\(accounting/);
assert.doesNotMatch(chat, /v9-choice-user-/);
assert.doesNotMatch(chat, /send\("message", choiceCardUserMessage/);
assert.doesNotMatch(chat, /send\("message", choiceCard\?\.stop_message/);
assert.doesNotMatch(chat, /choiceCardUserMessage/);
assert.match(caseRoute, /choice_card: choiceCardFromCaseDossier/);
assert.match(caseRoute, /current_question: projectCurrentQuestion/);
assert.match(caseRoute, /overlayPublicDecision/);
assert.match(caseRoute, /interview: publicDecisionFields/);
assert.doesNotMatch(chat, /已完成验证/);
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("does not parse suggestions from an incomplete run", () => {
assert.match(chat, /completed && !streamFailed \? parseAgentReply\(raw\)/);
assert.doesNotMatch(chat, /parseAgentReply\(partial/);
});
test("live answer.delta is the model reply, not a spoken-thinking split", () => {
assert.match(chat, /raw = event\.replace === true \? event\.text : raw \+ event\.text/);
assert.match(chat, /text: raw,/);
assert.doesNotMatch(chat, /settleRectificationSpokenAndThinking/);
assert.doesNotMatch(chat, /text: settled\.spoken/);
});
test("does not auto-submit a suggestion during render or recovery", () => {
assert.match(chat, /origin: action === "read_only" \? "choice_click" : "typed"/);
assert.doesNotMatch(chat, /origin: "suggestion_click"/);
assert.doesNotMatch(chat, /origin: "system_recovery"/);
assert.doesNotMatch(chat, /2002/);
});
test("persists message origin for every user turn", () => {
assert.match(route, /origin: z\.enum\(/);
assert.match(route, /messageOrigin:/);
assert.match(route, /clientActionId:/);
assert.match(chat, /clientActionId: requestId/);
assert.match(chat, /clientActionId: actionId/);
});
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.doesNotMatch(route, /action === "message" && caseStatus === "paused"/);
assert.doesNotMatch(agent, /USER_STOP_PATTERN|USER_STOP_NEGATION_PATTERN/);
assert.match(agent, /skill_verification_report/);
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, /(?:无需|不要求)结束、暂停或保存进度/);
assert.match(strategy, /不要一进场就出 A\/B\/C\/D/);
assert.match(agent, /有持久化 current_question 时,题干与选项完全由结构化槽位和 UI 承担/);
assert.match(agent, /正文不得提问、复述、改写或拼接题干/);
});
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.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", () => {
assert.doesNotMatch(agent, /offer_selection/);
assert.doesNotMatch(agent, /id 不是 adopt_representative、validated_range、provisional_range 或 provisional_range_user_stopped 时不得调用 rectification-offer-candidates/);
const tools = readFileSync(
new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url),
"utf8",
);
assert.match(tools, /function agentToolKeysForDecision/);
assert.match(tools, /dropped_probes: inspectDiscriminatorProbes/);
assert.match(tools, /dropped_probes: decision.droppedProbes/);
assert.doesNotMatch(tools, /offer_selection/);
});
test("choice cards expose the persisted prompt only to assistive tech", () => {
assert.match(choiceCardComponent, /<legend className="sr-only">\{props\.card\.prompt\}<\/legend>/);
assert.doesNotMatch(choiceCardComponent, /<legend>\{props\.card\.prompt\}<\/legend>/);
});
test("choice card answers and stop share one stacked primary list", () => {
assert.match(choiceCardComponent, /birth-time-primary-choices/);
assert.match(choiceCardComponent, /props\.card\.options\.map/);
assert.doesNotMatch(choiceCardComponent, /birth-time-special-choices/);
assert.doesNotMatch(choiceCardComponent, /is-secondary/);
});
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, /choiceAttachment/);
assert.match(page, /startConsultationAfterRectification/);
assert.match(page, /createSession\(modelCatalog\.defaultModelId\)/);
assert.doesNotMatch(agent, /start_consultation/);
assert.doesNotMatch(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/);
});