mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 03:59:50 +00:00
test: harden live QA retry handling
This commit is contained in:
@@ -142,6 +142,7 @@ type WhatsAppCredentialHeartbeat = ReturnType<typeof startQaCredentialLeaseHeart
|
||||
|
||||
const WHATSAPP_QA_CAPTURE_CONTENT_ENV = "OPENCLAW_QA_WHATSAPP_CAPTURE_CONTENT";
|
||||
const QA_REDACT_PUBLIC_METADATA_ENV = "OPENCLAW_QA_REDACT_PUBLIC_METADATA";
|
||||
const WHATSAPP_QA_TRANSIENT_DRIVER_ATTEMPTS = 3;
|
||||
const WHATSAPP_QA_ENV_KEYS = [
|
||||
"OPENCLAW_QA_WHATSAPP_DRIVER_PHONE_E164",
|
||||
"OPENCLAW_QA_WHATSAPP_SUT_PHONE_E164",
|
||||
@@ -474,6 +475,14 @@ function isTransientWhatsAppQaDriverError(error: unknown) {
|
||||
return /\bConnection Closed\b/iu.test(formatErrorMessage(error));
|
||||
}
|
||||
|
||||
async function restartWhatsAppQaDriverSession(params: {
|
||||
authDir: string;
|
||||
current: WhatsAppQaDriverSession;
|
||||
}) {
|
||||
await params.current.close().catch(() => {});
|
||||
return await startWhatsAppQaDriverSession({ authDir: params.authDir });
|
||||
}
|
||||
|
||||
async function runWhatsAppScenario(params: {
|
||||
driver: WhatsAppQaDriverSession;
|
||||
driverPhoneE164: string;
|
||||
@@ -772,7 +781,7 @@ export async function runWhatsAppQaLive(params: {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let retriedDriver = false;
|
||||
let driverAttempt = 1;
|
||||
while (true) {
|
||||
try {
|
||||
const result = await runWhatsAppScenario({
|
||||
@@ -792,17 +801,31 @@ export async function runWhatsAppQaLive(params: {
|
||||
sutPhoneE164: runtimeEnv.sutPhoneE164,
|
||||
});
|
||||
scenarioResults.push(
|
||||
retriedDriver
|
||||
? { ...result, details: `${result.details}; driver reconnected` }
|
||||
driverAttempt > 1
|
||||
? {
|
||||
...result,
|
||||
details: `${result.details}; driver reconnected ${driverAttempt - 1}x`,
|
||||
}
|
||||
: result,
|
||||
);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (!retriedDriver && isTransientWhatsAppQaDriverError(error)) {
|
||||
retriedDriver = true;
|
||||
await activeDriver.close().catch(() => {});
|
||||
activeDriver = await startWhatsAppQaDriverSession({ authDir: driverAuthDir });
|
||||
driver = activeDriver;
|
||||
if (
|
||||
driverAttempt < WHATSAPP_QA_TRANSIENT_DRIVER_ATTEMPTS &&
|
||||
isTransientWhatsAppQaDriverError(error)
|
||||
) {
|
||||
driverAttempt += 1;
|
||||
try {
|
||||
activeDriver = await restartWhatsAppQaDriverSession({
|
||||
authDir: driverAuthDir,
|
||||
current: activeDriver,
|
||||
});
|
||||
driver = activeDriver;
|
||||
} catch (restartError) {
|
||||
if (!isTransientWhatsAppQaDriverError(restartError)) {
|
||||
throw restartError;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
preservedGatewayDebugArtifacts = true;
|
||||
|
||||
@@ -613,6 +613,30 @@ async function runMatrixStreamingPreviewScenario(
|
||||
since: startSince,
|
||||
timeoutMs: context.timeoutMs,
|
||||
});
|
||||
if (preview.event.body === params.finalText) {
|
||||
advanceMatrixQaActorCursor({
|
||||
actorId: "driver",
|
||||
syncState: context.syncState,
|
||||
nextSince: preview.since,
|
||||
startSince,
|
||||
});
|
||||
const finalReply = buildMatrixReplyArtifact(preview.event, params.finalText);
|
||||
return {
|
||||
artifacts: {
|
||||
driverEventId,
|
||||
previewEventId: undefined,
|
||||
reply: finalReply,
|
||||
token: params.finalText,
|
||||
triggerBody,
|
||||
},
|
||||
details: [
|
||||
`driver event: ${driverEventId}`,
|
||||
`scenario: ${params.label}`,
|
||||
"preview event: <none>; final delivered without draft replacement",
|
||||
...buildMatrixReplyDetails("final reply", finalReply),
|
||||
].join("\n"),
|
||||
} satisfies MatrixQaScenarioExecution;
|
||||
}
|
||||
const finalized = await client.waitForRoomEvent({
|
||||
observedEvents: context.observedEvents,
|
||||
predicate: (event) =>
|
||||
|
||||
@@ -2991,6 +2991,47 @@ describe("matrix live qa scenarios", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts final-only partial streaming replies without a draft replacement", async () => {
|
||||
const fallbackFinalText = "MATRIX_QA_PARTIAL_STREAM_PREVIEW_COMPLETE";
|
||||
const { sendTextMessage, waitForRoomEvent } = mockMatrixQaRoomClient({
|
||||
driverEventId: "$partial-stream-trigger",
|
||||
events: [
|
||||
{
|
||||
event: ({ sendTextMessage }) =>
|
||||
matrixQaMessageEvent({
|
||||
kind: "message",
|
||||
eventId: "$partial-final-only",
|
||||
body: readMatrixQaReplyDirective(
|
||||
mockMessageBody(sendTextMessage, "sendTextMessage"),
|
||||
fallbackFinalText,
|
||||
),
|
||||
}),
|
||||
since: "driver-sync-final",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const scenario = requireMatrixQaScenario("matrix-room-partial-streaming-preview");
|
||||
|
||||
const result = await runMatrixQaScenario(scenario, matrixQaScenarioContext());
|
||||
const artifacts = result.artifacts as {
|
||||
driverEventId?: unknown;
|
||||
previewEventId?: unknown;
|
||||
reply?: { eventId?: unknown };
|
||||
};
|
||||
expect(artifacts.driverEventId).toBe("$partial-stream-trigger");
|
||||
expect(artifacts.previewEventId).toBeUndefined();
|
||||
expect(artifacts.reply?.eventId).toBe("$partial-final-only");
|
||||
expect(result.details).toContain("final delivered without draft replacement");
|
||||
expect(waitForRoomEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
expectSentTextMessage(sendTextMessage, {
|
||||
bodyIncludes: "Partial streaming QA check",
|
||||
mentionUserIds: ["@sut:matrix-qa.test"],
|
||||
roomId: "!main:matrix-qa.test",
|
||||
});
|
||||
});
|
||||
|
||||
it("captures Matrix tool progress inside the quiet preview before finalizing", async () => {
|
||||
const previewEventId = "$tool-progress-preview";
|
||||
const { sendTextMessage } = mockMatrixQaRoomClient({
|
||||
|
||||
Reference in New Issue
Block a user