正文不再整段 hold:闭合句立刻过 Pass 4,无 reject 即发(950)。 无出生分钟改为按句丢弃,全丢才用兜底句(951);该模式日期记 observe(952)。 校正流 token 级 thinking 死链按 P2 删除,测试翻转成否定合同(953)。
252 lines
9.4 KiB
TypeScript
252 lines
9.4 KiB
TypeScript
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-953,token 级 thinking 是死链且违反 P2(provider 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",
|
||
);
|
||
});
|