mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
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>
94 lines
3.9 KiB
TypeScript
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);
|
|
},
|
|
);
|