Files
stack/packages/mosaic/src/commands/fleet-launch-command.spec.ts
T
terra a12eeb4786 fleet: share Claude credentials by directory env, not a seat symlink
Claude Code saves credentials by writing a sibling temp file and rename()-ing
it over the target. rename(2) replaces a symlink rather than following it, so
the managed link W-F1/W-F2 planted at <seat>/.claude/.credentials.json is
destroyed by the first token refresh and the seat silently forks its
credentials. The in-place fallback arm opens with O_NOFOLLOW and would refuse
the link anyway. Evidence, quoting the 2.1.232 binary:
docs/reports/harness/claude-credential-write-path-2026-08-14.md (jarvis-brain).

CLAUDE_SECURESTORAGE_CONFIG_DIR resolves the credential directory
independently of CLAUDE_CONFIG_DIR, so the temp file and the rename both land
inside the bundle. That is the property the design wanted -- share the
credential, never the transcripts -- with no symlink and no privileges.

- new fleet/credential-sharing.ts owns the harness -> credential-file and
  harness -> credential-directory-variable maps, so scaffold and launch cannot
  disagree about the mechanism. It also removes the duplicate credential-file
  name table the two already carried.
- launch composes CLAUDE_SECURESTORAGE_CONFIG_DIR from the resolved bundle
  directory and plans no credential link for Claude. The value is always the
  absolute bundle path: Claude reads an empty value as ~/.claude, which is the
  operator's own account.
- scaffold stops emitting the credential symlink and its manifest entry for
  Claude, and tolerates one left by an earlier scaffold rather than reporting
  it as a foreign file or rewriting it.
- FIRST_AUTH_REFUSAL still fires when a real file occupies the seat path.
- Harnesses absent from the map (pi, codex, opencode) keep managed links; the
  containment specs now exercise them on pi.

Answers promotion gate #1 negatively for the frozen mechanism and positively
for the replacement. E3.3 (two seats refreshing one bundle at once) is still
open.
2026-08-14 18:20:54 -05:00

803 lines
29 KiB
TypeScript

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<string, unknown> = { 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":"[email protected]"}}\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":"[email protected]"}}\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 ([email protected])');
});
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, '<ROOT>');
expect(output).toMatchInlineSnapshot(`
"mosaic fleet launch fred --dry-run
profile: <ROOT>/user/fleet/agents/fred/profile.json (schema 1)
harness: claude
seat-home: <ROOT>/user/fleet/agents/fred/.claude
settings sources:
system: <ROOT>/system/runtime/claude/settings.json
user: <ROOT>/user/config/claude/settings.json
agent: <ROOT>/user/fleet/agents/fred/overlay.json
output: <ROOT>/user/fleet/agents/fred/.claude/settings.json
snapshot: <ROOT>/user/fleet/agents/fred/settings.generated.json
merged settings:
{
"hooks": [
"user"
],
"nested": {
"agent": true,
"system": true,
"user": true
}
}
bundle: primary -> fred_example.com ([email protected])
credential: <ROOT>/user/auth/claude/fred_example.com/.credentials.json
symlinks:
plugin code-review: <ROOT>/user/fleet/agents/fred/.claude/plugins/code-review -> <ROOT>/user/plugins/code-review
skill mosaic-tools: <ROOT>/user/fleet/agents/fred/.claude/skills/mosaic-tools -> <ROOT>/user/skills/mosaic-tools
declared env:
CLAUDE_CONFIG_DIR=<ROOT>/user/fleet/agents/fred/.claude
CLAUDE_SECURESTORAGE_CONFIG_DIR=<ROOT>/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);
});
});