fix(qa-matrix): feed recovery keys through stdin

This commit is contained in:
Gustavo Madeira Santana
2026-04-25 17:30:05 -04:00
parent 5c3f8a8068
commit fd5fc06007
5 changed files with 127 additions and 38 deletions

View File

@@ -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 });
}
});
});

View File

@@ -17,6 +17,7 @@ export type MatrixQaCliRunResult = {
export type MatrixQaCliSession = {
args: string[];
endStdin: () => void;
output: () => { stderr: string; stdout: string };
wait: () => Promise<MatrixQaCliRunResult>;
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<MatrixQaCliRunResult>((resolve, reject) => {
@@ -230,6 +240,7 @@ export async function runMatrixQaOpenClawCli(params: {
args: string[];
cwd?: string;
env: NodeJS.ProcessEnv;
stdin?: string;
timeoutMs: number;
}): Promise<MatrixQaCliRunResult> {
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<MatrixQaCliRunResult> =>
await runMatrixQaOpenClawCli({
allowNonZero: opts.allowNonZero,
args,
env,
stdin: opts.stdin,
timeoutMs: opts.timeoutMs,
}),
start: (args: string[], opts: { allowNonZero?: boolean; timeoutMs: number }) =>

View File

@@ -303,10 +303,12 @@ async function runMatrixQaCliJson<T>(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");

View File

@@ -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",

View File

@@ -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<string, unknown> | 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-");