From fd5fc060070d674c6d88ca65a30b6612eae885b5 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sat, 25 Apr 2026 17:30:05 -0400 Subject: [PATCH] fix(qa-matrix): feed recovery keys through stdin --- .../contract/scenario-runtime-cli.test.ts | 68 +++++++++++++++++++ .../runners/contract/scenario-runtime-cli.ts | 14 +++- .../scenario-runtime-e2ee-destructive.ts | 37 +++++----- .../runners/contract/scenario-runtime-e2ee.ts | 31 +++++---- .../src/runners/contract/scenarios.test.ts | 15 ++-- 5 files changed, 127 insertions(+), 38 deletions(-) diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts index f5922e8f34d..524301d940d 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts @@ -7,6 +7,7 @@ import { redactMatrixQaCliOutput, resolveMatrixQaOpenClawCliEntryPath, runMatrixQaOpenClawCli, + startMatrixQaOpenClawCli, } from "./scenario-runtime-cli.js"; describe("Matrix QA CLI runtime", () => { @@ -72,4 +73,71 @@ describe("Matrix QA CLI runtime", () => { await rm(root, { force: true, recursive: true }); } }); + + it("can pass stdin to CLI commands", async () => { + const root = await mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "matrix-qa-cli-stdin-")); + try { + await mkdir(path.join(root, "dist")); + await writeFile( + path.join(root, "dist", "index.mjs"), + [ + "let input = '';", + "process.stdin.setEncoding('utf8');", + "process.stdin.on('data', (chunk) => { input += chunk; });", + "process.stdin.on('end', () => {", + " process.stdout.write(JSON.stringify({ input: input.trim() }));", + "});", + ].join("\n"), + ); + const result = await runMatrixQaOpenClawCli({ + args: ["matrix", "verify", "backup", "restore", "--recovery-key-stdin", "--json"], + cwd: root, + env: process.env, + stdin: "stdin-recovery-key\n", + timeoutMs: 5_000, + }); + expect(result.stdout).toContain('"input":"stdin-recovery-key"'); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + it("can close stdin after interactive CLI prompts", async () => { + const root = await mkdtemp( + path.join(resolvePreferredOpenClawTmpDir(), "matrix-qa-cli-interactive-"), + ); + try { + await mkdir(path.join(root, "dist")); + await writeFile( + path.join(root, "dist", "index.mjs"), + [ + "let input = '';", + "process.stdin.setEncoding('utf8');", + "process.stdin.on('data', (chunk) => { input += chunk; process.stdout.write('prompt answered\\n'); });", + "process.stdin.on('end', () => {", + " process.stdout.write(JSON.stringify({ input: input.trim(), ended: true }));", + "});", + ].join("\n"), + ); + const session = startMatrixQaOpenClawCli({ + args: ["matrix", "verify", "self"], + cwd: root, + env: process.env, + timeoutMs: 5_000, + }); + await session.writeStdin("yes\n"); + await session.waitForOutput( + (output) => output.text.includes("prompt answered"), + "interactive prompt acknowledgement", + 5_000, + ); + session.endStdin(); + const result = await session.wait(); + + expect(result.stdout).toContain('"input":"yes"'); + expect(result.stdout).toContain('"ended":true'); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); }); diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts index d8f3a06d3a4..1b01b6a8356 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts @@ -17,6 +17,7 @@ export type MatrixQaCliRunResult = { export type MatrixQaCliSession = { args: string[]; + endStdin: () => void; output: () => { stderr: string; stdout: string }; wait: () => Promise; waitForOutput: ( @@ -95,6 +96,7 @@ export function startMatrixQaOpenClawCli(params: { args: string[]; cwd?: string; env: NodeJS.ProcessEnv; + stdin?: string; timeoutMs: number; }): MatrixQaCliSession { const cwd = params.cwd ?? process.cwd(); @@ -150,6 +152,9 @@ export function startMatrixQaOpenClawCli(params: { child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk))); child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk))); + if (params.stdin !== undefined) { + child.stdin.end(params.stdin); + } child.on("error", (error) => { clearTimeout(timeout); finish( @@ -177,6 +182,11 @@ export function startMatrixQaOpenClawCli(params: { return { args: params.args, + endStdin: () => { + if (!child.stdin.destroyed) { + child.stdin.end(); + } + }, output: readOutput, wait: async () => await new Promise((resolve, reject) => { @@ -230,6 +240,7 @@ export async function runMatrixQaOpenClawCli(params: { args: string[]; cwd?: string; env: NodeJS.ProcessEnv; + stdin?: string; timeoutMs: number; }): Promise { return await startMatrixQaOpenClawCli(params).wait(); @@ -327,12 +338,13 @@ export async function createMatrixQaOpenClawCliRuntime(params: { }, run: async ( args: string[], - opts: { allowNonZero?: boolean; timeoutMs: number }, + opts: { allowNonZero?: boolean; stdin?: string; timeoutMs: number }, ): Promise => await runMatrixQaOpenClawCli({ allowNonZero: opts.allowNonZero, args, env, + stdin: opts.stdin, timeoutMs: opts.timeoutMs, }), start: (args: string[], opts: { allowNonZero?: boolean; timeoutMs: number }) => diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee-destructive.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee-destructive.ts index 7c5f2f1e629..e5507abc82d 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee-destructive.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee-destructive.ts @@ -303,10 +303,12 @@ async function runMatrixQaCliJson(params: { decode?: (payload: unknown) => T; label: string; runtime: MatrixQaCliRuntime; + stdin?: string; timeoutMs: number; }) { const result = await params.runtime.run(params.args, { allowNonZero: params.allowNonZero, + stdin: params.stdin, timeoutMs: params.timeoutMs, }); const artifacts = await writeMatrixQaCliArtifacts({ @@ -696,12 +698,12 @@ export async function runMatrixQaE2eeStateLossExternalRecoveryKeyScenario( "restore", "--account", "external-key", - "--recovery-key", - setup.encodedRecoveryKey, + "--recovery-key-stdin", "--json", ], label: "restore-with-external-key", runtime: cli, + stdin: `${setup.encodedRecoveryKey}\n`, timeoutMs: context.timeoutMs, }); assertMatrixQaCliBackupRestoreSucceeded(restored.payload, "external recovery-key"); @@ -711,13 +713,14 @@ export async function runMatrixQaE2eeStateLossExternalRecoveryKeyScenario( "matrix", "verify", "device", - setup.encodedRecoveryKey, + "--recovery-key-stdin", "--account", "external-key", "--json", ], label: "verify-device-diagnostics", runtime: cli, + stdin: `${setup.encodedRecoveryKey}\n`, timeoutMs: context.timeoutMs, }); const backupKeyLoaded = @@ -835,12 +838,12 @@ export async function runMatrixQaE2eeStateLossStoredRecoveryKeyScenario( "restore", "--account", "stored-key", - "--recovery-key", - setup.encodedRecoveryKey, + "--recovery-key-stdin", "--json", ], label: "initial-restore-stores-key", runtime: cli, + stdin: `${setup.encodedRecoveryKey}\n`, timeoutMs: context.timeoutMs, }); assertMatrixQaCliBackupRestoreSucceeded(initial.payload, "initial stored-key"); @@ -976,12 +979,12 @@ export async function runMatrixQaE2eeStaleRecoveryKeyAfterBackupResetScenario( "restore", "--account", "stale-key", - "--recovery-key", - setup.encodedRecoveryKey, + "--recovery-key-stdin", "--json", ], label: "restore-with-stale-key", runtime: cli, + stdin: `${setup.encodedRecoveryKey}\n`, timeoutMs: context.timeoutMs, }); assertMatrixQaCliBackupRestoreFailed(restored.payload, "stale recovery-key restore"); @@ -1078,12 +1081,12 @@ async function waitForMatrixQaNonEmptyCliBackupRestore(params: { "restore", "--account", params.accountId, - "--recovery-key", - params.recoveryKey, + "--recovery-key-stdin", "--json", ], label: params.label, runtime: params.cli, + stdin: `${params.recoveryKey}\n`, timeoutMs: Math.max(1, remainingMs), }); last = restored; @@ -1197,12 +1200,12 @@ export async function runMatrixQaE2eeCorruptCryptoIdbSnapshotScenario( "restore", "--account", "corrupt-idb", - "--recovery-key", - setup.encodedRecoveryKey, + "--recovery-key-stdin", "--json", ], label: "initial-restore-before-corruption", runtime: cli, + stdin: `${setup.encodedRecoveryKey}\n`, timeoutMs: context.timeoutMs, }); assertMatrixQaCliBackupRestoreSucceeded(initial.payload, "corrupt-idb initial restore"); @@ -1219,12 +1222,12 @@ export async function runMatrixQaE2eeCorruptCryptoIdbSnapshotScenario( "restore", "--account", "corrupt-idb", - "--recovery-key", - setup.encodedRecoveryKey, + "--recovery-key-stdin", "--json", ], label: "restore-after-idb-corruption", runtime: cli, + stdin: `${setup.encodedRecoveryKey}\n`, timeoutMs: context.timeoutMs, }); assertMatrixQaCliBackupRestoreSucceeded(repaired.payload, "corrupt-idb recovery"); @@ -1273,12 +1276,12 @@ export async function runMatrixQaE2eeServerDeviceDeletedLocalStateIntactScenario "restore", "--account", "deleted-device", - "--recovery-key", - setup.encodedRecoveryKey, + "--recovery-key-stdin", "--json", ], label: "restore-before-device-delete", runtime: cli, + stdin: `${setup.encodedRecoveryKey}\n`, timeoutMs: context.timeoutMs, }); assertMatrixQaCliBackupRestoreSucceeded(restored.payload, "deleted-device preflight"); @@ -1454,12 +1457,12 @@ export async function runMatrixQaE2eeWrongAccountRecoveryKeyScenario( "restore", "--account", "wrong-account", - "--recovery-key", - driverSetup.encodedRecoveryKey, + "--recovery-key-stdin", "--json", ], label: "restore-with-wrong-account-key", runtime: cli, + stdin: `${driverSetup.encodedRecoveryKey}\n`, timeoutMs: context.timeoutMs, }); assertMatrixQaCliBackupRestoreFailed(restored.payload, "wrong-account recovery-key restore"); diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee.ts index c2facb98b75..ddc06c0a210 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee.ts @@ -458,10 +458,11 @@ async function createMatrixQaCliSelfVerificationRuntime(params: { OPENCLAW_DISABLE_AUTO_UPDATE: "1", OPENCLAW_STATE_DIR: stateDir, }; - const run = async (args: string[], timeoutMs = params.context.timeoutMs) => + const run = async (args: string[], timeoutMs = params.context.timeoutMs, stdin?: string) => await runMatrixQaOpenClawCli({ args, env, + stdin, timeoutMs, }); const start = (args: string[], timeoutMs = params.context.timeoutMs) => @@ -1227,17 +1228,20 @@ export async function runMatrixQaE2eeCliSelfVerificationScenario( userId: cliDevice.userId, }); try { - const restoreResult = await cli.run([ - "matrix", - "verify", - "backup", - "restore", - "--account", - accountId, - "--recovery-key", - encodedRecoveryKey, - "--json", - ]); + const restoreResult = await cli.run( + [ + "matrix", + "verify", + "backup", + "restore", + "--account", + accountId, + "--recovery-key-stdin", + "--json", + ], + context.timeoutMs, + `${encodedRecoveryKey}\n`, + ); const restoreArtifacts = await writeMatrixQaCliOutputArtifacts({ label: "verify-backup-restore", result: restoreResult, @@ -1310,8 +1314,9 @@ export async function runMatrixQaE2eeCliSelfVerificationScenario( cliSas, owner: ownerSas, }); - await session.writeStdin("yes\n"); await owner.confirmVerificationSas(ownerSas.id); + await session.writeStdin("yes\n"); + session.endStdin(); const completedCli = await session.wait(); const selfVerificationArtifacts = await writeMatrixQaCliOutputArtifacts({ label: "verify-self", diff --git a/extensions/qa-matrix/src/runners/contract/scenarios.test.ts b/extensions/qa-matrix/src/runners/contract/scenarios.test.ts index 24acf8e13d8..fd828b6ee51 100644 --- a/extensions/qa-matrix/src/runners/contract/scenarios.test.ts +++ b/extensions/qa-matrix/src/runners/contract/scenarios.test.ts @@ -3107,8 +3107,10 @@ describe("matrix live qa scenarios", () => { "Verification id: verification-1\nCompleted: yes\nDevice verified by owner: yes\nCross-signing verified: yes\n", }); const kill = vi.fn(); + const endStdin = vi.fn(); startMatrixQaOpenClawCli.mockReturnValue({ args: ["matrix", "verify", "self", "--account", "cli"], + endStdin, kill, output: vi.fn(() => ({ stderr: "", stdout: "" })), wait, @@ -3116,7 +3118,7 @@ describe("matrix live qa scenarios", () => { writeStdin, }); let cliAccountConfigDuringRun: Record | null = null; - runMatrixQaOpenClawCli.mockImplementation(async ({ args, env }) => { + runMatrixQaOpenClawCli.mockImplementation(async ({ args, env, stdin }) => { if (!cliAccountConfigDuringRun && env.OPENCLAW_CONFIG_PATH) { const cliConfig = JSON.parse( await readFile(String(env.OPENCLAW_CONFIG_PATH), "utf8"), @@ -3158,10 +3160,8 @@ describe("matrix live qa scenarios", () => { }), }; } - if ( - joined === - "matrix verify backup restore --account cli --recovery-key encoded-recovery-key --json" - ) { + if (joined === "matrix verify backup restore --account cli --recovery-key-stdin --json") { + expect(stdin).toBe("encoded-recovery-key\n"); return { args, exitCode: 0, @@ -3216,6 +3216,7 @@ describe("matrix live qa scenarios", () => { ]); expect(waitForOutput).toHaveBeenCalledTimes(2); expect(writeStdin).toHaveBeenCalledWith("yes\n"); + expect(endStdin).toHaveBeenCalledTimes(1); expect(wait).toHaveBeenCalledTimes(1); expect(kill).toHaveBeenCalledTimes(1); expect(runMatrixQaOpenClawCli).toHaveBeenCalledTimes(2); @@ -3227,12 +3228,12 @@ describe("matrix live qa scenarios", () => { "restore", "--account", "cli", - "--recovery-key", - "encoded-recovery-key", + "--recovery-key-stdin", "--json", ], ["matrix", "verify", "status", "--account", "cli", "--json"], ]); + expect(runMatrixQaOpenClawCli.mock.calls[0]?.[0].stdin).toBe("encoded-recovery-key\n"); const cliEnv = startMatrixQaOpenClawCli.mock.calls[0]?.[0].env; expect(cliEnv?.OPENCLAW_STATE_DIR).toContain("openclaw-matrix-cli-qa-"); expect(cliEnv?.OPENCLAW_CONFIG_PATH).toContain("openclaw-matrix-cli-qa-");