feat(fleet): fleet ps surfaces unmanaged socket sessions (#586)
This commit was merged in pull request #586.
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
buildSystemdEnableCommand,
|
||||
buildSystemdShowCommand,
|
||||
buildTmuxListPanesCommand,
|
||||
buildTmuxListSessionsCommand,
|
||||
classifySendResult,
|
||||
detectDrift,
|
||||
enableFleetUnits,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
parseHeartbeat,
|
||||
parseSystemdShow,
|
||||
parseTmuxListPanes,
|
||||
parseTmuxListSessions,
|
||||
registerFleetCommand,
|
||||
resolveFleetPaths,
|
||||
RUNTIME_ACCEPTABLE_COMMANDS,
|
||||
@@ -1074,6 +1076,10 @@ describe('fleet ps — JSON output shape (FR-6)', () => {
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
if (fullArgs.includes('list-sessions')) {
|
||||
// Only the roster agent session on the socket (no unmanaged sessions)
|
||||
return { stdout: 'canary-pi\n', stderr: '', exitCode: 0 };
|
||||
}
|
||||
return { stdout: '', stderr: '', exitCode: 0 };
|
||||
};
|
||||
|
||||
@@ -1117,11 +1123,15 @@ describe('fleet ps — JSON output shape (FR-6)', () => {
|
||||
expect(row.runtime).toBe('pi');
|
||||
expect(row.systemdActive).toBe('active');
|
||||
expect(row.systemdEnabled).toBe('disabled');
|
||||
|
||||
// managed/source fields for roster agents
|
||||
expect(row.managed).toBe(true);
|
||||
expect(row.source).toBe('roster');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fleet ps — command sequences issued', () => {
|
||||
it('issues systemd show + tmux list-panes per agent', async () => {
|
||||
it('issues systemd show + tmux list-panes per agent, then list-sessions for socket discovery', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-'));
|
||||
const rosterPath = join(home, 'fleet', 'roster.yaml');
|
||||
await mkdir(join(home, 'fleet'), { recursive: true });
|
||||
@@ -1135,6 +1145,10 @@ describe('fleet ps — command sequences issued', () => {
|
||||
const calls: string[][] = [];
|
||||
const runner: CommandRunner = async (command, args) => {
|
||||
calls.push([command, ...args]);
|
||||
if ([command, ...args].join(' ').includes('list-sessions')) {
|
||||
// Only the roster agent — no unmanaged sessions
|
||||
return { stdout: 'coder0\n', stderr: '', exitCode: 0 };
|
||||
}
|
||||
return {
|
||||
stdout: 'ActiveState=inactive\nSubState=dead\nUnitFileState=enabled\n',
|
||||
stderr: '',
|
||||
@@ -1155,6 +1169,7 @@ describe('fleet ps — command sequences issued', () => {
|
||||
expect(calls).toEqual([
|
||||
buildSystemdShowCommand('coder0'),
|
||||
buildTmuxListPanesCommand('coder0', 'mosaic-factory'),
|
||||
buildTmuxListSessionsCommand('mosaic-factory'),
|
||||
]);
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
@@ -1163,6 +1178,258 @@ describe('fleet ps — command sequences issued', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTmuxListSessionsCommand', () => {
|
||||
it('builds exact list-sessions command with session_name format', () => {
|
||||
expect(buildTmuxListSessionsCommand('mosaic-factory')).toEqual([
|
||||
'tmux',
|
||||
'-L',
|
||||
'mosaic-factory',
|
||||
'list-sessions',
|
||||
'-F',
|
||||
'#{session_name}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses DEFAULT_SOCKET_NAME when socket is omitted', () => {
|
||||
const cmd = buildTmuxListSessionsCommand();
|
||||
expect(cmd[2]).toBe('mosaic-factory');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTmuxListSessions', () => {
|
||||
it('splits newline-delimited session names', () => {
|
||||
expect(parseTmuxListSessions('canary-pi\n_holder\nsome-adhoc\n')).toEqual([
|
||||
'canary-pi',
|
||||
'_holder',
|
||||
'some-adhoc',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns empty array for blank output', () => {
|
||||
expect(parseTmuxListSessions('')).toEqual([]);
|
||||
expect(parseTmuxListSessions(' \n \n')).toEqual([]);
|
||||
});
|
||||
|
||||
it('trims whitespace from each line', () => {
|
||||
expect(parseTmuxListSessions(' canary-pi \n some-adhoc \n')).toEqual([
|
||||
'canary-pi',
|
||||
'some-adhoc',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fleet ps — unmanaged socket sessions', () => {
|
||||
it('includes unmanaged session row flagged UNMANAGED and excludes _holder', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-'));
|
||||
const rosterPath = join(home, 'fleet', 'roster.yaml');
|
||||
await mkdir(join(home, 'fleet'), { recursive: true });
|
||||
await writeFile(
|
||||
rosterPath,
|
||||
[
|
||||
'version: 1',
|
||||
'transport: tmux',
|
||||
'agents:',
|
||||
' - name: canary-pi',
|
||||
' runtime: pi',
|
||||
' class: canary',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const nowMs = Date.now();
|
||||
const activityEpoch = Math.floor((nowMs - 10_000) / 1000);
|
||||
|
||||
const runner: CommandRunner = async (command, args) => {
|
||||
const full = [command, ...args].join(' ');
|
||||
if (full.includes('list-sessions')) {
|
||||
// Socket has: canary-pi (roster), _holder (excluded), some-adhoc (unmanaged)
|
||||
return { stdout: 'canary-pi\n_holder\nsome-adhoc\n', stderr: '', exitCode: 0 };
|
||||
}
|
||||
if (full.includes('list-panes')) {
|
||||
return { stdout: `99999 bash 0 ${activityEpoch}\n`, stderr: '', exitCode: 0 };
|
||||
}
|
||||
if (full.includes('systemctl') && full.includes('show')) {
|
||||
return {
|
||||
stdout: 'ActiveState=inactive\nSubState=dead\nUnitFileState=unknown\n',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
return { stdout: '', stderr: '', exitCode: 0 };
|
||||
};
|
||||
|
||||
const lines: string[] = [];
|
||||
const origLog = console.log;
|
||||
console.log = (msg: string) => {
|
||||
lines.push(msg);
|
||||
};
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerFleetCommand(program, { runner, mosaicHome: home });
|
||||
|
||||
try {
|
||||
await program.parseAsync(['node', 'mosaic', 'fleet', 'ps', '--json']);
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const json = JSON.parse(lines.join('')) as AgentPsRow[];
|
||||
expect(Array.isArray(json)).toBe(true);
|
||||
|
||||
// Should have 2 rows: canary-pi (roster) + some-adhoc (unmanaged); _holder excluded
|
||||
expect(json).toHaveLength(2);
|
||||
|
||||
// Roster agent comes first
|
||||
const rosterRow = json[0]!;
|
||||
expect(rosterRow.name).toBe('canary-pi');
|
||||
expect(rosterRow.managed).toBe(true);
|
||||
expect(rosterRow.source).toBe('roster');
|
||||
|
||||
// Unmanaged session comes second
|
||||
const unmanagedRow = json[1]!;
|
||||
expect(unmanagedRow.name).toBe('some-adhoc');
|
||||
expect(unmanagedRow.managed).toBe(false);
|
||||
expect(unmanagedRow.source).toBe('socket');
|
||||
expect(unmanagedRow.runtime).toBe('unknown');
|
||||
|
||||
// _holder must not appear
|
||||
expect(json.map((r) => r.name)).not.toContain('_holder');
|
||||
|
||||
// tenant_id and host must be present on unmanaged rows
|
||||
expect(typeof unmanagedRow.tenant_id).toBe('string');
|
||||
expect(unmanagedRow.tenant_id.length).toBeGreaterThan(0);
|
||||
expect(typeof unmanagedRow.host).toBe('string');
|
||||
expect(unmanagedRow.host.length).toBeGreaterThan(0);
|
||||
|
||||
// driftFlag must be false for unmanaged (no roster runtime to compare)
|
||||
expect(unmanagedRow.driftFlag).toBe(false);
|
||||
});
|
||||
|
||||
it('shows UNMANAGED flag in table output for unmanaged sessions', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-'));
|
||||
const rosterPath = join(home, 'fleet', 'roster.yaml');
|
||||
await mkdir(join(home, 'fleet'), { recursive: true });
|
||||
await writeFile(
|
||||
rosterPath,
|
||||
[
|
||||
'version: 1',
|
||||
'transport: tmux',
|
||||
'agents:',
|
||||
' - name: canary-pi',
|
||||
' runtime: pi',
|
||||
' class: canary',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const runner: CommandRunner = async (command, args) => {
|
||||
const full = [command, ...args].join(' ');
|
||||
if (full.includes('list-sessions')) {
|
||||
return { stdout: 'canary-pi\nsome-adhoc\n', stderr: '', exitCode: 0 };
|
||||
}
|
||||
if (full.includes('list-panes')) {
|
||||
return { stdout: '0 bash 1 0\n', stderr: '', exitCode: 0 };
|
||||
}
|
||||
if (full.includes('systemctl') && full.includes('show')) {
|
||||
return {
|
||||
stdout: 'ActiveState=inactive\nSubState=dead\nUnitFileState=unknown\n',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
return { stdout: '', stderr: '', exitCode: 0 };
|
||||
};
|
||||
|
||||
const lines: string[] = [];
|
||||
const origLog = console.log;
|
||||
console.log = (msg: string) => {
|
||||
lines.push(msg);
|
||||
};
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerFleetCommand(program, { runner, mosaicHome: home });
|
||||
|
||||
try {
|
||||
await program.parseAsync(['node', 'mosaic', 'fleet', 'ps']);
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const tableOutput = lines.join('\n');
|
||||
// some-adhoc row must appear with UNMANAGED flag
|
||||
expect(tableOutput).toMatch(/some-adhoc/);
|
||||
expect(tableOutput).toMatch(/UNMANAGED/);
|
||||
// canary-pi roster row must not have UNMANAGED
|
||||
const rosterLine = lines.find((l) => l.includes('canary-pi'));
|
||||
expect(rosterLine).toBeDefined();
|
||||
expect(rosterLine).not.toMatch(/UNMANAGED/);
|
||||
});
|
||||
|
||||
it('gracefully shows only roster rows when list-sessions fails (socket missing)', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-'));
|
||||
const rosterPath = join(home, 'fleet', 'roster.yaml');
|
||||
await mkdir(join(home, 'fleet'), { recursive: true });
|
||||
await writeFile(
|
||||
rosterPath,
|
||||
[
|
||||
'version: 1',
|
||||
'transport: tmux',
|
||||
'agents:',
|
||||
' - name: canary-pi',
|
||||
' runtime: pi',
|
||||
' class: canary',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const runner: CommandRunner = async (command, args) => {
|
||||
const full = [command, ...args].join(' ');
|
||||
if (full.includes('list-sessions')) {
|
||||
// Simulate socket missing
|
||||
return { stdout: '', stderr: 'no server running on /tmp/...', exitCode: 1 };
|
||||
}
|
||||
if (full.includes('list-panes')) {
|
||||
return { stdout: '12345 pi 0 0\n', stderr: '', exitCode: 0 };
|
||||
}
|
||||
if (full.includes('systemctl') && full.includes('show')) {
|
||||
return {
|
||||
stdout: 'ActiveState=inactive\nSubState=dead\nUnitFileState=enabled\n',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
return { stdout: '', stderr: '', exitCode: 0 };
|
||||
};
|
||||
|
||||
const lines: string[] = [];
|
||||
const origLog = console.log;
|
||||
console.log = (msg: string) => {
|
||||
lines.push(msg);
|
||||
};
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerFleetCommand(program, { runner, mosaicHome: home });
|
||||
|
||||
try {
|
||||
// Must not throw
|
||||
await expect(
|
||||
program.parseAsync(['node', 'mosaic', 'fleet', 'ps', '--json']),
|
||||
).resolves.toBeDefined();
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const json = JSON.parse(lines.join('')) as AgentPsRow[];
|
||||
// Only roster agent visible; no crash
|
||||
expect(json).toHaveLength(1);
|
||||
expect(json[0]!.name).toBe('canary-pi');
|
||||
expect(json[0]!.managed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('agent watch', () => {
|
||||
it('builds exact grouped-viewer creation command', () => {
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user