Files
ruvnet__ruflo/v3/@claude-flow/cli/__tests__/hive-mind-skip-permissions.test.ts
Johnson K C 50bc51643b fix(cli): #2269 honor --dangerously-skip-permissions in hive-mind spawn (#2301)
* fix(cli): #2269 honor --dangerously-skip-permissions in hive-mind spawn

The arg parser (parser.ts normalizeKey) converts kebab-case to camelCase
and stores ONLY the normalized key, so `flags['dangerously-skip-permissions']`
is always undefined and `=== true` always evaluates false. The flag was
silently dropped: headless `hive-mind spawn --claude --non-interactive
--dangerously-skip-permissions` ran in permissionMode "default", so every
Edit/Write was auto-denied (no TTY to approve) and the agent finished
having changed nothing.

Fix mirrors the dual-key fallback used a few lines above for
`isNonInteractive` (`flags['non-interactive'] || flags.nonInteractive`).
This also honors the documented `default: true` for the option declaration
(line 590), since the parser populates the camelCase key with the default.

Repro from the issue:
  npx @claude-flow/cli hive-mind spawn --claude --non-interactive \
    --dangerously-skip-permissions -o "edit any file in this repo"

Before: init event shows `"permissionMode":"default"`, Edit calls land
in permission_denials, files unchanged.
After:  `--dangerously-skip-permissions` is appended to claudeArgs,
spawned `claude` runs in `bypassPermissions`, edits succeed.

Fixes #2269

Co-Authored-By: RuFlo <ruv@ruv.net>

* test(cli): #2269 regression test for hive-mind --dangerously-skip-permissions

Pins down two invariants together:
1. The parser stores ONLY the camelCase form of --dangerously-skip-permissions
   (this is the parser behavior that caused the original drop).
2. The skipPermissions predicate from hive-mind.ts evaluates true when the
   parser-produced flags shape is passed through it.

Also exercises the --no-auto-permissions opt-out and the legacy
hand-constructed-flag-map paths, so a future refactor of either the
parser's normalizeKey or the hive-mind action can't silently regress
the flag-drop bug.

The predicate is kept inline rather than imported, because the hive-mind
spawn action does real side effects (childSpawn) at module load and isn't
trivially importable for a pure unit test. The predicate's source of
truth (hive-mind.ts) and the test mirror are both annotated with #2269.

Co-Authored-By: RuFlo <ruv@ruv.net>

* fix(hive-mind): handle yargs-style negation for --no-auto-permissions

