Files
stack/packages/mosaic/src/commands/install-ordering-guard.spec.ts
jason.woltje d351caad36
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
feat(869-c2): install-ordering enforcement-hook guard (Part of #869)
Part of #869

Mos (id-11) Gate-16 merge: independent APPROVE @b6f36564 (8/8, verified vs real production settings template), author id2 != approver id11, clean mosaic-coder author, CI green wp1988.

Co-authored-by: jason.woltje <jason@diversecanvas.com>
Co-committed-by: jason.woltje <jason@diversecanvas.com>
2026-07-23 18:48:33 +00:00

302 lines
11 KiB
TypeScript

import { describe, it, expect, afterEach } from 'vitest';
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { leaseEnforcementActivatable } from './lease-activation-probe.js';
import {
ENFORCEMENT_HOOK_MARKERS,
FAIL_LOUD_MESSAGE,
guardClaudeSettingsWiring,
loudOptOutMessage,
runInstallOrderingGuard,
settingsHasEnforcementHooks,
stripEnforcementHooks,
} from './install-ordering-guard.js';
/**
* Red-first tests for issue #869 Point-1 C2 — the install-ordering guard.
*
* Root cause under test: `mosaic-link-runtime-assets` copies
* `runtime/claude/settings.json` (which embeds the PreToolUse
* `mutator-gate.py` hook and the Stop `receipt-observer-client.py` hook)
* straight into `~/.claude/settings.json`, unconditionally. If the
* activation half (C1: `leaseEnforcementActivatable()`) cannot be confirmed,
* wiring those hooks bricks the host with a fail-closed gate that can never
* be satisfied. This guard must refuse to wire in that case by default, and
* only wire anyway on an explicit, loud opt-out.
*
* All fixtures use temp directories — this suite never reads or writes the
* real `~/.claude/settings.json`.
*/
const FIXTURE_SETTINGS = {
model: 'opus',
hooks: {
PreCompact: [
{
matcher: '.*',
hooks: [{ type: 'command', command: 'python3 revoke-lease.py --reason pre-compact' }],
},
],
PreToolUse: [
{
matcher: '.*',
hooks: [
{
type: 'command',
command: 'python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude',
timeout: 3,
},
],
},
{
matcher: 'Write|Edit|MultiEdit',
hooks: [{ type: 'command', command: '~/.config/mosaic/tools/qa/prevent-memory-write.sh' }],
},
],
PostToolUse: [
{
matcher: 'Edit|MultiEdit|Write',
hooks: [{ type: 'command', command: '~/.config/mosaic/tools/qa/qa-hook-stdin.sh' }],
},
],
Stop: [
{
hooks: [
{
type: 'command',
command:
'python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude',
timeout: 3,
},
{ type: 'command', command: '~/.config/mosaic/tools/qa/reflect-stop-hook.sh' },
],
},
],
},
enabledPlugins: { 'feature-dev@claude-plugins-official': true },
};
function fixtureJson(): string {
return JSON.stringify(FIXTURE_SETTINGS, null, 2) + '\n';
}
describe('stripEnforcementHooks', () => {
it('removes the PreToolUse mutator-gate trigger entirely', () => {
const { settings } = stripEnforcementHooks(FIXTURE_SETTINGS);
const hooks = settings['hooks'] as Record<string, unknown[]>;
const preToolUse = hooks['PreToolUse'] as Array<{ hooks: Array<{ command: string }> }>;
expect(preToolUse.some((t) => t.hooks.some((h) => h.command.includes('mutator-gate.py')))).toBe(
false,
);
});
it('preserves the sibling prevent-memory-write.sh PreToolUse trigger', () => {
const { settings } = stripEnforcementHooks(FIXTURE_SETTINGS);
const hooks = settings['hooks'] as Record<string, unknown[]>;
const preToolUse = hooks['PreToolUse'] as Array<{ hooks: Array<{ command: string }> }>;
expect(
preToolUse.some((t) => t.hooks.some((h) => h.command.includes('prevent-memory-write.sh'))),
).toBe(true);
});
it('removes only the receipt-observer-client.py hook from Stop, keeping reflect-stop-hook.sh', () => {
const { settings } = stripEnforcementHooks(FIXTURE_SETTINGS);
const hooks = settings['hooks'] as Record<string, unknown[]>;
const stop = hooks['Stop'] as Array<{ hooks: Array<{ command: string }> }>;
const commands = stop.flatMap((t) => t.hooks.map((h) => h.command));
expect(commands.some((c) => c.includes('receipt-observer-client.py'))).toBe(false);
expect(commands.some((c) => c.includes('reflect-stop-hook.sh'))).toBe(true);
});
it('leaves PreCompact/PostToolUse hooks byte-identical', () => {
const { settings } = stripEnforcementHooks(FIXTURE_SETTINGS);
const hooks = settings['hooks'] as Record<string, unknown>;
expect(hooks['PreCompact']).toEqual(FIXTURE_SETTINGS.hooks.PreCompact);
expect(hooks['PostToolUse']).toEqual(FIXTURE_SETTINGS.hooks.PostToolUse);
});
it('reports what it removed', () => {
const { removed } = stripEnforcementHooks(FIXTURE_SETTINGS);
expect(removed).toContain(`PreToolUse:${ENFORCEMENT_HOOK_MARKERS.preToolUse}`);
expect(removed).toContain(`Stop:${ENFORCEMENT_HOOK_MARKERS.stop}`);
});
});
describe('settingsHasEnforcementHooks', () => {
it('is true for the unmodified fixture', () => {
expect(settingsHasEnforcementHooks(FIXTURE_SETTINGS)).toBe(true);
});
it('is false after stripping', () => {
const { settings } = stripEnforcementHooks(FIXTURE_SETTINGS);
expect(settingsHasEnforcementHooks(settings)).toBe(false);
});
it('is false for settings with no hooks key at all', () => {
expect(settingsHasEnforcementHooks({ model: 'opus' })).toBe(false);
});
});
describe('guardClaudeSettingsWiring', () => {
it('probe=false (default, no opt-out): strips enforcement hooks and reports non-zero with a loud, actionable message', () => {
const outcome = guardClaudeSettingsWiring(fixtureJson(), {}, { activatable: () => false });
expect(outcome.exitCode).toBe(1);
expect(outcome.wired).toBe(false);
expect(settingsHasEnforcementHooks(JSON.parse(outcome.json) as Record<string, unknown>)).toBe(
false,
);
expect(outcome.logs).toHaveLength(1);
expect(outcome.logs[0]?.level).toBe('error');
expect(outcome.logs[0]?.message).toBe(FAIL_LOUD_MESSAGE);
expect(outcome.logs[0]?.message).toMatch(/refusing to wire a dead gate/i);
expect(outcome.logs[0]?.message).toMatch(/#869/);
expect(outcome.logs[0]?.message).toMatch(/--allow-inactive-enforcement/);
});
it('probe=false + explicit opt-out flag: wires hooks as-is and emits a loud warning', () => {
const outcome = guardClaudeSettingsWiring(
fixtureJson(),
{ allowInactiveEnforcement: true },
{ activatable: () => false },
);
expect(outcome.exitCode).toBe(0);
expect(outcome.wired).toBe(true);
expect(settingsHasEnforcementHooks(JSON.parse(outcome.json) as Record<string, unknown>)).toBe(
true,
);
expect(outcome.logs).toHaveLength(1);
expect(outcome.logs[0]?.level).toBe('warn');
expect(outcome.logs[0]?.message).toBe(loudOptOutMessage());
expect(outcome.logs[0]?.message).toMatch(/WITHOUT confirmed activation/);
});
it('probe=true: wires hooks normally with no logs, regardless of opt-out', () => {
const outcome = guardClaudeSettingsWiring(fixtureJson(), {}, { activatable: () => true });
expect(outcome.exitCode).toBe(0);
expect(outcome.wired).toBe(true);
expect(outcome.logs).toHaveLength(0);
expect(JSON.parse(outcome.json)).toEqual(FIXTURE_SETTINGS);
});
it('probe=true + opt-out flag set anyway: still wires normally, no spurious warning', () => {
const outcome = guardClaudeSettingsWiring(
fixtureJson(),
{ allowInactiveEnforcement: true },
{ activatable: () => true },
);
expect(outcome.exitCode).toBe(0);
expect(outcome.wired).toBe(true);
expect(outcome.logs).toHaveLength(0);
});
it('defaults to the real leaseEnforcementActivatable() when no activatable dep is injected', () => {
// Deliberately does not assume a fixed true/false value for the real
// probe (whether dist/cli.js happens to be built varies by environment —
// asserting a hardcoded expectation here would make the test flaky, not
// red-first). Instead it proves the wiring is genuinely delegated: the
// no-deps call must agree with an explicit call to the same real
// predicate, not some other hardcoded value.
const reallyActivatable = leaseEnforcementActivatable();
const outcome = guardClaudeSettingsWiring(fixtureJson());
if (reallyActivatable) {
expect(outcome.exitCode).toBe(0);
expect(outcome.wired).toBe(true);
} else {
expect(outcome.exitCode).toBe(1);
expect(outcome.wired).toBe(false);
}
});
});
describe('runInstallOrderingGuard (file-level, temp dirs only)', () => {
let dir: string;
afterEach(() => {
if (dir) rmSync(dir, { recursive: true, force: true });
});
function makeSrc(): string {
dir = mkdtempSync(join(tmpdir(), 'mosaic-install-ordering-guard-'));
const src = join(dir, 'settings.json');
writeFileSync(src, fixtureJson());
return src;
}
it('probe=false: writes a dest settings.json with hooks stripped and returns exitCode 1', () => {
const src = makeSrc();
const dest = join(dir, 'claude-settings.json');
const result = runInstallOrderingGuard(src, dest, {}, { activatable: () => false });
expect(result.exitCode).toBe(1);
expect(result.destWritten).toBe(true);
expect(existsSync(dest)).toBe(true);
const written = JSON.parse(readFileSync(dest, 'utf-8')) as Record<string, unknown>;
expect(settingsHasEnforcementHooks(written)).toBe(false);
});
it('probe=false + opt-out: writes dest with hooks intact and returns exitCode 0', () => {
const src = makeSrc();
const dest = join(dir, 'claude-settings.json');
const result = runInstallOrderingGuard(
src,
dest,
{ allowInactiveEnforcement: true },
{ activatable: () => false },
);
expect(result.exitCode).toBe(0);
const written = JSON.parse(readFileSync(dest, 'utf-8')) as Record<string, unknown>;
expect(settingsHasEnforcementHooks(written)).toBe(true);
});
it('probe=true: writes dest with hooks intact and returns exitCode 0', () => {
const src = makeSrc();
const dest = join(dir, 'claude-settings.json');
const result = runInstallOrderingGuard(src, dest, {}, { activatable: () => true });
expect(result.exitCode).toBe(0);
const written = JSON.parse(readFileSync(dest, 'utf-8')) as Record<string, unknown>;
expect(settingsHasEnforcementHooks(written)).toBe(true);
});
it('backs up a pre-existing divergent dest before overwriting (copy_file_managed parity)', () => {
const src = makeSrc();
const dest = join(dir, 'claude-settings.json');
writeFileSync(dest, JSON.stringify({ preexisting: true }));
const result = runInstallOrderingGuard(src, dest, {}, { activatable: () => true });
expect(result.destWritten).toBe(true);
expect(result.backupPath).toBeDefined();
expect(existsSync(result.backupPath!)).toBe(true);
expect(JSON.parse(readFileSync(result.backupPath!, 'utf-8'))).toEqual({ preexisting: true });
});
it('is a no-op write when dest already matches the guarded content (idempotent)', () => {
const src = makeSrc();
const dest = join(dir, 'claude-settings.json');
const first = runInstallOrderingGuard(src, dest, {}, { activatable: () => true });
expect(first.destWritten).toBe(true);
const second = runInstallOrderingGuard(src, dest, {}, { activatable: () => true });
expect(second.destWritten).toBe(false);
expect(second.backupPath).toBeUndefined();
});
it('never touches the real home directory settings path used by this test file', () => {
// Sanity guard for the suite itself: every dest path used above lives
// under the mkdtemp() scratch dir, never under homedir()/.claude.
expect(dir).toContain('mosaic-install-ordering-guard-');
});
});