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 285b86a3d794..82d587a7ee29 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 @@ -255,6 +255,67 @@ describe("WhatsApp QA live runtime", () => { expect(report).not.toContain("+15550000002"); }); + it("publishes WhatsApp gateway debug artifacts only when files exist", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wa-debug-test-")); + const debugDir = path.join(tempRoot, "gateway-debug"); + try { + await expect(testing.hasWhatsAppGatewayDebugArtifacts(debugDir)).resolves.toBe(false); + await fs.mkdir(debugDir); + await expect(testing.hasWhatsAppGatewayDebugArtifacts(debugDir)).resolves.toBe(false); + await fs.writeFile(path.join(debugDir, "gateway.stderr.log"), "stderr\n"); + await expect(testing.hasWhatsAppGatewayDebugArtifacts(debugDir)).resolves.toBe(true); + } finally { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + + it("redacts published WhatsApp run output without advertising empty debug artifacts", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wa-publish-test-")); + const debugDir = path.join(tempRoot, "gateway-debug"); + try { + await fs.mkdir(debugDir); + const emptyDebugView = await testing.buildPublishedWhatsAppQaRunView({ + cleanupIssues: [ + "WhatsApp QA failed before scenario completion: private setup failure details", + ], + gatewayDebugDirPath: debugDir, + preservedGatewayDebugArtifacts: true, + redactMetadata: true, + scenarioResults: [ + { + id: "whatsapp-canary", + title: "WhatsApp DM canary", + standardId: "canary", + status: "fail", + details: "private setup failure details", + }, + ], + }); + + expect(emptyDebugView.gatewayDebugDirPath).toBeUndefined(); + expect(emptyDebugView.cleanupIssues).toEqual([ + "WhatsApp QA failed before scenario completion: " + + "details redacted (OPENCLAW_QA_REDACT_PUBLIC_METADATA=1)", + ]); + expect(emptyDebugView.scenarioResults[0]?.details).toBe( + "details redacted (OPENCLAW_QA_REDACT_PUBLIC_METADATA=1)", + ); + + await fs.writeFile(path.join(debugDir, "gateway.stderr.log"), "stderr\n"); + await expect( + testing.buildPublishedWhatsAppQaRunView({ + cleanupIssues: [], + gatewayDebugDirPath: debugDir, + preservedGatewayDebugArtifacts: true, + redactMetadata: true, + scenarioResults: [], + }), + ).resolves.toMatchObject({ gatewayDebugDirPath: debugDir }); + } finally { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + it("redacts published scenario details before rendering public artifacts", () => { const publishedScenarios = testing.redactWhatsAppQaScenarioResults([ { diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.ts index f9089e9c5871..dada0212b62c 100644 --- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.ts @@ -31,7 +31,6 @@ import { } from "../shared/credential-lease.runtime.js"; import { appendQaLiveLaneIssue as appendLiveLaneIssue, - buildQaLiveLaneArtifactsError as buildLiveLaneArtifactsError, redactQaLiveLaneDetails, redactQaLiveLaneIssues, } from "../shared/live-artifacts.js"; @@ -2950,6 +2949,43 @@ function appendPreScenarioFailureResults(params: { } } +async function hasWhatsAppGatewayDebugArtifacts(gatewayDebugDirPath: string) { + try { + const entries = await fs.readdir(gatewayDebugDirPath); + return entries.length > 0; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return false; + } + throw error; + } +} + +async function buildPublishedWhatsAppQaRunView(params: { + cleanupIssues: string[]; + gatewayDebugDirPath: string; + preservedGatewayDebugArtifacts: boolean; + redactMetadata: boolean; + scenarioResults: WhatsAppQaScenarioResult[]; +}) { + const publishedCleanupIssues = params.redactMetadata + ? redactQaLiveLaneIssues(params.cleanupIssues) + : params.cleanupIssues; + const publishedScenarioResults = params.redactMetadata + ? redactWhatsAppQaScenarioResults(params.scenarioResults) + : params.scenarioResults; + const gatewayDebugDirPath = + params.preservedGatewayDebugArtifacts && + (await hasWhatsAppGatewayDebugArtifacts(params.gatewayDebugDirPath)) + ? params.gatewayDebugDirPath + : undefined; + return { + cleanupIssues: publishedCleanupIssues, + gatewayDebugDirPath, + scenarioResults: publishedScenarioResults, + }; +} + function formatWhatsAppScenarioProgressLine(params: { details?: string; index: number; @@ -3171,17 +3207,7 @@ export async function runWhatsAppQaLive(params: { } } } catch (error) { - cleanupIssues.push( - buildLiveLaneArtifactsError({ - heading: "WhatsApp QA failed before scenario completion.", - details: [formatErrorMessage(error)], - artifacts: { - gatewayDebug: gatewayDebugDirPath, - }, - }), - ); - preservedGatewayDebugArtifacts = true; - await fs.mkdir(gatewayDebugDirPath, { recursive: true }).catch(() => {}); + appendLiveLaneIssue(cleanupIssues, "WhatsApp QA failed before scenario completion", error); appendPreScenarioFailureResults({ details: formatErrorMessage(error), scenarioResults, @@ -3221,19 +3247,20 @@ export async function runWhatsAppQaLive(params: { const summaryPath = path.join(outputDir, QA_EVIDENCE_FILENAME); const observedMessagesPath = path.join(outputDir, "whatsapp-qa-observed-messages.json"); const credentialFingerprint = fingerprintQaCredentialId(credentialLease?.credentialId); - const publishedCleanupIssues = redactPublicMetadata - ? redactQaLiveLaneIssues(cleanupIssues) - : cleanupIssues; - const publishedScenarioResults = redactPublicMetadata - ? redactWhatsAppQaScenarioResults(scenarioResults) - : scenarioResults; + const publishedRunView = await buildPublishedWhatsAppQaRunView({ + cleanupIssues, + gatewayDebugDirPath, + preservedGatewayDebugArtifacts, + redactMetadata: redactPublicMetadata, + scenarioResults, + }); const evidence = buildLiveTransportEvidenceSummary({ artifactPaths: [ { kind: "summary", path: path.basename(summaryPath) }, { kind: "report", path: path.basename(reportPath) }, { kind: "transport-observations", path: path.basename(observedMessagesPath) }, ], - checks: publishedScenarioResults.map(({ standardId, ...check }) => ({ + checks: publishedRunView.scenarioResults.map(({ standardId, ...check }) => ({ ...check, coverageIds: standardId ? [`channels.whatsapp.${standardId}`] : undefined, })), @@ -3259,13 +3286,13 @@ export async function runWhatsAppQaLive(params: { await fs.writeFile( reportPath, `${renderWhatsAppQaMarkdown({ - cleanupIssues: publishedCleanupIssues, + cleanupIssues: publishedRunView.cleanupIssues, credentialFingerprint, credentialSource: credentialLease?.source ?? requestedCredentialSource, finishedAt, - gatewayDebugDirPath: preservedGatewayDebugArtifacts ? gatewayDebugDirPath : undefined, + gatewayDebugDirPath: publishedRunView.gatewayDebugDirPath, redactMetadata: redactPublicMetadata, - scenarios: publishedScenarioResults, + scenarios: publishedRunView.scenarioResults, startedAt, sutPhoneE164: runtimeEnv?.sutPhoneE164, })}\n`, @@ -3275,7 +3302,7 @@ export async function runWhatsAppQaLive(params: { reportPath, summaryPath, observedMessagesPath, - gatewayDebugDirPath: preservedGatewayDebugArtifacts ? gatewayDebugDirPath : undefined, + gatewayDebugDirPath: publishedRunView.gatewayDebugDirPath, scenarios: scenarioResults, }; } @@ -3283,6 +3310,7 @@ export async function runWhatsAppQaLive(params: { export const testing = { assertSafeArchiveEntries, appendPreScenarioFailureResults, + buildPublishedWhatsAppQaRunView, buildWhatsAppQaConfig, callWhatsAppGatewayMessageAction, callWhatsAppGatewayPoll, @@ -3296,6 +3324,7 @@ export const testing = { formatWhatsAppScenarioProgressLine, fingerprintWhatsAppCredentialId: fingerprintQaCredentialId, formatWhatsAppScenarioWaitDiagnostics, + hasWhatsAppGatewayDebugArtifacts, isTransientWhatsAppQaDriverError, matchesWhatsAppApprovalResolvedText, parseWhatsAppQaCredentialPayload, diff --git a/src/commands/migrate/apply.test.ts b/src/commands/migrate/apply.test.ts index a65007c682af..2572e39d909c 100644 --- a/src/commands/migrate/apply.test.ts +++ b/src/commands/migrate/apply.test.ts @@ -9,9 +9,14 @@ import { runMigrationApply } from "./apply.js"; const stateDir = mkdtempSync(path.join(tmpdir(), "openclaw-migrate-apply-")); -vi.mock("../../config/paths.js", () => ({ - resolveStateDir: () => stateDir, -})); +vi.mock("../../config/paths.js", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + resolveGatewayPort: () => 18789, + resolveStateDir: () => stateDir, + }; +}); function buildEmptyPlan(): MigrationPlan { return {