fix(consult): 降级正文过 Pass 4,去重并停止空跑 compose(BUG-959/960/961)
降级交付复用 releasePass4Sentences;每次 attempt 清空 uncontractedText;成功降级后跳过 compose。
This commit is contained in:
@@ -1279,6 +1279,7 @@ test("incomplete runtime contract with body is delivered degraded instead of dis
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
onComplete: () => { completed += 1; },
|
||||
onError: () => { failed += 1; },
|
||||
@@ -1310,6 +1311,7 @@ test("incomplete runtime contract without body still fails closed (BUG-956)", as
|
||||
async function* chunks() {}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
onComplete: () => { completed += 1; },
|
||||
onError: () => { failed += 1; },
|
||||
@@ -1329,6 +1331,157 @@ test("incomplete runtime contract without body still fails closed (BUG-956)", as
|
||||
step.kind === "validation" && step.name === "runtime-contract-incomplete" && step.status === "failed"));
|
||||
});
|
||||
|
||||
test("degraded delivery drops guarantee sentences through Pass 4 (BUG-959)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
let completed = 0;
|
||||
async function* chunks() {
|
||||
yield { type: "text-delta", payload: { text: "我保证你一定会升职。" } };
|
||||
yield { type: "text-delta", payload: { text: "方向上可以推进。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
onComplete: () => { completed += 1; },
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
assert.equal(completed, 1);
|
||||
const answers = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text);
|
||||
assert.equal(answers.some((text) => /一定会升职|我保证/.test(text)), false);
|
||||
assert.match(answers.join(""), /方向上可以推进/);
|
||||
assert.equal(answers.join("").includes(CONTRACT_DEGRADED_NOTE), true);
|
||||
const completedEvent = events.find((event) => (event as { type?: string }).type === "run.completed") as {
|
||||
receipt?: { steps: Array<{ kind: string; name: string; status: string }> };
|
||||
};
|
||||
assert.ok(completedEvent.receipt?.steps.some((step) =>
|
||||
step.kind === "validation" && step.name === "contract-degraded" && step.status === "failed"));
|
||||
assert.ok(completedEvent.receipt?.steps.some((step) =>
|
||||
step.kind === "validation" && step.name === "pass4-reject:guarantee" && step.status === "failed"));
|
||||
});
|
||||
|
||||
test("degraded delivery that Pass 4 empties stays incomplete instead of a note-only answer (BUG-959)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
let completed = 0;
|
||||
let failed = 0;
|
||||
async function* chunks() {
|
||||
yield { type: "text-delta", payload: { text: "我保证你一定会升职。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
onComplete: () => { completed += 1; },
|
||||
onError: () => { failed += 1; },
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
assert.equal(completed, 0);
|
||||
assert.equal(failed, 1);
|
||||
const answer = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text)
|
||||
.join("");
|
||||
assert.equal(answer.includes(CONTRACT_DEGRADED_NOTE), false);
|
||||
const failure = events.find((event) => (event as { type?: string }).type === "run.failed") as {
|
||||
code: string;
|
||||
receipt?: { steps: Array<{ name: string }> };
|
||||
};
|
||||
assert.equal(failure.code, "runtime_contract_incomplete");
|
||||
assert.ok(failure.receipt?.steps.some((step) => step.name === "pass4-reject:guarantee"));
|
||||
assert.ok(failure.receipt?.steps.some((step) => step.name === "runtime-contract-incomplete"));
|
||||
});
|
||||
|
||||
test("degraded delivery uses the refusal when Pass 4 drops every general-mode sentence (BUG-959)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
async function* chunks() {
|
||||
yield { type: "text-delta", payload: { text: "你的上升是巨蟹座。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "general_no_birth_time",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
const answer = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text)
|
||||
.join("");
|
||||
assert.match(answer, new RegExp(GENERAL_NO_BIRTH_TIME_REFUSAL));
|
||||
assert.equal(answer.includes(CONTRACT_DEGRADED_NOTE), true);
|
||||
assert.doesNotMatch(answer, /你的上升是巨蟹座/);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 1);
|
||||
});
|
||||
|
||||
test("degraded delivery keeps only the last attempt body (BUG-960)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
async function* first() {
|
||||
yield { type: "text-delta", payload: { text: "第一段。" } };
|
||||
}
|
||||
async function* second() {
|
||||
yield { type: "text-delta", payload: { text: "第二段。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: first(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
retry: async () => second(),
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
const answer = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text)
|
||||
.join("");
|
||||
assert.match(answer, /第二段/);
|
||||
assert.doesNotMatch(answer, /第一段/);
|
||||
assert.equal(answer.includes(CONTRACT_DEGRADED_NOTE), true);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 1);
|
||||
});
|
||||
|
||||
test("degraded delivery does not start a compose pass (BUG-961)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
let composeCalls = 0;
|
||||
async function* chunks() {
|
||||
yield { type: "text-delta", payload: { text: "方向上可以推进。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
composeAnswer: async () => {
|
||||
composeCalls += 1;
|
||||
async function* composed() {
|
||||
yield { type: "text-delta", payload: { text: "不该出现的 compose。" } };
|
||||
yield { type: "finish", payload: { stepResult: { reason: "stop" }, output: { usage: {}, steps: [{}] } } };
|
||||
}
|
||||
return composed();
|
||||
},
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
assert.equal(composeCalls, 0);
|
||||
assert.equal(events.some((event) => {
|
||||
const item = event as { type?: string; phase?: string };
|
||||
return item.type === "phase.started" && item.phase === "compose";
|
||||
}), false);
|
||||
const answer = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text)
|
||||
.join("");
|
||||
assert.match(answer, /方向上可以推进/);
|
||||
assert.doesNotMatch(answer, /不该出现的 compose/);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 1);
|
||||
});
|
||||
|
||||
test("skill-binding abort is a distinct receipt step from a missing tool call (BUG-955)", async () => {
|
||||
const bindingState = createConsultationRuntimeState();
|
||||
let bindingError = "";
|
||||
|
||||
Reference in New Issue
Block a user