mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
fix(cli): show refusals for blocked setup dry runs
Render a terminal dry-run plan only after setup admission succeeds. Blocked skill or registration plans use the existing remediation output. Reuse one bounded PTY helper for success and refusal workflows; verify no linked target or client configuration changes. Follow-up-to: #3108 Validation: public CLI PTY tests and RPC owner controls pass (77 tests); build and static checks pass.
This commit is contained in:
1 parent
ccd748635c
commit
cd88163e42
2 files changed
+96
-43
No files matched your search
@@ -226,8 +226,14 @@ const runSetupCommand = async (
|
||||
: undefined,
|
||||
);
|
||||
if (interactive) renderInteractiveSetupResult(result, environment);
|
||||
else if (options.dryRun && !input.formatExplicit && hasInteractiveTerminal())
|
||||
renderSetupDryRun(result.plannedActions);
|
||||
else if (
|
||||
options.dryRun &&
|
||||
!input.formatExplicit &&
|
||||
hasInteractiveTerminal()
|
||||
) {
|
||||
if (result.status === "planned") renderSetupDryRun(result.plannedActions);
|
||||
else renderInteractiveSetupResult(result, environment);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { access, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
access,
|
||||
lstat,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { spawn } from "@lydell/node-pty";
|
||||
@@ -74,48 +83,53 @@ const clientStates = [
|
||||
|
||||
const agentActions = actions.filter(({ kind }) => kind !== "install_hopper");
|
||||
|
||||
const runSetupDryRunInTty = async (
|
||||
home: string,
|
||||
arguments_: readonly string[],
|
||||
): Promise<{ readonly exitCode: number; readonly output: string }> => {
|
||||
const terminal = spawn(
|
||||
process.execPath,
|
||||
["scripts/rea.mjs", "setup", "--dry-run", ...arguments_],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
REA_LOG_LEVEL: "silent",
|
||||
NO_COLOR: "1",
|
||||
},
|
||||
name: "xterm-256color",
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
},
|
||||
);
|
||||
let output = "";
|
||||
terminal.onData((data) => {
|
||||
output += data;
|
||||
});
|
||||
const exitCode = await new Promise<number>((resolvePromise, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
terminal.kill();
|
||||
reject(new Error(`TTY dry run timed out after output:\n${output}`));
|
||||
}, 15_000);
|
||||
terminal.onExit(({ exitCode: code }) => {
|
||||
clearTimeout(timeout);
|
||||
resolvePromise(code);
|
||||
});
|
||||
});
|
||||
return { exitCode, output };
|
||||
};
|
||||
|
||||
describe("TTY setup dry run", () => {
|
||||
it("prints the plan and no-mutation notice for a TTY dry run", async () => {
|
||||
const home = await createTestTempDirectory("rea-cli-setup-dry-run-");
|
||||
const terminal = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/rea.mjs",
|
||||
"setup",
|
||||
"--dry-run",
|
||||
"--client",
|
||||
"codex",
|
||||
"--skill=false",
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
REA_LOG_LEVEL: "silent",
|
||||
NO_COLOR: "1",
|
||||
},
|
||||
name: "xterm-256color",
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
},
|
||||
);
|
||||
let output = "";
|
||||
terminal.onData((data) => {
|
||||
output += data;
|
||||
});
|
||||
const exitCode = await new Promise<number>((resolvePromise, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
terminal.kill();
|
||||
reject(new Error(`TTY dry run timed out after output:\n${output}`));
|
||||
}, 15_000);
|
||||
terminal.onExit(({ exitCode: code }) => {
|
||||
clearTimeout(timeout);
|
||||
resolvePromise(code);
|
||||
});
|
||||
});
|
||||
const { exitCode, output } = await runSetupDryRunInTty(home, [
|
||||
"--client",
|
||||
"codex",
|
||||
"--skill=false",
|
||||
]);
|
||||
|
||||
expect(exitCode, output).toBe(0);
|
||||
expect(output).toContain("CREATE Codex");
|
||||
@@ -127,6 +141,39 @@ describe("TTY setup dry run", () => {
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the refusal instead of a blocked skill plan for a TTY dry run", async () => {
|
||||
const home = await createTestTempDirectory("rea-cli-setup-linked-dry-run-");
|
||||
const managed = join(home, "managed-skill");
|
||||
const linked = join(home, ".claude/skills/reverse-engineer-anything");
|
||||
const original = "user-managed skill bytes\n";
|
||||
await mkdir(managed);
|
||||
await writeFile(join(managed, "SKILL.md"), original);
|
||||
await mkdir(dirname(linked), { recursive: true });
|
||||
await symlink(
|
||||
managed,
|
||||
linked,
|
||||
process.platform === "win32" ? "junction" : "dir",
|
||||
);
|
||||
|
||||
const { exitCode, output } = await runSetupDryRunInTty(home, [
|
||||
"--client",
|
||||
"claude_code",
|
||||
]);
|
||||
|
||||
expect(exitCode, output).toBe(1);
|
||||
expect(output).toContain("Skill directory is a symbolic link.");
|
||||
expect(output).toContain(linked);
|
||||
expect(output).toContain("--skill=false");
|
||||
expect(output).not.toContain("REA setup plan");
|
||||
expect(output).not.toContain("Install or update the REA skill");
|
||||
expect((await lstat(linked)).isSymbolicLink()).toBe(true);
|
||||
expect(await readFile(join(managed, "SKILL.md"), "utf8")).toBe(original);
|
||||
expect(await readdir(managed)).toEqual(["SKILL.md"]);
|
||||
await expect(access(join(home, ".claude.json"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("interactive setup journey", () => {
|
||||
|
||||
Reference in new issue
Block a user