mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(telegram): preserve visible draft recovery (#120626)
* fix(telegram): preserve visible draft recovery * fix(telegram): await visible draft send * test(telegram): use const in draft recovery test * test(telegram): add live partial failure proof * test(telegram): register live recovery coverage * ci(qa): support mock Telegram proof scenarios * test(telegram): isolate live recovery fallback * test(telegram): assert live recovery behavior * fix(agents): join partial reply delivery * test(telegram): keep settlement proof at core boundary
This commit is contained in:
committed by
GitHub
parent
917fd92686
commit
0df1a89e3a
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import type { StreamEvent } from "./mock-openai-contracts.js";
|
||||
import {
|
||||
buildAssistantEvents,
|
||||
buildPartialFailureEvents,
|
||||
buildAssistantThenToolCallEvents,
|
||||
buildFailedResponseEvents,
|
||||
buildReasoningAndAssistantEvents,
|
||||
@@ -31,13 +32,35 @@ function readOutputItemSlots(events: StreamEvent[]) {
|
||||
|
||||
describe("mock OpenAI Responses output item slots", () => {
|
||||
it("emits the provider no-details failure used by repeated-request recovery QA", () => {
|
||||
expect(buildFailedResponseEvents()).toEqual([
|
||||
const events = buildFailedResponseEvents();
|
||||
expect(events).toEqual([
|
||||
expect.objectContaining({ type: "response.created" }),
|
||||
expect.objectContaining({
|
||||
type: "response.failed",
|
||||
response: expect.not.objectContaining({ error: expect.anything() }),
|
||||
}),
|
||||
]);
|
||||
expect(events.some((event) => event.type === "response.output_text.delta")).toBe(false);
|
||||
});
|
||||
|
||||
it("emits an unfinished assistant delta before the failed response", () => {
|
||||
const marker = "TELEGRAM-VISIBLE-PARTIAL-BEFORE-FAILURE";
|
||||
const events = buildPartialFailureEvents(marker);
|
||||
|
||||
expect(events.map((event) => event.type)).toEqual([
|
||||
"response.created",
|
||||
"response.output_item.added",
|
||||
"response.output_text.delta",
|
||||
"response.failed",
|
||||
]);
|
||||
expect(events[1]).toMatchObject({
|
||||
item: { type: "message", role: "assistant", status: "in_progress" },
|
||||
});
|
||||
expect(events[2]).toMatchObject({
|
||||
type: "response.output_text.delta",
|
||||
delta: marker,
|
||||
});
|
||||
expect(events.some((event) => event.type === "response.output_item.done")).toBe(false);
|
||||
});
|
||||
|
||||
it("indexes preview deltas and the final answer on the same assistant slot", () => {
|
||||
|
||||
@@ -21,6 +21,40 @@ export function buildFailedResponseEvents(): StreamEvent[] {
|
||||
];
|
||||
}
|
||||
|
||||
export function buildPartialFailureEvents(partialText: string): StreamEvent[] {
|
||||
const responseId = "resp_qa_partial_failed_1";
|
||||
const itemId = "msg_qa_partial_failed_1";
|
||||
return [
|
||||
{ type: "response.created", response: { id: responseId } },
|
||||
{
|
||||
type: "response.output_item.added",
|
||||
output_index: 0,
|
||||
item: {
|
||||
type: "message",
|
||||
id: itemId,
|
||||
role: "assistant",
|
||||
phase: "final_answer",
|
||||
content: [],
|
||||
status: "in_progress",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "response.output_text.delta",
|
||||
item_id: itemId,
|
||||
output_index: 0,
|
||||
content_index: 0,
|
||||
delta: partialText,
|
||||
},
|
||||
{
|
||||
type: "response.failed",
|
||||
response: {
|
||||
id: responseId,
|
||||
status: "failed",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildToolCallEvents(prompt: string): StreamEvent[] {
|
||||
const targetPath = readTargetFromPrompt(prompt);
|
||||
return buildToolCallEventsWithArgs("read", { path: targetPath });
|
||||
|
||||
@@ -219,6 +219,13 @@ function expectOpenAiStreamingResponsesText(server: MockServer, body: Record<str
|
||||
return expectStreamingResponsesText(server, { model: "gpt-5.6-luna", ...body });
|
||||
}
|
||||
|
||||
function parseStreamingResponseEvents(body: string): StreamEvent[] {
|
||||
return body
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("data: {") && line.endsWith("}"))
|
||||
.map((line) => JSON.parse(line.slice("data: ".length)) as StreamEvent);
|
||||
}
|
||||
|
||||
const requireRecord = createRequireRecord("record", "expected-label-capitalized");
|
||||
|
||||
function requireArray(value: unknown, label: string): unknown[] {
|
||||
@@ -913,6 +920,36 @@ describe("qa mock openai server", () => {
|
||||
expect(blockContinuationBody).not.toContain('"item_id":"msg_mock_block_1"');
|
||||
});
|
||||
|
||||
it("serves Telegram visible and unsent failure directives", async () => {
|
||||
const server = await startMockServer();
|
||||
const visibleEvents = parseStreamingResponseEvents(
|
||||
await expectOpenAiStreamingResponsesText(server, {
|
||||
input: [makeUserInput("Telegram visible partial failure QA check")],
|
||||
}),
|
||||
);
|
||||
const unsentEvents = parseStreamingResponseEvents(
|
||||
await expectOpenAiStreamingResponsesText(server, {
|
||||
input: [makeUserInput("Telegram unsent failure QA check")],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(visibleEvents.map((event) => event.type)).toEqual([
|
||||
"response.created",
|
||||
"response.output_item.added",
|
||||
"response.output_text.delta",
|
||||
"response.failed",
|
||||
]);
|
||||
expect(visibleEvents[2]).toMatchObject({
|
||||
type: "response.output_text.delta",
|
||||
delta: "TELEGRAM-VISIBLE-PARTIAL-BEFORE-FAILURE",
|
||||
});
|
||||
expect(unsentEvents.map((event) => event.type)).toEqual([
|
||||
"response.created",
|
||||
"response.failed",
|
||||
]);
|
||||
expect(unsentEvents.some((event) => event.type === "response.output_text.delta")).toBe(false);
|
||||
});
|
||||
|
||||
it("plans deterministic tool-progress reads from prompt paths", async () => {
|
||||
const server = await startMockServer();
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ import {
|
||||
buildQaLongFinalText,
|
||||
buildAssistantThenToolCallEvents,
|
||||
buildAssistantEvents,
|
||||
buildPartialFailureEvents,
|
||||
buildReasoningOnlyEvents,
|
||||
buildReasoningAndAssistantEvents,
|
||||
buildFailedResponseEvents,
|
||||
@@ -288,6 +289,9 @@ const QA_STREAMING_TOOL_PROGRESS_CONTINUATION_RE =
|
||||
/^Continue with (?:the current Matrix QA scenario|the QA scenario plan and report worked, failed, and blocked items)\.$/i;
|
||||
const QA_CODE_MODE_TARGET_MARKER = "qa-code-mode-target:";
|
||||
const QA_FAILED_TOOL_TERMINAL_RECOVERY_PROMPT_RE = /failed tool terminal recovery qa check/i;
|
||||
const QA_TELEGRAM_VISIBLE_PARTIAL_FAILURE_PROMPT_RE = /telegram visible partial failure qa check/i;
|
||||
const QA_TELEGRAM_UNSENT_FAILURE_PROMPT_RE = /telegram unsent failure qa check/i;
|
||||
const QA_TELEGRAM_VISIBLE_PARTIAL_FAILURE_MARKER = "TELEGRAM-VISIBLE-PARTIAL-BEFORE-FAILURE";
|
||||
// Keep each real provider request active long enough for retries to span the
|
||||
// unchanged five-minute recovery bound while remaining below first-byte timeout.
|
||||
const QA_REPEATED_REQUEST_RESPONSE_PAUSE_MS = 110_000;
|
||||
@@ -868,6 +872,12 @@ async function buildResponsesPayload(
|
||||
if (QA_REPEATED_REQUEST_QUEUED_REPLY_PROMPT_RE.test(prompt)) {
|
||||
return buildAssistantEvents(QA_REPEATED_REQUEST_QUEUED_REPLY_MARKER);
|
||||
}
|
||||
if (QA_TELEGRAM_VISIBLE_PARTIAL_FAILURE_PROMPT_RE.test(prompt)) {
|
||||
return buildPartialFailureEvents(QA_TELEGRAM_VISIBLE_PARTIAL_FAILURE_MARKER);
|
||||
}
|
||||
if (QA_TELEGRAM_UNSENT_FAILURE_PROMPT_RE.test(prompt)) {
|
||||
return buildFailedResponseEvents();
|
||||
}
|
||||
if (QA_REPEATED_REQUEST_RECOVERY_PROMPT_RE.test(allInputText)) {
|
||||
return buildFailedResponseEvents();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user