Files
Jyotisha/frontend/tests/rectification-agentic-entry.test.ts
T
Jesse_Chen 8b8e5214f9 fix(web): keep onboarding and mobile natal board on one aligned overlay
Put the compact board trigger in the chat header, keep the sheet above the composer on iOS, and align onboarding messages with the intake card on the session column.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 23:54:20 +08:00

589 lines
30 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 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 completedActivityReceipt = readFileSync(
new URL("../src/components/completed-activity-receipt.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(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.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, error, messages, savedTime\]\);/);
assert.doesNotMatch(chat, /conversationEnd|scrollIntoView/);
});
test("rectification keeps receipts for the varga sentence and hides Activity from the user", () => {
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 === "thinking"\}/);
assert.match(chat, /vargaSentenceFromMethods/);
assert.match(chat, /vargaSentence=\{vargaSentence\}/);
assert.match(board, /RectificationHouseTableView/);
assert.doesNotMatch(chat, /activeActivity/);
assert.doesNotMatch(chat, /正在读取校正记录/);
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));
}
});
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 loadCandidate\(\)\.then\(\(result\) => \{/);
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, /createPortal\(/);
assert.match(chat, /\[data-rectification-header-slot\]/);
assert.match(page, /className="chat-header-rectification"/);
assert.match(page, /data-rectification-header-slot=""/);
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 === "thinking"\}/);
});
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.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/);
});
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, /完成凭证完全由服务端公开 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, /盘外对照/);
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/);
});