Files
Jyotisha/frontend/tests/rectification-agentic-entry.test.ts
T
Jesse_Chen 9d8c73561f
Staging Backend Quality Gate / validate (push) Failing after 7m30s
Staging Backend Quality Gate / publish (push) Has been skipped
test(deploy): integrate billing admin rollout checks
2026-08-06 20:15:50 +08:00

193 lines
10 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");
test("birth-time rectification entry no longer routes through V4", () => {
assert.doesNotMatch(component, /loadActiveRectificationV4|RectificationV4Panel|transitionRectificationV4/);
assert.match(component, /return <AgenticRectificationChat \{\.\.\.props\} \/>/);
});
test("opening is a server-owned operation rather than a hidden user prompt", () => {
assert.doesNotMatch(chat, /agenticOpeningInstruction|用户刚进入生时校正会话/);
assert.match(chat, /action: "opening"/);
assert.match(route, /z\.literal\("opening"\)/);
assert.match(route, /conversation\.action === "opening"/);
});
test("incomplete profiles stay in the shared onboarding flow", () => {
const opening = page.slice(
page.indexOf("async function openBirthTimeRectification"),
page.indexOf("resumeRectificationSession.current ="),
);
assert.match(opening, /const missingStep = missingProfileStep\(profile\)/);
assert.match(opening, /setOnboardingStep\(missingStep\)/);
assert.ok(
opening.indexOf("const missingStep = missingProfileStep(profile)")
< opening.indexOf("setRectificationSessionId(rectificationSession.id)"),
);
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\(nextProfile\)/);
assert.doesNotMatch(refresh, /setProfileDraft/);
});
test("agent tool calls leave a final step for visible prose and never end silently", () => {
assert.match(route, /const agenticRectificationMaxSteps = 8/);
assert.match(route, /\{ maxSteps: agenticRectificationMaxSteps \}/);
assert.match(route, /if \(!emitted \|\| !reply\.text\) \{[\s\S]*type: "error"[\s\S]*await settle\(false\)[\s\S]*return;/);
assert.doesNotMatch(route, /send\(\{ type: "done", emitted \}\)/);
});
test("rectification messages survive remounts and suppress duplicate openings", () => {
assert.match(chat, /initialMessages: readonly ChatMessage\[\]/);
assert.match(chat, /if \(initialMessages\.length > 0 \|\| openingStarted\.current\) return/);
assert.match(chat, /sessionId,/);
assert.match(chat, /onMessagesChange\?\.\(/);
assert.match(page, /key=\{rectificationSessionId\}/);
assert.match(page, /initialMessages=\{activeSession\?\.messages \?\? \[\]\}/);
assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/);
});
test("successful Agent turns are persisted by the authenticated rectification route", () => {
assert.match(route, /sessionId: z\.string\(\)\.uuid\(\)/);
assert.match(route, /\.from\("chat_sessions"\)[\s\S]*\.eq\("user_id", userId\)/);
assert.match(route, /conversation\.action === "opening" && persistedMessages\.length > 0/);
assert.match(route, /\.update\(\{ messages: nextMessages, updated_at:/);
assert.match(route, /if \(saveError \|\| !savedSession\) throw new Error\("RectificationSessionPersistenceError"\)/);
});
test("candidate results restore only through the authenticated rectification session", () => {
const getRoute = route.slice(
route.indexOf("export async function GET"),
route.indexOf("export async function POST"),
);
assert.match(getRoute, /sessionId/);
assert.match(getRoute, /\.eq\("user_id", user\.id\)/);
assert.match(getRoute, /session\.session_type !== "birth_time_rectification"/);
assert.match(getRoute, /loadLatestAgenticRectificationResult\(accounting, user\.id, sessionId\)/);
});
test("candidate acceptance is non-billable and happens before unified usage authorization", () => {
const acceptance = route.indexOf('parsed.data.action === "accept_candidate"');
const authorize = route.indexOf("authorizeUsage(accounting");
assert.ok(acceptance >= 0 && authorize > acceptance);
assert.match(route, /featureKey: "rectification"/);
assert.doesNotMatch(route, /begin_consultation_credit|complete_consultation_credit|cancel_consultation_credit/);
});
test("Agentic rectification completes or releases unified usage without hiding settlement failures", () => {
assert.match(route, /settlement = await completeUsage\(accounting, userId, billingRequestId, \{/);
assert.match(route, /eventKey: requestId,/);
assert.match(route, /actualModelId: selectedModel\.id/);
assert.match(route, /modelConfigVersion: selectedModel\.configVersion/);
assert.match(route, /inputTokens,/);
assert.match(route, /outputTokens,/);
assert.match(route, /costMicrousd: Math\.round/);
assert.match(route, /durationMs: Date\.now\(\) - usageStartedAt/);
assert.match(route, /settlement = await releaseUsage\(accounting, userId, billingRequestId, "rectification_cancelled"\)/);
assert.match(route, /if \(!settlement\.success\) throw new Error\(settlement\.error_code \?\? "usage_settlement_failed"\)/);
assert.match(route, /if \(!await settle\(true, result\.totalUsage\)\)[\s\S]*type: "error"[\s\S]*return;[\s\S]*send\(\{ type: "done", emitted: true \}\)/);
});
test("opening and message retries reuse the caller-owned turn request as their usage event key", () => {
assert.match(route, /const requestId = conversation\.requestId;/);
assert.match(route, /completeUsage\(accounting, userId, billingRequestId, \{[\s\S]*eventKey: requestId,/);
assert.doesNotMatch(route, /eventKey:\s*(?:globalThis\.)?crypto\.randomUUID\(\)/);
});
test("Agentic 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"\)/);
assert.match(route, /resolveSessionLanguageModel\(\s*chatSession\.model_id,\s*chatSession\.model_config_version,?\s*\)/);
assert.match(route, /const billingRequestPrefix = `rectification:\$\{sessionId\}`/);
assert.match(route, /requestId: billingRequestId/);
assert.match(route, /modelConfigVersion: selectedModel\.configVersion/);
assert.doesNotMatch(route, /loadLanguageModelCatalog|resolveLanguageModelFromCatalog|\bresolveLanguageModel\(|\bdefaultLanguageModel\(/);
});
test("candidate state streams before done and renders reusable multi-column choices", () => {
assert.match(route, /send\(\{ type: "candidates", result: candidateResult \}\)[\s\S]*send\(\{ type: "done", emitted: true \}\)/);
assert.match(chat, /fetch\(`\/api\/rectification\/agent\?sessionId=/);
assert.match(chat, /action: "accept_candidate"/);
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.match(chat, /当前推荐/);
assert.match(chat, /已采用/);
});
test("Agent cannot offer a candidate selection in the same turn that asks for more evidence", () => {
assert.match(agent, /offer_selection/);
assert.match(agent, /If you will ask for another event or date detail in the same reply, offer_selection must be false/);
assert.match(agent, /Never both ask for more evidence and offer candidate adoption in the same reply/);
});
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("new rectification sessions are created before the Agent surface mounts", () => {
assert.match(page, /await rectificationPersistence\.current\.enqueue[\s\S]*setRectificationSessionId\(rectificationSession\.id\)/);
assert.doesNotMatch(page, /setRectificationSessionId\(rectificationSession\.id\)[\s\S]{0,500}persistSession\(rectificationSession, "create"\)/);
});