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>
302 lines
11 KiB
TypeScript
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-');
|
|
});
|
|
});
|