The original PR (#2301) correctly diagnosed the kebab→camel parser
normalization that made the activation half of the
--dangerously-skip-permissions flag silently no-op. The deny half had
the same shape of bug but the patch only widened it to kebab|camel —
neither of which is what the parser actually produces for
--no-auto-permissions.

The parser stores yargs-style negation:

  --no-auto-permissions → { autoPermissions: false }

NOT { 'no-auto-permissions': true } or { noAutoPermissions: true }.

So after the original fix, --dangerously-skip-permissions
--no-auto-permissions would have skipped permissions anyway — strictly
more permissive than the pre-fix state, where activation never fired at
all. Caught by running hive-mind-skip-permissions.test.ts case "skip
permissions=false when --no-auto-permissions is also passed", which
went red.

This commit adds the third deny clause (`flags.autoPermissions === false`)
and pins the parser contract with three new tests:
- parser produces autoPermissions:false for --no-auto-permissions
- predicate denies when only the yargs-style negation key is set
- predicate ignores autoPermissions:true (not a deny signal)

Tests: 9/9 pass. Closes #2269.

Co-Authored-By: JOhnsonKC201 <johnsonkc201@users.noreply.github.com>
Co-Authored-By: rvrheenen <rvrheenen@users.noreply.github.com>
Co-Authored-By: RuFlo <ruv@ruv.net>

---------

Co-authored-by: RuFlo <ruv@ruv.net>
Co-authored-by: JOhnsonKC201 <johnsonkc201@users.noreply.github.com>
Co-authored-by: rvrheenen <rvrheenen@users.noreply.github.com>
2026-06-12 16:58:21 -04:00

113 lines
5.1 KiB
TypeScript

/**
* Regression test for #2269.
*
* The arg parser's normalizeKey converts kebab-case CLI flags to camelCase and
* stores ONLY the normalized key. Code in hive-mind.ts that previously read
* `flags['dangerously-skip-permissions']` was therefore always undefined, and
* `=== true` always evaluated false, so `--dangerously-skip-permissions` was
* silently dropped from the spawned `claude` process — every Edit/Write call
* landed in `permission_denials` and headless runs couldn't change any files.
*
* The fix mirrors the dual-key fallback used elsewhere in hive-mind.ts (e.g.
* isNonInteractive): accept either the kebab key or the camelCase key.
*
* This test pins down both the parser's normalization behavior AND the
* predicate from the fix, so a future refactor of either piece can't
* silently regress the flag-drop bug.
*/
import { describe, it, expect } from 'vitest';
import { CommandParser } from '../src/parser.js';
// The exact predicate from v3/@claude-flow/cli/src/commands/hive-mind.ts
// (kept inline rather than importing the whole spawn action, which spawns a
// real child process at module-load time).
function shouldSkipPermissions(flags: Record<string, unknown>): boolean {
return (
(flags['dangerously-skip-permissions'] === true || flags.dangerouslySkipPermissions === true) &&
!(flags['no-auto-permissions'] || flags.noAutoPermissions || flags.autoPermissions === false)
);
}
describe('#2269 hive-mind --dangerously-skip-permissions flag handling', () => {
it('parser normalizes the kebab flag to camelCase and leaves the kebab key undefined', () => {
const parser = new CommandParser({ allowUnknownFlags: true });
const { flags } = parser.parse(['--dangerously-skip-permissions']);
// This is the heart of the bug: reading the kebab key alone is always
// undefined because the parser stores the normalized key.
expect(flags['dangerously-skip-permissions']).toBeUndefined();
expect(flags.dangerouslySkipPermissions).toBe(true);
});
it('skipPermissions=true when the CLI passes --dangerously-skip-permissions', () => {
const parser = new CommandParser({ allowUnknownFlags: true });
const { flags } = parser.parse(['--dangerously-skip-permissions']);
expect(shouldSkipPermissions(flags as Record<string, unknown>)).toBe(true);
});
it('skipPermissions=false when --no-auto-permissions is also passed', () => {
const parser = new CommandParser({ allowUnknownFlags: true });
const { flags } = parser.parse([
'--dangerously-skip-permissions',
'--no-auto-permissions',
]);
expect(shouldSkipPermissions(flags as Record<string, unknown>)).toBe(false);
});
it('skipPermissions=false when neither key is set', () => {
expect(shouldSkipPermissions({})).toBe(false);
});
it('predicate accepts the camelCase key directly (parser-independent contract)', () => {
// Belt-and-braces: even if a future caller hand-constructs the flags
// object with only the camelCase key, the predicate still works.
expect(shouldSkipPermissions({ dangerouslySkipPermissions: true })).toBe(true);
expect(shouldSkipPermissions({
dangerouslySkipPermissions: true,
noAutoPermissions: true,
})).toBe(false);
});
it('predicate still accepts the legacy kebab key (back-compat with hand-built flag maps)', () => {
// Some callers (tests, programmatic invocations) may build the flags
// object manually with the kebab key. The fix is intentionally a dual
// read, so this path keeps working.
expect(shouldSkipPermissions({ 'dangerously-skip-permissions': true })).toBe(true);
});
it('parser produces the yargs-style negation autoPermissions:false for --no-auto-permissions', () => {
// This pins the parser contract that drives the deny-clause third term.
// If the parser ever changes its negation idiom (e.g. starts storing
// noAutoPermissions:true instead), this test fails BEFORE the predicate
// silently goes back to ignoring --no-auto-permissions in production.
const parser = new CommandParser({ allowUnknownFlags: true });
const { flags } = parser.parse(['--no-auto-permissions']);
expect(flags.autoPermissions).toBe(false);
expect(flags['no-auto-permissions']).toBeUndefined();
expect(flags.noAutoPermissions).toBeUndefined();
});
it('predicate denies when only the yargs-style negation key is set', () => {
// Belt-and-braces for the parser-produced shape, independent of the
// CLI integration test above.
expect(shouldSkipPermissions({
dangerouslySkipPermissions: true,
autoPermissions: false,
})).toBe(false);
});
it('predicate ignores autoPermissions:true (a positive autoPermissions does NOT enable skip on its own)', () => {
// Guard against an over-broad reading of the third term: only
// autoPermissions === false should be treated as the negation signal.
expect(shouldSkipPermissions({ autoPermissions: true })).toBe(false);
// And it still allows skip when the negation is absent.
expect(shouldSkipPermissions({
dangerouslySkipPermissions: true,
autoPermissions: true,
})).toBe(true);
});
});