feat(fleet): compose and launch profile-backed seats

This commit is contained in:
Jason Woltje
2026-08-13 11:30:53 -05:00
parent 216cd72226
commit 378c227cbb
6 changed files with 1340 additions and 36 deletions
@@ -0,0 +1,370 @@
import {
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
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;
} {
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', 'claude', 'fred_example.com');
mkdirSync(join(systemHome, 'framework', 'runtime', 'claude'), { recursive: true });
mkdirSync(agentDir, { recursive: true });
mkdirSync(namedBundleDir, { recursive: true });
writeFileSync(join(systemHome, 'framework', 'runtime', 'claude', 'settings.json'), '{}\n');
writeFileSync(join(agentDir, 'profile.json'), `${JSON.stringify(profile, null, 2)}\n`);
writeFileSync(join(namedBundleDir, '.credentials.json'), '{}\n', { mode: 0o600 });
writeFileSync(
join(namedBundleDir, 'account.json'),
'{"oauthAccount":{"emailAddress":"[email protected]"}}\n',
);
symlinkSync('fred_example.com', join(userHome, 'auth', 'claude', 'primary'), 'dir');
return { root, systemHome, userHome, agentDir, namedBundleDir };
}
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('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('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('accepts a real 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);
});
});
describe('managed plugin and skill links', () => {
it('installs listed entries and prunes only stale managed symlinks', () => {
const fx = fixture({
schema: 1,
harness: 'claude',
plugins: ['keep'],
skills: ['mosaic-tools'],
});
mkdirSync(join(fx.userHome, 'plugins', 'keep'), { recursive: true });
mkdirSync(join(fx.userHome, 'plugins', 'old'), { recursive: true });
mkdirSync(join(fx.userHome, 'skills', 'mosaic-tools'), { recursive: true });
const pluginHome = join(fx.agentDir, '.claude', 'plugins');
mkdirSync(pluginHome, { recursive: true });
symlinkSync(join(fx.userHome, 'plugins', 'old'), join(pluginHome, 'old'), 'dir');
const plan = resolveFleetLaunchComposition('fred', {
systemHome: fx.systemHome,
userHome: fx.userHome,
});
expect(plan.prune).toEqual([join(pluginHome, 'old')]);
applyFleetLaunchComposition(plan);
expect(() => lstatSync(join(pluginHome, 'old'))).toThrow();
expect(lstatSync(join(pluginHome, 'keep')).isSymbolicLink()).toBe(true);
expect(lstatSync(join(fx.agentDir, '.claude', 'skills', 'mosaic-tools')).isSymbolicLink()).toBe(
true,
);
});
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'),
MOSAIC_AGENT_NAME: 'fred',
SEAT_FLAG: 'yes',
},
{ agentDir: fx.agentDir },
);
expect(lstatSync(join(fx.agentDir, '.claude', '.credentials.json')).isSymbolicLink()).toBe(
true,
);
});
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, 'framework', '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/framework/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])
symlinks:
credentials: <ROOT>/user/fleet/agents/fred/.claude/.credentials.json -> <ROOT>/user/auth/claude/fred_example.com/.credentials.json
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
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(lstatSync(plan.credential.link).isSymbolicLink()).toBe(true);
});
});