import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Command } from 'commander'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { applyFleetLaunchComposition, deepMergeSettings, FleetLaunchError, formatFleetLaunchDryRun, parseFleetAgentProfile, registerFleetLaunchCommand, resolveFleetLaunchComposition, } from './fleet-launch-command.js'; const roots: string[] = []; afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); function fixture(profile: Record = { schema: 1, harness: 'claude' }): { root: string; systemHome: string; userHome: string; agentDir: string; namedBundleDir: string; credentialName: string; } { const harness = String(profile.harness ?? 'claude'); const credentialName = harness === 'claude' ? '.credentials.json' : 'auth.json'; const root = mkdtempSync(join(tmpdir(), 'mosaic-fleet-launch-')); roots.push(root); const systemHome = join(root, 'system'); const userHome = join(root, 'user'); const agentDir = join(userHome, 'fleet', 'agents', 'fred'); const namedBundleDir = join(userHome, 'auth', harness, 'fred_example.com'); mkdirSync(join(systemHome, 'runtime', harness), { recursive: true }); mkdirSync(agentDir, { recursive: true }); mkdirSync(namedBundleDir, { recursive: true }); writeFileSync(join(systemHome, 'runtime', harness, 'settings.json'), '{}\n'); writeFileSync(join(agentDir, 'profile.json'), `${JSON.stringify(profile, null, 2)}\n`); writeFileSync(join(namedBundleDir, credentialName), '{}\n', { mode: 0o600 }); writeFileSync( join(namedBundleDir, 'account.json'), '{"oauthAccount":{"emailAddress":"fred@example.com"}}\n', ); symlinkSync('fred_example.com', join(userHome, 'auth', harness, 'primary'), 'dir'); return { root, systemHome, userHome, agentDir, namedBundleDir, credentialName }; } describe('fleet launch profile schema 1', () => { it('rejects an unknown key and names it', () => { expect(() => parseFleetAgentProfile('{"schema":1,"harness":"claude","pluigns":[]}'), ).toThrowError(/unknown profile key "pluigns"/); }); it('uses a dedicated SCHEMA_TOO_NEW error with an upgrade hint', () => { try { parseFleetAgentProfile('{"schema":2,"harness":"claude"}'); throw new Error('expected parse to fail'); } catch (error) { expect(error).toBeInstanceOf(FleetLaunchError); expect((error as FleetLaunchError).code).toBe('SCHEMA_TOO_NEW'); expect((error as Error).message).toMatch(/upgrade Mosaic/i); } }); }); describe('three-layer settings merge', () => { it('keeps base-only settings', () => { expect(deepMergeSettings({ base: { enabled: true } })).toEqual({ base: { enabled: true } }); }); it('uses the last layer for scalar conflicts', () => { expect(deepMergeSettings({ model: 'base' }, { model: 'user' })).toEqual({ model: 'user' }); }); it('replaces arrays instead of appending', () => { expect(deepMergeSettings({ hooks: ['base'] }, { hooks: ['user'] })).toEqual({ hooks: ['user'], }); }); it('uses null as a key-deleting tombstone', () => { expect( deepMergeSettings({ nested: { keep: true, remove: true } }, { nested: { remove: null } }), ).toEqual({ nested: { keep: true } }); }); it('replaces a hook event array wholesale with the higher layer', () => { const qaStop = { hooks: [{ type: 'command', command: 'qa-stop.sh' }] }; const leaseStop = { hooks: [{ type: 'command', command: 'receipt-observer.py' }] }; const qaPre = { matcher: 'Write', hooks: [{ type: 'command', command: 'qa-pre.sh' }] }; expect( deepMergeSettings( { hooks: { Stop: [qaStop], PreToolUse: [qaPre] } }, { hooks: { Stop: [leaseStop] } }, ), ).toEqual({ hooks: { Stop: [leaseStop], PreToolUse: [qaPre] } }); }); it('reconstructs every gated hook event from the base and lease overlay', () => { const fx = fixture({ schema: 1, harness: 'claude', overlay: 'lease-overlay.json' }); const frameworkRuntime = join(process.cwd(), 'framework', 'runtime', 'claude'); writeFileSync( join(fx.systemHome, 'runtime', 'claude', 'settings.json'), readFileSync(join(frameworkRuntime, 'settings.json'), 'utf8'), ); writeFileSync( join(fx.agentDir, 'lease-overlay.json'), readFileSync(join(frameworkRuntime, 'lease-overlay.json'), 'utf8'), ); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(plan.settings.merged['hooks']).toEqual({ PreToolUse: [ { matcher: 'Write|Edit|MultiEdit', hooks: [ { type: 'command', command: '~/.config/mosaic/tools/qa/prevent-memory-write.sh', timeout: 10, }, ], }, { matcher: '.*', hooks: [ { type: 'command', command: 'python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude --recovery-command ~/.config/mosaic/tools/lease-broker/recover-context.py', timeout: 3, }, ], }, ], PostToolUse: [ { matcher: 'Edit|MultiEdit|Write', hooks: [ { type: 'command', command: '~/.config/mosaic/tools/qa/qa-hook-stdin.sh', timeout: 60, }, ], }, { matcher: 'Edit|MultiEdit|Write', hooks: [ { type: 'command', command: '~/.config/mosaic/tools/qa/typecheck-hook.sh', timeout: 30, }, ], }, ], Stop: [ { hooks: [ { type: 'command', command: '~/.config/mosaic/tools/qa/reflect-stop-hook.sh', timeout: 15, }, ], }, { hooks: [ { type: 'command', command: 'python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude --latest-entry; observer_status=$?; python3 ~/.config/mosaic/tools/lease-broker/promote-complete.py; exit $observer_status', timeout: 15, }, ], }, ], PreCompact: [ { matcher: '.*', hooks: [ { type: 'command', command: 'python3 "$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py" --runtime claude --reason pre-compact', }, ], }, ], SessionStart: [ { matcher: 'compact', hooks: [ { type: 'command', command: 'python3 "$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py" --runtime claude --reason session-start-compact', }, ], }, { matcher: 'resume|clear', hooks: [ { type: 'command', command: 'python3 "$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py" --runtime claude --reason session-start-rollover --bump-generation', }, ], }, ], UserPromptSubmit: [ { matcher: '^/mosaic-promote$', hooks: [ { type: 'command', command: 'python3 ~/.config/mosaic/tools/lease-broker/promote-begin.py', timeout: 15, }, ], }, ], }); }); it('still deletes a whole hook event via the null tombstone', () => { expect( deepMergeSettings( { hooks: { Stop: [{ hooks: [{ type: 'command', command: 'qa-stop.sh' }] }] } }, { hooks: { Stop: null } }, ), ).toEqual({ hooks: {} }); }); it('replaces an allowedCommands-shaped non-hook array wholesale', () => { expect( deepMergeSettings( { allowedCommands: ['pnpm', 'git'], nested: { hooks: { Stop: ['base'] } } }, { allowedCommands: ['node'], nested: { hooks: { Stop: ['user'] } } }, ), ).toEqual({ allowedCommands: ['node'], nested: { hooks: { Stop: ['user'] } } }); }); it('deep-merges all three layers in precedence order', () => { expect( deepMergeSettings( { nested: { system: true, shared: 'system' }, list: [1] }, { nested: { user: true, shared: 'user' }, list: [2] }, { nested: { agent: true, shared: 'agent' }, list: [3] }, ), ).toEqual({ nested: { system: true, user: true, agent: true, shared: 'agent' }, list: [3], }); }); }); describe('profile-selected overlay', () => { it('defaults to no overlay when the optional profile field is omitted', () => { const fx = fixture(); writeFileSync(join(fx.agentDir, 'overlay.json'), '{"mustNotLoad":true}\n'); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(plan.settings.merged).toEqual({}); expect(plan.settings.layers[2]?.present).toBe(false); }); }); describe('unscaffolded agent names', () => { it('points an unscaffolded name at mosaic fleet agent new', () => { const fx = fixture(); try { resolveFleetLaunchComposition('ghost', { systemHome: fx.systemHome, userHome: fx.userHome, }); throw new Error('expected resolution to fail'); } catch (error: unknown) { const launchError = error as FleetLaunchError; expect(launchError.code).toBe('AGENT_NOT_SCAFFOLDED'); expect(launchError.message).toContain("no such fleet agent 'ghost'"); expect(launchError.message).toContain('mosaic fleet agent new ghost'); } }); }); describe('A3 credential validation', () => { it('refuses a symlinked bundle credential file', () => { const fx = fixture(); rmSync(join(fx.namedBundleDir, '.credentials.json')); const outside = join(fx.root, 'outside-credentials.json'); writeFileSync(outside, '{}\n'); symlinkSync(outside, join(fx.namedBundleDir, '.credentials.json')); expect(() => resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }), ).toThrowError(/real, non-symlink credential file/); }); it('refuses an auth ancestor symlink that relocates the credential trust root', () => { const fx = fixture(); rmSync(join(fx.userHome, 'auth'), { recursive: true, force: true }); const outsideAuth = join(fx.root, 'outside-auth'); const outsideBundle = join(outsideAuth, 'claude', 'fred_example.com'); mkdirSync(outsideBundle, { recursive: true }); writeFileSync(join(outsideBundle, '.credentials.json'), '{}\n', { mode: 0o600 }); writeFileSync( join(outsideBundle, 'account.json'), '{"oauthAccount":{"emailAddress":"fred@example.com"}}\n', ); symlinkSync('fred_example.com', join(outsideAuth, 'claude', 'primary'), 'dir'); symlinkSync(outsideAuth, join(fx.userHome, 'auth'), 'dir'); expect(() => resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }), ).toThrowError(/auth directory must be a real, non-symlink directory/); }); it('refuses a group- or world-readable credential file', () => { const fx = fixture(); chmodSync(join(fx.namedBundleDir, '.credentials.json'), 0o644); expect(() => resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }), ).toThrowError(/credential file must not grant group or other permissions/); }); it('accepts a real private credential file contained in the harness auth root', () => { const fx = fixture(); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(plan.credential.target).toBe(join(fx.namedBundleDir, '.credentials.json')); expect(plan.bundle.display).toBe('primary -> fred_example.com (fred@example.com)'); }); it('refuses first-auth state when a real file occupies the seat link', () => { const fx = fixture(); const seatHome = join(fx.agentDir, '.claude'); mkdirSync(seatHome, { recursive: true }); writeFileSync(join(seatHome, '.credentials.json'), '{"private":true}\n'); expect(() => resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }), ).toThrowError(/first-auth.*refusing to delete or overwrite/i); expect(lstatSync(join(seatHome, '.credentials.json')).isSymbolicLink()).toBe(false); }); it('points Claude at the resolved bundle directory and plans no credential link', () => { const fx = fixture(); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(plan.credential.link).toBeUndefined(); expect(plan.credential.dir).toBe(fx.namedBundleDir); // An empty value resolves to ~/.claude, which is the operator's own account, // so the exported value must always be the absolute bundle path. expect(plan.env['CLAUDE_SECURESTORAGE_CONFIG_DIR']).toBe(fx.namedBundleDir); expect(plan.env['CLAUDE_SECURESTORAGE_CONFIG_DIR']).not.toBe(''); }); it('keeps the managed credential link for a harness with no credential-directory variable', () => { const fx = fixture({ schema: 1, harness: 'pi' }); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(plan.credential.link).toBe(join(fx.agentDir, '.pi', 'auth.json')); expect(plan.credential.target).toBe(join(fx.namedBundleDir, 'auth.json')); expect(Object.keys(plan.env)).not.toContain('CLAUDE_SECURESTORAGE_CONFIG_DIR'); }); }); describe('managed plugin and skill links', () => { it('refuses an unrecorded foreign symlink without mutating it', () => { const fx = fixture({ schema: 1, harness: 'claude', plugins: [] }); const pluginHome = join(fx.agentDir, '.claude', 'plugins'); const foreign = join(fx.root, 'foreign-plugin'); mkdirSync(pluginHome, { recursive: true }); mkdirSync(foreign, { recursive: true }); symlinkSync(foreign, join(pluginHome, 'foreign'), 'dir'); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(() => applyFleetLaunchComposition(plan)).toThrowError( /unrecorded or retargeted symlink/, ); expect(readlinkSync(join(pluginHome, 'foreign'))).toBe(foreign); }); it('performs no writes when a late foreign install link is refused', () => { const fx = fixture({ schema: 1, harness: 'claude', plugins: ['keep'] }); const target = join(fx.userHome, 'plugins', 'keep'); const link = join(fx.agentDir, '.claude', 'plugins', 'keep'); mkdirSync(target, { recursive: true }); mkdirSync(join(link, '..'), { recursive: true }); writeFileSync(join(fx.agentDir, '.claude', '.mosaic-managed-links.json'), '{"links":{}}\n'); symlinkSync(target, link, 'dir'); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); const snapshot = join(fx.agentDir, 'settings.generated.json'); const temp = join(fx.agentDir, '.claude', '.mosaic-managed-links.json.tmp'); expect(() => applyFleetLaunchComposition(plan)).toThrowError( /unrecorded or retargeted symlink/, ); expect(existsSync(snapshot)).toBe(false); expect(existsSync(temp)).toBe(false); expect(readlinkSync(link)).toBe(target); }); it('prunes a recorded matching stale symlink', () => { const fx = fixture({ schema: 1, harness: 'claude', plugins: ['old'] }); mkdirSync(join(fx.userHome, 'plugins', 'old'), { recursive: true }); const initial = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); applyFleetLaunchComposition(initial); writeFileSync( join(fx.agentDir, 'profile.json'), '{"schema":1,"harness":"claude","plugins":[]}\n', ); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); const pluginHome = join(fx.agentDir, '.claude', 'plugins'); expect(plan.prune).toEqual([join(pluginHome, 'old')]); applyFleetLaunchComposition(plan); expect(() => lstatSync(join(pluginHome, 'old'))).toThrow(); }); it('refuses a recorded link retargeted after composition and leaves it intact', () => { const fx = fixture({ schema: 1, harness: 'claude', plugins: ['old'] }); const managedTarget = join(fx.userHome, 'plugins', 'old'); const foreignTarget = join(fx.root, 'foreign-plugin'); mkdirSync(managedTarget, { recursive: true }); mkdirSync(foreignTarget, { recursive: true }); const initial = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); applyFleetLaunchComposition(initial); writeFileSync( join(fx.agentDir, 'profile.json'), '{"schema":1,"harness":"claude","plugins":[]}\n', ); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); const link = join(fx.agentDir, '.claude', 'plugins', 'old'); rmSync(link); symlinkSync(foreignTarget, link, 'dir'); expect(() => applyFleetLaunchComposition(plan)).toThrowError( /unrecorded or retargeted symlink/, ); expect(readlinkSync(link)).toBe(foreignTarget); }); it('refuses a tampered manifest entry outside this seat and leaves it intact', () => { const fx = fixture({ schema: 1, harness: 'claude' }); const seatHome = join(fx.agentDir, '.claude'); mkdirSync(seatHome, { recursive: true }); const manifest = join(seatHome, '.mosaic-managed-links.json'); const crossSeat = join(fx.userHome, 'fleet', 'agents', 'other', '.claude', 'plugins', 'keep'); writeFileSync( manifest, JSON.stringify({ links: { [crossSeat]: join(fx.userHome, 'plugins', 'keep') } }), ); expect(() => resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome }), ).toThrowError(/escapes an approved seat\/store root/); expect(readFileSync(manifest, 'utf8')).toContain(crossSeat); }); it('refuses a symlinked manifest temporary path without modifying its target', () => { const fx = fixture({ schema: 1, harness: 'claude', plugins: ['keep'] }); const target = join(fx.userHome, 'plugins', 'keep'); const sentinel = join(fx.root, 'sentinel.json'); mkdirSync(target, { recursive: true }); mkdirSync(join(fx.agentDir, '.claude'), { recursive: true }); writeFileSync(sentinel, 'unchanged\n', { mode: 0o600 }); symlinkSync(sentinel, join(fx.agentDir, '.claude', '.mosaic-managed-links.json.tmp'), 'file'); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(() => applyFleetLaunchComposition(plan)).toThrowError(/cannot be created exclusively/); expect(readFileSync(sentinel, 'utf8')).toBe('unchanged\n'); }); // Credential links exist only for harnesses that are not pointed at their bundle // by environment, so the containment rules are exercised on one of those. it('refuses an exact-target unrecorded credential symlink', () => { const fx = fixture({ schema: 1, harness: 'pi' }); const seatHome = join(fx.agentDir, '.pi'); const link = join(seatHome, fx.credentialName); mkdirSync(seatHome, { recursive: true }); symlinkSync(join(fx.namedBundleDir, fx.credentialName), link, 'file'); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(() => applyFleetLaunchComposition(plan)).toThrowError( /unrecorded or retargeted symlink/, ); expect(readlinkSync(link)).toBe(join(fx.namedBundleDir, fx.credentialName)); }); it.each(['plugins', 'skills'] as const)( 'refuses an exact-target unrecorded %s symlink', (kind) => { const fx = fixture({ schema: 1, harness: 'claude', [kind]: ['keep'] }); const target = join(fx.userHome, kind, 'keep'); const link = join(fx.agentDir, '.claude', kind, 'keep'); mkdirSync(target, { recursive: true }); mkdirSync(join(link, '..'), { recursive: true }); writeFileSync(join(fx.agentDir, '.claude', '.mosaic-managed-links.json'), '{"links":{}}\n'); symlinkSync(target, link, 'dir'); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(() => applyFleetLaunchComposition(plan)).toThrowError( /unrecorded or retargeted symlink/, ); expect(readlinkSync(link)).toBe(target); }, ); it('refuses an unrecorded mismatched credential symlink', () => { const fx = fixture({ schema: 1, harness: 'pi' }); const seatHome = join(fx.agentDir, '.pi'); const foreignCredential = join(fx.root, 'foreign-credential.json'); mkdirSync(seatHome, { recursive: true }); writeFileSync(foreignCredential, '{}\n', { mode: 0o600 }); symlinkSync(foreignCredential, join(seatHome, fx.credentialName), 'file'); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(() => applyFleetLaunchComposition(plan)).toThrowError( /unrecorded or retargeted symlink/, ); expect(readFileSync(join(seatHome, fx.credentialName), 'utf8')).toBe('{}\n'); }); it('tolerates harness metadata files in the install root and still refuses real directories', () => { const fx = fixture({ schema: 1, harness: 'claude', plugins: [] }); const pluginHome = join(fx.agentDir, '.claude', 'plugins'); mkdirSync(pluginHome, { recursive: true }); writeFileSync(join(pluginHome, 'installed_plugins.json'), '{}\n'); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); expect(plan.prune).toEqual([]); expect(readFileSync(join(pluginHome, 'installed_plugins.json'), 'utf8')).toBe('{}\n'); mkdirSync(join(pluginHome, 'stray-plugin'), { recursive: true }); expect(() => resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }), ).toThrowError(/real plugin directory occupies managed install root.*refusing to prune/i); }); it('surfaces a real directory at a managed link path without deleting it', () => { const fx = fixture({ schema: 1, harness: 'claude', plugins: ['keep'] }); mkdirSync(join(fx.userHome, 'plugins', 'keep'), { recursive: true }); const occupied = join(fx.agentDir, '.claude', 'plugins', 'keep'); mkdirSync(occupied, { recursive: true }); expect(() => resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }), ).toThrowError(/real plugin directory.*refusing to delete/i); expect(lstatSync(occupied).isDirectory()).toBe(true); }); }); describe('fleet launch command outcomes', () => { it('--dry-run prints without writing or invoking the launcher', () => { const fx = fixture(); const program = new Command().exitOverride(); const fleet = program.command('fleet'); const launcher = vi.fn(); const stdout = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); registerFleetLaunchCommand(fleet, () => fx.systemHome, { userHome: fx.userHome, launcher, }); try { program.parse(['node', 'mosaic', 'fleet', 'launch', 'fred', '--dry-run']); expect(stdout).toHaveBeenCalledWith( expect.stringContaining('mosaic fleet launch fred --dry-run'), ); expect(launcher).not.toHaveBeenCalled(); expect(() => lstatSync(join(fx.agentDir, '.claude'))).toThrow(); } finally { stdout.mockRestore(); } }); it('applies the plan and invokes the existing launch seam with declared values', () => { const fx = fixture({ schema: 1, harness: 'claude', model: 'opus', env: { SEAT_FLAG: 'yes' }, }); const program = new Command().exitOverride(); const fleet = program.command('fleet'); const launcher = vi.fn(); registerFleetLaunchCommand(fleet, () => fx.systemHome, { userHome: fx.userHome, launcher, }); program.parse(['node', 'mosaic', 'fleet', 'launch', 'fred']); expect(launcher).toHaveBeenCalledWith( 'claude', ['--model', 'opus'], { CLAUDE_CONFIG_DIR: join(fx.agentDir, '.claude'), CLAUDE_SECURESTORAGE_CONFIG_DIR: fx.namedBundleDir, MOSAIC_AGENT_NAME: 'fred', SEAT_FLAG: 'yes', }, { agentDir: fx.agentDir, mosaicHome: fx.systemHome }, ); // The bundle is reached by environment, so nothing is planted at the seat path. expect(existsSync(join(fx.agentDir, '.claude', '.credentials.json'))).toBe(false); }); it('sets a non-zero exit code and never invokes the launcher', () => { const fx = fixture({ schema: 1, harness: 'claude', unknown: true }); const program = new Command().exitOverride(); const fleet = program.command('fleet'); const launcher = vi.fn(); const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); const priorExitCode = process.exitCode; process.exitCode = 0; registerFleetLaunchCommand(fleet, () => fx.systemHome, { userHome: fx.userHome, launcher, }); try { program.parse(['node', 'mosaic', 'fleet', 'launch', 'fred']); expect(process.exitCode).toBe(1); expect(launcher).not.toHaveBeenCalled(); expect(stderr).toHaveBeenCalledWith(expect.stringContaining('unknown profile key "unknown"')); } finally { process.exitCode = priorExitCode; stderr.mockRestore(); } }); }); describe('dry-run composition', () => { it('renders a deterministic full composition and writes nothing', () => { const fx = fixture({ schema: 1, harness: 'claude', bundle: 'primary', model: 'opus', overlay: 'overlay.json', plugins: ['code-review'], skills: ['mosaic-tools'], env: { SEAT_FLAG: 'yes' }, }); writeFileSync( join(fx.systemHome, 'runtime', 'claude', 'settings.json'), '{"theme":"dark","hooks":["system"],"nested":{"system":true}}\n', ); mkdirSync(join(fx.userHome, 'config', 'claude'), { recursive: true }); writeFileSync( join(fx.userHome, 'config', 'claude', 'settings.json'), '{"hooks":["user"],"nested":{"user":true}}\n', ); writeFileSync(join(fx.agentDir, 'overlay.json'), '{"theme":null,"nested":{"agent":true}}\n'); mkdirSync(join(fx.userHome, 'plugins', 'code-review'), { recursive: true }); mkdirSync(join(fx.userHome, 'skills', 'mosaic-tools'), { recursive: true }); const plan = resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome, }); const output = formatFleetLaunchDryRun(plan).replaceAll(fx.root, ''); expect(output).toMatchInlineSnapshot(` "mosaic fleet launch fred --dry-run profile: /user/fleet/agents/fred/profile.json (schema 1) harness: claude seat-home: /user/fleet/agents/fred/.claude settings sources: system: /system/runtime/claude/settings.json user: /user/config/claude/settings.json agent: /user/fleet/agents/fred/overlay.json output: /user/fleet/agents/fred/.claude/settings.json snapshot: /user/fleet/agents/fred/settings.generated.json merged settings: { "hooks": [ "user" ], "nested": { "agent": true, "system": true, "user": true } } bundle: primary -> fred_example.com (fred@example.com) credential: /user/auth/claude/fred_example.com/.credentials.json symlinks: plugin code-review: /user/fleet/agents/fred/.claude/plugins/code-review -> /user/plugins/code-review skill mosaic-tools: /user/fleet/agents/fred/.claude/skills/mosaic-tools -> /user/skills/mosaic-tools declared env: CLAUDE_CONFIG_DIR=/user/fleet/agents/fred/.claude CLAUDE_SECURESTORAGE_CONFIG_DIR=/user/auth/claude/fred_example.com MOSAIC_AGENT_NAME=fred SEAT_FLAG=yes argv: ["claude","--model","opus"]" `); expect(() => readFileSync(join(fx.agentDir, '.claude', 'settings.json'), 'utf8')).toThrow(); applyFleetLaunchComposition(plan); expect(JSON.parse(readFileSync(plan.settings.output, 'utf8'))).toEqual({ hooks: ['user'], nested: { agent: true, system: true, user: true }, }); expect(readFileSync(plan.settings.snapshot, 'utf8')).toBe( readFileSync(plan.settings.output, 'utf8'), ); expect(plan.credential.link).toBeUndefined(); expect(plan.credential.dir).toBe(fx.namedBundleDir); }); });