52f3c04b3b
Measured use of the three chips above the composer was negligible. They were also not what they appeared to be: the server looked up a fixed triplet by session theme and passed it as metadata that overrode anything the model produced, so the same ten hardcoded sets served every user regardless of question or chart. That is a plausible reason nobody pressed them. Both copies of the per-theme table are gone, reply metadata narrows to the session title, and the two parse entry points collapse into one now that they return the same shape. The write schema still tolerates a suggestions field so a client on the previous bundle does not lose its message mid-deploy, and stored answers containing the legacy hidden block are still stripped rather than shown raw. Co-authored-by: Cursor <cursoragent@cursor.com>
428 lines
21 KiB
TypeScript
428 lines
21 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 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 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"/);
|
|
assert.match(chat, /const container = conversation\.current/);
|
|
assert.match(chat, /top: container\.scrollHeight/);
|
|
assert.match(chat, /\}, \[busy, error, messages, savedTime\]\);/);
|
|
assert.doesNotMatch(chat, /conversationEnd|scrollIntoView/);
|
|
});
|
|
|
|
test("rectification activity separates live work from the receipt above the Agent message", () => {
|
|
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, /activeActivity\?: PublicActivity/);
|
|
assert.match(chat, /completedReceipt\?: CompletedActivityReceiptView/);
|
|
assert.match(chat, /showActivity=\{displayedMessage\.state === "thinking" \|\| Boolean\(displayedMessage\.activeActivity\)\}/);
|
|
assert.match(chat, /"rectification-read-case": "正在读取校正记录…"/);
|
|
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("{candidateResult?.selectionAllowed"),
|
|
);
|
|
const receiptIndex = messageRender.indexOf("<CompletedActivityReceipt");
|
|
const replyIndex = messageRender.indexOf("<ChatMessageRow");
|
|
assert.ok(receiptIndex >= 0 && replyIndex >= 0);
|
|
assert.ok(receiptIndex < replyIndex);
|
|
assert.match(completedActivityReceipt, /<details className=/);
|
|
assert.doesNotMatch(completedActivityReceipt, /<details[^>]*\sopen/);
|
|
assert.match(completedActivityReceipt, /本轮完成 · \$\{stepLabels\.length\} 个步骤 · \$\{receipt\.methods\.length\} 项计算依据/);
|
|
assert.match(completedActivityReceipt, /执行步骤/);
|
|
assert.match(completedActivityReceipt, /计算依据/);
|
|
assert.doesNotMatch(completedActivityReceipt, /<button|className=.*pill|className=.*tag/);
|
|
for (const genericCopy of ["开始本轮执行", "正在加载专用方法", "专用方法已加载", "本轮做了什么"]) {
|
|
assert.doesNotMatch(chat, new RegExp(genericCopy));
|
|
}
|
|
|
|
const activityStyles = styles.slice(styles.indexOf(".rectification-message-wrap"));
|
|
assert.match(activityStyles, /\.rectification-activity-receipt \{[\s\S]*color:/);
|
|
assert.match(activityStyles, /\.rectification-activity-receipt summary:focus-visible/);
|
|
const receiptRule = activityStyles.match(/\.rectification-activity-receipt \{([^}]*)\}/)?.[1] ?? "";
|
|
assert.doesNotMatch(receiptRule, /background:/);
|
|
assert.doesNotMatch(receiptRule, /border:/);
|
|
});
|
|
|
|
test("completed Agent replies restore feedback, copy and safe in-place regeneration actions", () => {
|
|
for (const label of ["赞", "踩", "复制回答", "重新生成回答"]) {
|
|
assert.match(chat, new RegExp(`aria-label="${label}"`));
|
|
}
|
|
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("{candidateResult?.selectionAllowed"),
|
|
);
|
|
const receiptIndex = messageRender.indexOf("<CompletedActivityReceipt");
|
|
const replyIndex = messageRender.indexOf("<ChatMessageRow");
|
|
const actionsIndex = messageRender.indexOf('className="rectification-message-actions"');
|
|
assert.ok(receiptIndex >= 0 && replyIndex >= 0 && actionsIndex >= 0);
|
|
assert.ok(receiptIndex < replyIndex && replyIndex < actionsIndex);
|
|
|
|
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.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, /正在采用…/);
|
|
});
|
|
|
|
|
|
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(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 are proposed and confirmed in the same Agent run", () => {
|
|
const tools = readFileSync(
|
|
new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.match(agent, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence,同轮完成记录/);
|
|
assert.match(agent, /不得要求用户逐条重新发送或再次回答.*确认/);
|
|
assert.doesNotMatch(agent, /“是\/对”只能确认当前 pending draft/);
|
|
assert.match(tools, /当前轮主动、明确且无歧义的一件或多件可拆分事件/);
|
|
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, /不得在同一回复中一边要求继续补证据,一边提供候选采用/);
|
|
const tools = readFileSync(
|
|
new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.doesNotMatch(tools, /offer_selection/);
|
|
});
|
|
|
|
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/);
|
|
});
|