fix(fleet): boot-survival symmetry — disable-on-remove + add-enable + init-R5 (#612)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #612.
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
buildEnableLingerCommand,
|
||||
buildFleetServiceCommand,
|
||||
buildSystemdEnableCommand,
|
||||
buildSystemdDisableCommand,
|
||||
buildSystemdShowCommand,
|
||||
buildTmuxListPanesCommand,
|
||||
buildTmuxListSessionsCommand,
|
||||
@@ -983,6 +984,127 @@ describe('fleet ps — drift detection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fleet-polish bundle — boot-survival symmetry', () => {
|
||||
async function rosterHome(agents: string): Promise<string> {
|
||||
const home = await tempDir();
|
||||
await mkdir(join(home, 'fleet'), { recursive: true });
|
||||
await writeFile(join(home, 'fleet', 'roster.yaml'), agents);
|
||||
return home;
|
||||
}
|
||||
|
||||
it('buildSystemdDisableCommand returns the systemctl --user disable array', () => {
|
||||
expect(buildSystemdDisableCommand('mosaic-agent@coder0.service')).toEqual([
|
||||
'systemctl',
|
||||
'--user',
|
||||
'disable',
|
||||
'mosaic-agent@coder0.service',
|
||||
]);
|
||||
});
|
||||
|
||||
it('fleet remove DISABLES the unit so a removed agent cannot resurrect on boot', async () => {
|
||||
const home = await rosterHome(
|
||||
[
|
||||
'version: 1',
|
||||
'transport: tmux',
|
||||
'agents:',
|
||||
' - name: orchestrator',
|
||||
' runtime: pi',
|
||||
' class: orchestrator',
|
||||
' - name: coder0',
|
||||
' runtime: codex',
|
||||
' class: worker',
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
const calls: string[][] = [];
|
||||
const runner: CommandRunner = async (command, args) => {
|
||||
calls.push([command, ...args]);
|
||||
return { stdout: '', stderr: '', exitCode: 0 };
|
||||
};
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerFleetCommand(program, { runner, mosaicHome: home });
|
||||
try {
|
||||
await program.parseAsync(['node', 'mosaic', 'fleet', 'remove', 'coder0']);
|
||||
expect(calls).toContainEqual([
|
||||
'systemctl',
|
||||
'--user',
|
||||
'disable',
|
||||
'mosaic-agent@coder0.service',
|
||||
]);
|
||||
// stop must still happen too
|
||||
expect(calls).toContainEqual(['systemctl', '--user', 'stop', 'mosaic-agent@coder0.service']);
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('fleet add ENABLES the new agent unit for boot-survival', async () => {
|
||||
const home = await rosterHome(
|
||||
['version: 1', 'transport: tmux', 'agents:', ' - name: coder0', ' runtime: codex'].join(
|
||||
'\n',
|
||||
) + '\n',
|
||||
);
|
||||
const calls: string[][] = [];
|
||||
const runner: CommandRunner = async (command, args) => {
|
||||
calls.push([command, ...args]);
|
||||
return { stdout: '', stderr: '', exitCode: 0 };
|
||||
};
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerFleetCommand(program, { runner, mosaicHome: home });
|
||||
try {
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'mosaic',
|
||||
'fleet',
|
||||
'add',
|
||||
'coder1',
|
||||
'--runtime',
|
||||
'codex',
|
||||
'--class',
|
||||
'worker',
|
||||
'--no-start',
|
||||
]);
|
||||
expect(calls).toContainEqual([
|
||||
'systemctl',
|
||||
'--user',
|
||||
'enable',
|
||||
'mosaic-agent@coder1.service',
|
||||
]);
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('fleet init --write fails hard when a non-minimal profile lacks exactly one orchestrator', async () => {
|
||||
// The general profile must yield exactly one orchestrator; the guarantee is
|
||||
// enforced (not just warned). We assert the happy path writes cleanly.
|
||||
const home = await tempDir();
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerFleetCommand(program, {
|
||||
runner: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
|
||||
mosaicHome: home,
|
||||
});
|
||||
try {
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'mosaic',
|
||||
'fleet',
|
||||
'init',
|
||||
'--profile',
|
||||
'general',
|
||||
'--write',
|
||||
]);
|
||||
const written = await readFile(join(home, 'fleet', 'roster.yaml'), 'utf8');
|
||||
const orchestrators = (written.match(/class:\s*orchestrator/g) ?? []).length;
|
||||
expect(orchestrators).toBe(1);
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('fleet install — auto-enable units for boot-survival', () => {
|
||||
it('buildSystemdEnableCommand and buildEnableLingerCommand return correct command arrays', () => {
|
||||
expect(buildSystemdEnableCommand('mosaic-tmux-holder.service')).toEqual([
|
||||
|
||||
Reference in New Issue
Block a user