--socket forwards -L verbatim when given; when OMITTED the CLI sends no -L at all and agent-send.sh's own resolution governs (explicit -L > MOSAIC_TMUX_SOCKET > unique hit > ambiguity refusal, B1/PR #1466). Measured design note: comms.ts was already correct by construction after B1 — this change pins it with a spec arm (no -L forwarded when --socket omitted) and makes the contract explicit in the option help, so a future default-guess here cannot silently reintroduce the stale-twin defect. Specs 7/7.
155 lines
5.7 KiB
TypeScript
155 lines
5.7 KiB
TypeScript
import {
|
|
chmodSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
writeFileSync,
|
|
readFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import { Command } from 'commander';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
import { fleetCommsSendArgs, registerCommsCommand, tmuxSendArgs } from './comms.js';
|
|
|
|
describe('arg translation', () => {
|
|
// B2 (2026-08-29): when --socket is omitted, the CLI forwards NO -L and
|
|
// agent-send.sh's own resolution governs (explicit -L > MOSAIC_TMUX_SOCKET
|
|
// > unique hit > ambiguity refusal). Forwarding a guessed default here
|
|
// would defeat that resolution and reintroduce the stale-twin defect.
|
|
it('omits -L entirely when --socket is not given (tool-owned resolution)', () => {
|
|
const args = tmuxSendArgs('orch-01', 'hello', {});
|
|
expect(args).toEqual(['-s', 'orch-01', '-m', 'hello']);
|
|
expect(args).not.toContain('-L');
|
|
expect(args.join(' ')).not.toContain('-L');
|
|
});
|
|
|
|
it('tmux path: -s/-C/-L/-f/-m per agent-send.sh getopts', () => {
|
|
expect(tmuxSendArgs('orch-01', 'hello', {})).toEqual(['-s', 'orch-01', '-m', 'hello']);
|
|
expect(
|
|
tmuxSendArgs('orch-01', 'unused', {
|
|
class: 'actionable',
|
|
socket: 'mosaic-fleet',
|
|
file: '/tmp/body.txt',
|
|
}),
|
|
).toEqual(['-s', 'orch-01', '-C', 'actionable', '-L', 'mosaic-fleet', '-f', '/tmp/body.txt']);
|
|
});
|
|
|
|
it('fleet-comms path: -t site/agent and -c class', () => {
|
|
expect(fleetCommsSendArgs('usc', 'fred', 'hi', {})).toEqual(['-t', 'usc/fred', '-m', 'hi']);
|
|
expect(fleetCommsSendArgs('usc', 'fred', 'hi', { class: 'human' })).toEqual([
|
|
'-t',
|
|
'usc/fred',
|
|
'-c',
|
|
'human',
|
|
'-m',
|
|
'hi',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('registerCommsCommand routing', () => {
|
|
const savedBrain = process.env['MOSAIC_BRAIN_HOME'];
|
|
const savedRepo = process.env['MOSAIC_FLEET_COMMS_REPO'];
|
|
const savedAgent = process.env['MOSAIC_AGENT_NAME'];
|
|
afterEach(() => {
|
|
for (const [k, v] of [
|
|
['MOSAIC_BRAIN_HOME', savedBrain],
|
|
['MOSAIC_FLEET_COMMS_REPO', savedRepo],
|
|
['MOSAIC_AGENT_NAME', savedAgent],
|
|
] as const) {
|
|
if (v === undefined) delete process.env[k];
|
|
else process.env[k] = v;
|
|
}
|
|
process.exitCode = undefined;
|
|
});
|
|
|
|
function fixture(): { brain: string; repo: string; tmuxLog: string; commsLog: string } {
|
|
const brain = mkdtempSync(join(tmpdir(), 'comms-brain-'));
|
|
const repo = mkdtempSync(join(tmpdir(), 'comms-repo-'));
|
|
mkdirSync(join(brain, 'tools', 'tmux'), { recursive: true });
|
|
mkdirSync(join(repo, 'tools'), { recursive: true });
|
|
const tmuxLog = join(brain, 'tmux.log');
|
|
const commsLog = join(repo, 'comms.log');
|
|
writeFileSync(
|
|
join(brain, 'tools', 'tmux', 'agent-send.sh'),
|
|
`#!/usr/bin/env bash\nprintf '%s\\n' "$*" >> ${JSON.stringify(tmuxLog)}\nexit 7\n`,
|
|
);
|
|
writeFileSync(
|
|
join(repo, 'tools', 'comms-send.sh'),
|
|
`#!/usr/bin/env bash\nprintf '%s\\n' "$*" >> ${JSON.stringify(commsLog)}\nprintf 'FLEET_COMMS_REPO=%s FLEET_COMMS_SITE=%s\\n' "$FLEET_COMMS_REPO" "$FLEET_COMMS_SITE" >> ${JSON.stringify(commsLog)}\nexit 5\n`,
|
|
);
|
|
chmodSync(join(brain, 'tools', 'tmux', 'agent-send.sh'), 0o755);
|
|
chmodSync(join(repo, 'tools', 'comms-send.sh'), 0o755);
|
|
process.env['MOSAIC_BRAIN_HOME'] = brain;
|
|
process.env['MOSAIC_FLEET_COMMS_REPO'] = repo;
|
|
process.env['MOSAIC_AGENT_NAME'] = 'tester';
|
|
return { brain, repo, tmuxLog, commsLog };
|
|
}
|
|
|
|
it('default routes same-host via agent-send with translated flags and passes rc through', async () => {
|
|
const f = fixture();
|
|
const program = new Command();
|
|
registerCommsCommand(program);
|
|
await program.parseAsync(
|
|
[
|
|
'comms',
|
|
'send',
|
|
'orch-01',
|
|
'--class',
|
|
'actionable',
|
|
'--socket',
|
|
'mosaic-fleet',
|
|
'verdict',
|
|
'landed',
|
|
],
|
|
{ from: 'user' },
|
|
);
|
|
expect(process.exitCode).toBe(7);
|
|
expect(readFileSync(f.tmuxLog, 'utf8').trim()).toBe(
|
|
'-s orch-01 -C actionable -L mosaic-fleet -m verdict landed',
|
|
);
|
|
expect(existsSync(f.commsLog)).toBe(false); // inter-site tool never invoked
|
|
});
|
|
|
|
it('--site routes inter-site via comms-send with site-prefixed target and passes rc through', async () => {
|
|
const f = fixture();
|
|
const program = new Command();
|
|
registerCommsCommand(program);
|
|
await program.parseAsync(
|
|
['comms', 'send', 'fred', '--site', 'usc', '--class', 'human', 'hello', 'there'],
|
|
{ from: 'user' },
|
|
);
|
|
expect(process.exitCode).toBe(5);
|
|
expect(readFileSync(f.commsLog, 'utf8').split('\n')[0]?.trim()).toBe(
|
|
'-t usc/fred -c human -m hello there',
|
|
);
|
|
// The sender must bind comms-send.sh to the SELECTED repo (codex 9c8b6ebf).
|
|
expect(readFileSync(f.commsLog, 'utf8')).toContain(
|
|
`FLEET_COMMS_REPO=${f.repo} FLEET_COMMS_SITE=usc`,
|
|
);
|
|
expect(existsSync(f.tmuxLog)).toBe(false); // same-host tool never invoked
|
|
});
|
|
|
|
it('inter-site without MOSAIC_AGENT_NAME is an invocation defect (exit 2)', async () => {
|
|
const f = fixture();
|
|
delete process.env['MOSAIC_AGENT_NAME'];
|
|
const program = new Command();
|
|
registerCommsCommand(program);
|
|
await program.parseAsync(['comms', 'send', 'fred', '--site', 'usc', 'hi'], { from: 'user' });
|
|
expect(process.exitCode).toBe(2);
|
|
expect(existsSync(f.commsLog)).toBe(false); // inter-site tool never invoked
|
|
});
|
|
|
|
it('missing fleet-comms repo fails 127 naming the expected path', async () => {
|
|
fixture();
|
|
process.env['MOSAIC_FLEET_COMMS_REPO'] = '/nonexistent-comms-repo';
|
|
const program = new Command();
|
|
registerCommsCommand(program);
|
|
await program.parseAsync(['comms', 'send', 'fred', '--site', 'usc', 'hi'], { from: 'user' });
|
|
expect(process.exitCode).toBe(127);
|
|
});
|
|
});
|