Files
Jyotisha/frontend/tests/rectification-declared-window-20260909.test.ts
T
Jesse_ChenandClaude Opus 5.5 eb773f1299 refactor(rectification): split POST /api/rectification/agent into branch handlers
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
2026-09-26 19:40:07 +08:00

102 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
);
});