405 lines
20 KiB
TypeScript
405 lines
20 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("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: parsed\.suggestions/);
|
|
assert.match(chat, /parseAgentReplyBody/);
|
|
});
|
|
|
|
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/);
|
|
});
|