Move-only (TASK-rectification-code-split-20260926 T2/T4). No behavior, API, copy, DB, Skill, billing or scoring change. - route.ts: 1077 -> 152 lines; POST 927 -> 114. It builds the Supabase clients (so the setup-failure mapping stays here) and assembles: agent-route-request.ts (auth / product / schema / flag / Case-Session binding), agent-route-typed-message.ts (declared-window reply, typed-answer preflight, unfocused classification), agent-route-structured-choice.ts, agent-route-agent-turn.ts (opening / read-only / typed agent stream and its exit gate), agent-route-billing.ts, agent-route-support.ts (schema, one-shot NDJSON reply, context types). - The four mutable preflight lets (expectedWrite, collectIntent, writeClassified, classifierDiagnostic) that crossed branches are one turnState object; values and flow unchanged. - Tests: whole-source contracts read tests/rectification-agent-route-surface.ts; the typed fast-path slice is rebuilt from the new files; billing, declared-window and structured-choice slices now call the handlers (billing in a child process because feature-pricing imports server-only), each with 原值/新值/原因. Route growth caps added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
102 lines
5.0 KiB
TypeScript
102 lines
5.0 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
COLUMN_COMPARE_BUDGET_MS,
|
||
columnTimesForSlowCompare,
|
||
} from "../src/lib/rectification-agentic/v9/column-times-for-compare.ts";
|
||
import { parseDeclaredBirthWindow } from "../src/lib/rectification-agentic/v9/declared-window-utterance.ts";
|
||
import { RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
|
||
import { replyToDeclaredBirthWindow } from "../src/lib/rectification-agentic/v9/agent-route-typed-message.ts";
|
||
import { ndjsonEvents, routeContextFixture, ROUTE_TURN_ID } from "./rectification-agent-route-fixtures.ts";
|
||
import { readRectificationAgentRouteFile } from "./rectification-agent-route-surface.ts";
|
||
|
||
test("dated or event-clock sentences are not treated as a declared birth window", () => {
|
||
assert.equal(parseDeclaredBirthWindow("2024 年 8 月 8 日 20:00 左右分手"), null);
|
||
assert.equal(parseDeclaredBirthWindow("下午 3 点到 5 点被车撞"), null);
|
||
assert.equal(parseDeclaredBirthWindow("2019 年 10 月 23 日 9 点半入职"), null);
|
||
assert.equal(parseDeclaredBirthWindow("20:00 左右分手"), null);
|
||
assert.equal(parseDeclaredBirthWindow("2014年入学,大概秋天"), null);
|
||
});
|
||
|
||
test("birth-context and bare clock ranges still count as a declared window", () => {
|
||
assert.deepEqual(
|
||
parseDeclaredBirthWindow("我的出生时间是 14 点 45 到 14 点 50"),
|
||
{ kind: "range", start: "14:45", end: "14:50" },
|
||
);
|
||
assert.deepEqual(
|
||
parseDeclaredBirthWindow("我是 14:47 左右生的"),
|
||
{ kind: "around", time: "14:47" },
|
||
);
|
||
assert.deepEqual(
|
||
parseDeclaredBirthWindow("14:45–14:50"),
|
||
{ kind: "range", start: "14:45", end: "14:50" },
|
||
);
|
||
assert.deepEqual(
|
||
parseDeclaredBirthWindow("14:47 左右"),
|
||
{ kind: "around", time: "14:47" },
|
||
);
|
||
assert.deepEqual(
|
||
parseDeclaredBirthWindow("大概 14:47 左右"),
|
||
{ kind: "around", time: "14:47" },
|
||
);
|
||
});
|
||
|
||
test("choice-focus declared window is still answered from the route before the model", async () => {
|
||
// 原值: route.ts 源码里 `if (action === "message" && parseDeclaredBirthWindow` 早于
|
||
// `const selectedModel = isStructuredChoice`(以及早于 `await classifyTurnIntentWithRetry`)。
|
||
// 新值: route.ts 先 `await replyToDeclaredBirthWindow(context)` 并在有回复时直接 return,之后才解析模型、才进打字预检
|
||
// (分类器只在 handleRectificationTypedMessage 里);再直接调用 replyToDeclaredBirthWindow:
|
||
// 报时段的消息落一条确定性 turn、流里给锁定回复和 run.completed,别的消息返回 null、不碰持久层。
|
||
// 原因: TASK-rectification-code-split-20260926 这一截原样搬进 agent-route-typed-message.ts;调用断言强于源码顺序。
|
||
const routeFile = readRectificationAgentRouteFile("../src/app/api/rectification/agent/route.ts");
|
||
const intercept = routeFile.indexOf("const declaredWindowReply = await replyToDeclaredBirthWindow(context);");
|
||
const selectedModel = routeFile.indexOf("const selectedModel = isStructuredChoice");
|
||
const typedPreflight = routeFile.indexOf("return handleRectificationTypedMessage(");
|
||
assert.ok(intercept > 0);
|
||
assert.match(routeFile, /if \(declaredWindowReply\) return declaredWindowReply;/);
|
||
assert.ok(selectedModel > intercept);
|
||
assert.ok(typedPreflight > selectedModel);
|
||
const declared = routeContextFixture({ action: "message", message: "大概 14:47 左右" });
|
||
const reply = await replyToDeclaredBirthWindow(declared.context);
|
||
assert.ok(reply);
|
||
assert.equal(reply.status, 200);
|
||
assert.deepEqual(await ndjsonEvents(reply), [
|
||
{ type: "answer.delta", text: RECTIFICATION_USER_COPY.declaredWindowLockedReply },
|
||
{ type: "run.completed", turnId: ROUTE_TURN_ID },
|
||
]);
|
||
assert.deepEqual(declared.rpcCalls.map((call) => call.fn), ["append_agentic_rectification_turn"]);
|
||
assert.equal(declared.rpcCalls[0].args.p_assistant_message, RECTIFICATION_USER_COPY.declaredWindowLockedReply);
|
||
const ordinary = routeContextFixture({ action: "message", message: "2019 年换了工作" });
|
||
assert.equal(await replyToDeclaredBirthWindow(ordinary.context), null);
|
||
assert.deepEqual(ordinary.rpcCalls, []);
|
||
});
|
||
|
||
test("score persist passes active minutes only after a slow by_time compare", () => {
|
||
const persist = readFileSync(
|
||
new URL("../src/lib/rectification-agentic/v9/score-persist.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(persist, /columnTimesForSlowCompare/);
|
||
assert.match(persist, /columnTimes,/);
|
||
});
|
||
|
||
test("slow by_time compares may pass only active inference minutes", () => {
|
||
assert.equal(
|
||
columnTimesForSlowCompare({ previousColumnCompareMs: 1200, activeTimes: ["14:46", "14:47", "14:48"] }),
|
||
undefined,
|
||
);
|
||
assert.deepEqual(
|
||
columnTimesForSlowCompare({
|
||
previousColumnCompareMs: COLUMN_COMPARE_BUDGET_MS + 1,
|
||
activeTimes: ["14:46", "14:47", "14:46", "bad"],
|
||
}),
|
||
["14:46", "14:47"],
|
||
);
|
||
assert.equal(
|
||
columnTimesForSlowCompare({ previousColumnCompareMs: 4000, activeTimes: [] }),
|
||
undefined,
|
||
);
|
||
});
|