From 78b4a4afedf33e39a1b64eee07ffa7543abe79ef Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Sat, 18 Jul 2026 16:33:38 +0800
Subject: [PATCH] fix(qa-lab): bound WhatsApp auth archive extraction (#109501)
---
.../whatsapp/whatsapp-live.runtime.test.ts | 20 +++++++++++++++++++
.../whatsapp/whatsapp-live.setup.ts | 11 ++++++++--
2 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts
index 0b02f9c79ae0..86db047d1f2d 100644
--- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts
+++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts
@@ -26,6 +26,16 @@ import {
import { getWhatsAppQaScenarioDefinition } from "./whatsapp-live.scenarios.js";
import { unpackWhatsAppAuthArchive } from "./whatsapp-live.setup.js";
+const runExecSpy = vi.hoisted(() =>
+ vi.fn(),
+);
+
+vi.mock("openclaw/plugin-sdk/process-runtime", async (importOriginal) => {
+ const actual = await importOriginal();
+ runExecSpy.mockImplementation(actual.runExec);
+ return { ...actual, runExec: runExecSpy };
+});
+
const testing = {
buildWhatsAppQaConfig,
callWhatsAppGatewayMessageAction,
@@ -233,6 +243,7 @@ describe("WhatsApp QA live runtime", () => {
it("unpacks auth archives into a caller-provided temp directory", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wa-qa-test-"));
try {
+ runExecSpy.mockClear();
const archiveBase64 = await createTgz({
root: tempRoot,
entries: {
@@ -249,6 +260,15 @@ describe("WhatsApp QA live runtime", () => {
await expect(fs.readFile(path.join(authDir, "session/key.json"), "utf8")).resolves.toBe(
"{}\n",
);
+ const archivePath = path.join(tempRoot, "driver.tgz");
+ const execOptions = { logOutput: false, timeoutMs: 60_000 };
+ expect(runExecSpy).toHaveBeenNthCalledWith(1, "tar", ["-tzf", archivePath], execOptions);
+ expect(runExecSpy).toHaveBeenNthCalledWith(
+ 2,
+ "tar",
+ ["-xzf", archivePath, "-C", authDir],
+ execOptions,
+ );
} finally {
await fs.rm(tempRoot, { recursive: true, force: true });
}
diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.setup.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.setup.ts
index 0bf4bad55ed3..c454679d0c82 100644
--- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.setup.ts
+++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.setup.ts
@@ -7,6 +7,7 @@ import type { WhatsAppQaGateway } from "./whatsapp-live.contracts.js";
const WHATSAPP_QA_READY_TIMEOUT_MS = 150_000;
const WHATSAPP_QA_READY_STABILITY_MS = 20_000;
+const WHATSAPP_QA_AUTH_ARCHIVE_TIMEOUT_MS = 60_000;
const WHATSAPP_QA_SIGNAL_SESSION_FILE_RE = /^session-[^/\\]+\.json$/u;
type WhatsAppChannelStatus = {
@@ -114,7 +115,10 @@ export async function waitForWhatsAppChannelStable(gateway: WhatsAppQaGateway, a
}
async function listTarEntries(archivePath: string): Promise {
- const { stdout } = await runExec("tar", ["-tzf", archivePath], { logOutput: false });
+ const { stdout } = await runExec("tar", ["-tzf", archivePath], {
+ logOutput: false,
+ timeoutMs: WHATSAPP_QA_AUTH_ARCHIVE_TIMEOUT_MS,
+ });
return normalizeStringEntries(stdout.split("\n"));
}
@@ -141,7 +145,10 @@ export async function unpackWhatsAppAuthArchive(params: {
await fs.writeFile(archivePath, Buffer.from(params.archiveBase64, "base64"), { mode: 0o600 });
const entries = await listTarEntries(archivePath);
assertSafeArchiveEntries(entries);
- await runExec("tar", ["-xzf", archivePath, "-C", authDir], { logOutput: false });
+ await runExec("tar", ["-xzf", archivePath, "-C", authDir], {
+ logOutput: false,
+ timeoutMs: WHATSAPP_QA_AUTH_ARCHIVE_TIMEOUT_MS,
+ });
await fs.rm(archivePath, { force: true });
if (params.clearSignalSessions === true) {
await clearWhatsAppAuthSignalSessions(authDir);