Files
reverse-engineering-anything/tests/acceptance/rizinCommandOutcomes.test.ts
T
gensokyo-devandClaude Opus 5.5 6ce708cfde fix(rizin): report shell refusals instead of complete empty results
Rizin exits 0 when its shell refuses a statement, so execute_rizin_command
reported completion_status complete with exit code 0 and an empty stdout
that read as a genuine empty result.

Rizin 0.6 through 0.9.1 log exactly four refusal lines from
librz/core/cmd/cmd.c: an unknown command, a wrong argument count, an
invalid statement (RZ_CMD_STATUS_INVALID, which also stops the remaining
statements) and a parse error. The Rizin command profile now classifies a
completed process's stderr against those lines; the shared one-shot
service reports completion_status "rejected" and a limitation that names
the refusal and says stdout holds only earlier statements' output. Other
ERROR lines come from commands that ran and stay diagnostics.

Fixes #10364

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 04:42:35 +01:00

94 lines
3.9 KiB
TypeScript

import { createHash } from "node:crypto";
import { chmod, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { REVERSE_ENGINEERING_TOOL_CONTRACTS } from "../../src/contracts/reverseEngineeringToolContracts.js";
import { rizinCommandLimitations } from "../../src/rizin/RizinCommand.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { cliTest } from "../support/cli/cliFixture.js";
const outcomes = [
{
command: "axffj",
stdout: "",
stderr: "ERROR: Command 'axffj' does not exist.\n",
// The refusal line on stderr is classified; process facts stay as observed.
completion: "rejected",
},
{ command: "q", stdout: "", stderr: "", completion: "complete" },
{
command: "echo diagnostic",
stdout: "ERROR: Command 'axffj' does not exist.\n",
stderr: "WARNING: fixture warning\n",
// Refusal text on stdout is command output, not a Rizin diagnostic.
completion: "complete",
},
] as const;
cliTest.skipIf(process.platform === "win32").for(outcomes)(
"preserves process facts and exposes command-validity uncertainty: $command",
async (outcome, { cli }) => {
const root = await createTestTempDirectory("rea-rizin-command-outcomes-");
const provider = join(root, "rizin-fixture");
const artifact = join(root, "artifact.bin");
const content = "caller-selected inert artifact";
await writeFile(artifact, content);
// Real Node subprocesses simulate producer output; they are not Rizin.
await writeFile(
provider,
`#!${process.execPath}\n` +
`const cases=${JSON.stringify(outcomes)};\n` +
"const selected=cases.find(item=>item.command===process.argv[process.argv.indexOf('-c')+1]);\n" +
"if(!selected)process.exit(99);\n" +
"process.stdout.write(selected.stdout);process.stderr.write(selected.stderr);\n",
);
await chmod(provider, 0o755);
const environment = { REA_RIZIN_COMMAND: provider };
const response = await cli.run({
arguments: ["execute-rizin-command", artifact, outcome.command, "--json"],
environment,
});
expect(response.exitCode, `${response.stdout}\n${response.stderr}`).toBe(0);
const { client, call } = await connectLocalToolsMcp(environment);
const definition = (await client.listTools()).tools.find(
({ name }) => name === "execute_rizin_command",
);
const mcp = await call("execute_rizin_command", {
path: artifact,
command: outcome.command,
});
expect(mcp.isError, JSON.stringify(mcp.content)).not.toBe(true);
const contract = REVERSE_ENGINEERING_TOOL_CONTRACTS.find(
({ name }) => name === "execute_rizin_command",
);
if (!contract) throw new Error("Missing execute_rizin_command contract");
const limitations = rizinCommandLimitations({
path: artifact,
command: outcome.command,
});
// Even unqualified commands must disclose the provider validity gap.
expect(limitations).not.toHaveLength(0);
for (const raw of [response.json, mcp.structuredContent]) {
const evidence = contract.outputSchema.parse(raw);
expect(evidence.normalized_result).toMatchObject({
stdout: outcome.stdout,
stderr: outcome.stderr,
exit_code: 0,
completion_status: outcome.completion,
output_truncated: false,
artifact_changed: false,
});
expect(evidence.raw_result).toEqual(evidence.normalized_result);
expect(evidence.subject?.digest.sha256).toBe(
createHash("sha256").update(content).digest("hex"),
);
expect(evidence.parameters.command).toBe(outcome.command);
expect(evidence.limitations).toEqual(
expect.arrayContaining([...limitations]),
);
}
expect(definition?.description).toBe(contract.description);
},
);