Files
Jyotisha/frontend/tests/rectification-step-answer.test.ts
T
jesse-ux cd4775aec6 fix(consult): BUG-950~953 Pass 4 按句放行,无分钟按句丢弃
正文不再整段 hold:闭合句立刻过 Pass 4,无 reject 即发(950)。
无出生分钟改为按句丢弃,全丢才用兜底句(951);该模式日期记 observe(952)。
校正流 token 级 thinking 死链按 P2 删除,测试翻转成否定合同(953)。
2026-09-18 14:08:45 +08:00

252 lines
9.4 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 {
applyStepAnswerChunk,
createStepAnswerState,
flushStepAnswerOnStreamFinish,
shouldPublishStepText,
} from "../src/lib/rectification-agentic/v9/step-answer.ts";
import { mapStreamChunkToPhase } from "../src/lib/rectification-agentic/v9/stream-mapping.ts";
import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
function isPublicTool(name: string): boolean {
return (PUBLIC_RECTIFICATION_TOOLS as readonly string[]).includes(name);
}
function chunk(type: string, payload?: Record<string, unknown>) {
return { type, payload };
}
test("does not publish intermediate tool-step text as answer.delta", () => {
const state = createStepAnswerState();
applyStepAnswerChunk(state, chunk("text-delta", { text: "Let me set" }), isPublicTool);
applyStepAnswerChunk(state, chunk("tool-call", { toolName: "rectification-record-evidence-batch" }), isPublicTool);
const afterTool = applyStepAnswerChunk(
state,
chunk("tool-result", { toolName: "rectification-record-evidence-batch" }),
isPublicTool,
);
assert.equal(afterTool.kind, "discard");
const live = applyStepAnswerChunk(
state,
chunk("text-delta", { text: "记下了,2020年4月开始实习。" }),
isPublicTool,
);
assert.deepEqual(live, { kind: "live", text: "记下了,2020年4月开始实习。" });
const finish = applyStepAnswerChunk(state, chunk("step-finish", { reason: "stop" }), isPublicTool);
assert.equal(finish.kind, "none");
});
test("never publishes reasoning-delta to the browser", () => {
// 原值:mapStreamChunkToThinking 把中文 reasoning 变成 thinking.delta
// 新值:校正流对 reasoning-delta 必须丢弃;源码不得把它映射成对外事件
// 原因:BUG-953token 级 thinking 是死链且违反 P2provider reasoning 永不外发)
assert.equal(mapStreamChunkToPhase(chunk("reasoning-delta", { text: "Let me" }) as never), null);
assert.equal(mapStreamChunkToPhase(chunk("text-delta", { text: "Let me" }) as never), null);
const state = createStepAnswerState();
assert.equal(
applyStepAnswerChunk(state, chunk("reasoning-delta", { text: "先核对经历。" }), isPublicTool).kind,
"none",
);
const mapping = readFileSync(new URL("../src/lib/rectification-agentic/v9/stream-mapping.ts", import.meta.url), "utf8");
assert.doesNotMatch(mapping, /function mapStreamChunkToThinking|function toPublicThinkingDelta|InternalThinkingDeltaEvent/);
});
test("consecutive Chinese reasoning fragments assemble into a visible thinking line", () => {
// 原值:createThinkingFragmentAssembler 把「先核」+「对经历。」拼成可见思考行
// 新值:分片组装器删除;连续 reasoning-delta 仍全部丢弃
// 原因:BUG-953,翻转成否定合同,测试总数不降
const state = createStepAnswerState();
assert.equal(applyStepAnswerChunk(state, chunk("reasoning-delta", { text: "先核" }), isPublicTool).kind, "none");
assert.equal(applyStepAnswerChunk(state, chunk("reasoning-delta", { text: "对经历。" }), isPublicTool).kind, "none");
const gate = readFileSync(new URL("../src/lib/think-step-gate.ts", import.meta.url), "utf8");
assert.doesNotMatch(gate, /acceptThinkingFragment|createThinkingFragmentAssembler/);
});
test("publishes only the terminal no-tool step as assistant text", () => {
const state = createStepAnswerState();
applyStepAnswerChunk(state, chunk("step-start"), isPublicTool);
applyStepAnswerChunk(state, chunk("text-delta", { text: "_probe" }), isPublicTool);
applyStepAnswerChunk(state, chunk("tool-call", { toolName: "rectification-compare-candidates" }), isPublicTool);
applyStepAnswerChunk(state, chunk("step-finish", { stepResult: { reason: "tool-calls" } }), isPublicTool);
assert.equal(shouldPublishStepText({ calledTool: true, text: "_probe" }, "tool-calls"), false);
applyStepAnswerChunk(state, chunk("step-start"), isPublicTool);
const live = applyStepAnswerChunk(state, chunk("text-delta", { text: "已经记下实习。" }), isPublicTool);
assert.deepEqual(live, { kind: "live", text: "已经记下实习。" });
const finish = flushStepAnswerOnStreamFinish(state, "stop");
assert.equal(finish.kind, "none");
});
test("streams terminal Chinese tokens live after compare", () => {
const state = createStepAnswerState();
applyStepAnswerChunk(
state,
chunk("tool-result", { toolName: "rectification-compare-candidates" }),
isPublicTool,
);
const first = applyStepAnswerChunk(state, chunk("text-delta", { text: "记下了," }), isPublicTool);
const second = applyStepAnswerChunk(
state,
chunk("text-delta", { text: "2020年4月开始实习。" }),
isPublicTool,
);
assert.deepEqual(first, { kind: "live", text: "记下了," });
assert.deepEqual(second, { kind: "live", text: "2020年4月开始实习。" });
assert.equal(applyStepAnswerChunk(state, chunk("step-finish", { reason: "stop" }), isPublicTool).kind, "none");
});
test("retracts a live spoken prefix if that step later calls a public tool", () => {
const state = createStepAnswerState();
applyStepAnswerChunk(
state,
chunk("tool-result", { toolName: "rectification-compare-candidates" }),
isPublicTool,
);
assert.deepEqual(
applyStepAnswerChunk(state, chunk("text-delta", { text: "记下了," }), isPublicTool),
{ kind: "live", text: "记下了," },
);
const retracted = applyStepAnswerChunk(
state,
chunk("tool-call", { toolName: "rectification-read-diagnostics" }),
isPublicTool,
);
assert.equal(retracted.kind, "retract");
applyStepAnswerChunk(
state,
chunk("tool-result", { toolName: "rectification-read-diagnostics" }),
isPublicTool,
);
const spoken = applyStepAnswerChunk(
state,
chunk("text-delta", { text: "诊断已经看过。接下来确认入职年份。" }),
isPublicTool,
);
assert.deepEqual(spoken, { kind: "live", text: "诊断已经看过。接下来确认入职年份。" });
});
test("keeps a live opening greeting when the next public tool is set-focus", () => {
const state = createStepAnswerState();
assert.deepEqual(
applyStepAnswerChunk(
state,
chunk("text-delta", { text: "你好,我们从你最容易想起的经历开始就行。" }),
isPublicTool,
),
{ kind: "live", text: "你好,我们从你最容易想起的经历开始就行。" },
);
assert.equal(
applyStepAnswerChunk(
state,
chunk("tool-call", { toolName: "rectification-set-focus" }),
isPublicTool,
).kind,
"none",
);
assert.equal(
applyStepAnswerChunk(
state,
chunk("tool-result", { toolName: "rectification-set-focus" }),
isPublicTool,
).kind,
"none",
);
// 原值: set-focus 后第二段 live 发布
// 新值: discard
// 原因: BUG-584 决策 2,保留 set-focus 前的问候,丢掉工具结果后的复述
assert.equal(
applyStepAnswerChunk(
state,
chunk("text-delta", { text: "我们慢慢来就好——想到哪件就聊哪件,不用一次说完。" }),
isPublicTool,
).kind,
"discard",
);
});
test("set-focus with no prior spoken text still publishes the later greeting", () => {
const state = createStepAnswerState();
assert.equal(
applyStepAnswerChunk(
state,
chunk("tool-call", { toolName: "rectification-set-focus" }),
isPublicTool,
).kind,
"none",
);
assert.equal(
applyStepAnswerChunk(
state,
chunk("tool-result", { toolName: "rectification-set-focus" }),
isPublicTool,
).kind,
"none",
);
assert.deepEqual(
applyStepAnswerChunk(
state,
chunk("text-delta", { text: "你好,我们从你最容易想起的经历开始就行。" }),
isPublicTool,
),
{ kind: "live", text: "你好,我们从你最容易想起的经历开始就行。" },
);
});
test("set-focus publishes an unfinished CJK remainder instead of dropping it", () => {
const state = createStepAnswerState();
assert.equal(
applyStepAnswerChunk(
state,
chunk("text-delta", { text: "范围已经收到 05:00 到 05:10" }),
isPublicTool,
).kind,
"none",
);
assert.deepEqual(
applyStepAnswerChunk(
state,
chunk("tool-call", { toolName: "rectification-set-focus" }),
isPublicTool,
),
{ kind: "publish", pieces: ["范围已经收到 05:00 到 05:10"] },
);
});
test("English planning before set-focus is still discarded", () => {
const state = createStepAnswerState();
assert.equal(
applyStepAnswerChunk(state, chunk("text-delta", { text: "Let me set the next collect focus" }), isPublicTool).kind,
"none",
);
assert.equal(
applyStepAnswerChunk(
state,
chunk("tool-call", { toolName: "rectification-set-focus" }),
isPublicTool,
).kind,
"discard",
);
});
test("does not live-publish English planning before a tool-call", () => {
const state = createStepAnswerState();
assert.equal(
applyStepAnswerChunk(state, chunk("text-delta", { text: "Let me set" }), isPublicTool).kind,
"none",
);
assert.equal(
applyStepAnswerChunk(state, chunk("text-delta", { text: " _probe" }), isPublicTool).kind,
"none",
);
assert.equal(
applyStepAnswerChunk(
state,
chunk("tool-call", { toolName: "rectification-record-evidence-batch" }),
isPublicTool,
).kind,
"none",
);
});