fix(#792): fleet roster ENOENT actionable exit + installer heading printf (#818)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #818.
This commit is contained in:
2026-07-17 21:36:36 +00:00
parent 686c881fe4
commit 3f77229e88
8 changed files with 248 additions and 10 deletions

View File

@@ -60,6 +60,7 @@ import {
type SleepFn,
} from './fleet.js';
import { registerAgentCommand } from './agent.js';
import { parseFleetRosterDocument, readFleetRosterText } from '../fleet/fleet-roster-v1.js';
function buildProgram(): Command {
const program = new Command();
@@ -166,6 +167,91 @@ describe('registerFleetCommand', () => {
});
});
describe('fleet roster configuration failures', () => {
it('reports a missing roster with an actionable initialization hint', async () => {
const home = await tempDir();
try {
const program = buildProgram();
await expect(
program.parseAsync(['node', 'mosaic', 'fleet', '--mosaic-home', home, 'ps']),
).rejects.toThrow(
`No fleet roster found at ${join(home, 'fleet', 'roster.json')}. Run \`mosaic fleet init\``,
);
} finally {
await rm(home, { recursive: true, force: true });
}
});
it('reports a malformed roster with the path and repair hint', async () => {
const home = await tempDir();
const rosterPath = join(home, 'fleet', 'roster.json');
try {
await mkdir(dirname(rosterPath), { recursive: true });
await writeFile(rosterPath, '{not valid json');
await expect(loadFleetRoster(rosterPath)).rejects.toThrow(
`Fleet roster at ${rosterPath} is invalid. Fix the file or run \`mosaic fleet init --force\``,
);
} finally {
await rm(home, { recursive: true, force: true });
}
});
it('reports an unreadable roster without exposing the filesystem error', async () => {
const home = await tempDir();
try {
await expect(readFleetRosterText(home)).rejects.toThrow(
`Could not read fleet roster at ${home}. Check the file exists and is readable.`,
);
} finally {
await rm(home, { recursive: true, force: true });
}
});
it('reports a malformed roster while selecting the v1/v2 command path', () => {
expect(() => parseFleetRosterDocument('version: [', '/srv/mosaic/fleet/roster.yaml')).toThrow(
'Fleet roster at /srv/mosaic/fleet/roster.yaml is invalid. Fix the file or run `mosaic fleet init --force`.',
);
});
it('reports a semantically invalid v1 roster as an actionable nonzero command error', async () => {
const home = await tempDir();
const rosterPath = join(home, 'fleet', 'roster.yaml');
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
try {
await mkdir(dirname(rosterPath), { recursive: true });
await writeFile(
rosterPath,
[
'version: 1',
'transport: tmux',
'agents:',
' - name: canary-pi',
' runtime: pi',
' - name: canary-pi',
' runtime: codex',
].join('\n'),
);
await expect(
buildProgram().parseAsync(['node', 'mosaic', 'fleet', '--mosaic-home', home, 'ps']),
).rejects.toMatchObject({
code: 'fleet.roster',
exitCode: 1,
message: 'Fleet roster has duplicate agent name: canary-pi.',
});
expect(stderrSpy.mock.calls.flat().join('')).toContain(
'Fleet roster has duplicate agent name: canary-pi.',
);
expect(stderrSpy.mock.calls.flat().join('')).not.toMatch(/\n\s+at\s/);
} finally {
stderrSpy.mockRestore();
await rm(home, { recursive: true, force: true });
}
});
});
describe('fleet roster parsing', () => {
let cleanup: string | undefined;