733 lines
25 KiB
TypeScript
733 lines
25 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
chmodSync,
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
writeFileSync,
|
|
rmSync,
|
|
readFileSync,
|
|
symlinkSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { parseFleetRosterV1, type FleetRoster, type FleetAgent } from './fleet-roster-v1.js';
|
|
import {
|
|
buildFleetCommsBlock,
|
|
renderPeerReach,
|
|
readFleetCommsBlock,
|
|
resolveCommsBlock,
|
|
resolvePeerCommand,
|
|
renderToolsContractStatus,
|
|
} from './comms-onboarding.js';
|
|
|
|
const ROSTER = [
|
|
'version: 1',
|
|
'transport: tmux',
|
|
'tmux:',
|
|
' socket_name: mosaic-fleet',
|
|
'agents:',
|
|
' - name: orchestrator',
|
|
' runtime: claude',
|
|
' class: orchestrator',
|
|
' host: w-jarvis',
|
|
' - name: enhancer',
|
|
' runtime: claude',
|
|
' class: enhancer',
|
|
' host: w-jarvis',
|
|
' - name: coder0',
|
|
' runtime: pi',
|
|
' class: implementer',
|
|
' host: w-jarvis',
|
|
' - name: coder0-0',
|
|
' runtime: claude',
|
|
' class: implementer',
|
|
' host: 10.1.10.37',
|
|
' ssh: jwoltje@10.1.10.37',
|
|
'',
|
|
].join('\n');
|
|
|
|
function roster(source = ROSTER): FleetRoster {
|
|
return parseFleetRosterV1(source, 'yaml');
|
|
}
|
|
|
|
describe('shared fleet roster v1 resolver', () => {
|
|
it('resolves comms fields and the global socket through the canonical roster contract', () => {
|
|
const resolved = roster();
|
|
expect(resolved.tmux.socketName).toBe('mosaic-fleet');
|
|
expect(resolved.agents.find((agent) => agent.name === 'coder0-0')).toMatchObject({
|
|
className: 'code',
|
|
host: '10.1.10.37',
|
|
ssh: 'jwoltje@10.1.10.37',
|
|
});
|
|
});
|
|
|
|
it('rejects unknown fields instead of leniently constructing a second roster view', () => {
|
|
expect(() => parseFleetRosterV1(`${ROSTER}\nunknown: value\n`, 'yaml')).toThrow(
|
|
/unknown field/i,
|
|
);
|
|
});
|
|
|
|
it('rejects an unsupported independent per-agent socket instead of targeting a nonexistent session', () => {
|
|
expect(() =>
|
|
roster(
|
|
ROSTER.replace(
|
|
' host: w-jarvis\n - name: coder0',
|
|
' host: w-jarvis\n socket: other-socket\n - name: coder0',
|
|
),
|
|
),
|
|
).toThrow(/independent per-agent sockets are not supported/i);
|
|
});
|
|
|
|
it('rejects unsafe operational targeting values', () => {
|
|
expect(() =>
|
|
roster(ROSTER.replace(' ssh: jwoltje@10.1.10.37', ' ssh: host;touch-owned')),
|
|
).toThrow(/unsupported targeting characters/i);
|
|
});
|
|
|
|
it('normalizes matching connector settings for YAML and JSON rosters', () => {
|
|
const yamlSource = `${ROSTER}connector:\n kind: discord\n discord:\n channel_id: "123"\n`;
|
|
const jsonSource = JSON.stringify({
|
|
version: 1,
|
|
transport: 'tmux',
|
|
agents: [{ name: 'a', runtime: 'claude', class: 'worker' }],
|
|
connector: {
|
|
kind: 'matrix',
|
|
matrix: {
|
|
homeserver_url: 'https://matrix.example',
|
|
user_id: '@a:example',
|
|
room_id: '!room:example',
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parseFleetRosterV1(yamlSource, 'yaml').connector).toEqual({
|
|
kind: 'discord',
|
|
discord: { channelId: '123' },
|
|
});
|
|
expect(parseFleetRosterV1(jsonSource, 'json').connector).toEqual({
|
|
kind: 'matrix',
|
|
matrix: {
|
|
homeserverUrl: 'https://matrix.example',
|
|
userId: '@a:example',
|
|
roomId: '!room:example',
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
['discord channel_id', { kind: 'discord', discord: { channel_id: '' } }],
|
|
['discord channel_id whitespace', { kind: 'discord', discord: { channel_id: ' ' } }],
|
|
[
|
|
'matrix homeserver_url',
|
|
{
|
|
kind: 'matrix',
|
|
matrix: { homeserver_url: '', user_id: '@a:example', room_id: '!room:example' },
|
|
},
|
|
],
|
|
[
|
|
'matrix homeserver_url whitespace',
|
|
{
|
|
kind: 'matrix',
|
|
matrix: { homeserver_url: '\t', user_id: '@a:example', room_id: '!room:example' },
|
|
},
|
|
],
|
|
[
|
|
'matrix user_id',
|
|
{
|
|
kind: 'matrix',
|
|
matrix: {
|
|
homeserver_url: 'https://matrix.example',
|
|
user_id: '',
|
|
room_id: '!room:example',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
'matrix user_id whitespace',
|
|
{
|
|
kind: 'matrix',
|
|
matrix: {
|
|
homeserver_url: 'https://matrix.example',
|
|
user_id: ' ',
|
|
room_id: '!room:example',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
'matrix room_id',
|
|
{
|
|
kind: 'matrix',
|
|
matrix: {
|
|
homeserver_url: 'https://matrix.example',
|
|
user_id: '@a:example',
|
|
room_id: '',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
'matrix room_id whitespace',
|
|
{
|
|
kind: 'matrix',
|
|
matrix: {
|
|
homeserver_url: 'https://matrix.example',
|
|
user_id: '@a:example',
|
|
room_id: '\n',
|
|
},
|
|
},
|
|
],
|
|
])(
|
|
'rejects empty or whitespace-only parser-required connector string: %s',
|
|
(_label, connector) => {
|
|
expect(() =>
|
|
parseFleetRosterV1(
|
|
JSON.stringify({
|
|
version: 1,
|
|
transport: 'tmux',
|
|
agents: [{ name: 'a', runtime: 'claude', class: 'worker' }],
|
|
connector,
|
|
}),
|
|
'json',
|
|
),
|
|
).toThrow(/required/i);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
['tmux with discord settings', { kind: 'tmux', discord: { channel_id: '123' } }],
|
|
['discord without discord settings', { kind: 'discord' }],
|
|
[
|
|
'discord with matrix settings',
|
|
{ kind: 'discord', discord: { channel_id: '123' }, matrix: {} },
|
|
],
|
|
['matrix without matrix settings', { kind: 'matrix' }],
|
|
[
|
|
'matrix with discord settings',
|
|
{
|
|
kind: 'matrix',
|
|
matrix: {
|
|
homeserver_url: 'https://matrix.example',
|
|
user_id: '@a:example',
|
|
room_id: '!room:example',
|
|
},
|
|
discord: { channel_id: '123' },
|
|
},
|
|
],
|
|
])('rejects connector kind/settings mismatch: %s', (_label, connector) => {
|
|
expect(() =>
|
|
parseFleetRosterV1(
|
|
JSON.stringify({
|
|
version: 1,
|
|
transport: 'tmux',
|
|
agents: [{ name: 'a', runtime: 'claude', class: 'worker' }],
|
|
connector,
|
|
}),
|
|
'json',
|
|
),
|
|
).toThrow();
|
|
});
|
|
|
|
it.each([
|
|
['tmux socket', ['tmux', 'socket_name'], ['tmux', 'socketName'], 'same', 'different'],
|
|
['tmux holder', ['tmux', 'holder_session'], ['tmux', 'holderSession'], 'same', 'different'],
|
|
[
|
|
'defaults working directory',
|
|
['defaults', 'working_directory'],
|
|
['defaults', 'workingDirectory'],
|
|
'same',
|
|
'different',
|
|
],
|
|
[
|
|
'runtime reset command',
|
|
['runtimes', 'claude', 'reset_command'],
|
|
['runtimes', 'claude', 'resetCommand'],
|
|
'same',
|
|
'different',
|
|
],
|
|
[
|
|
'agent working directory',
|
|
['agents', 0, 'working_directory'],
|
|
['agents', 0, 'workingDirectory'],
|
|
'same',
|
|
'different',
|
|
],
|
|
[
|
|
'agent model hint',
|
|
['agents', 0, 'model_hint'],
|
|
['agents', 0, 'modelHint'],
|
|
'same',
|
|
'different',
|
|
],
|
|
[
|
|
'agent reasoning level',
|
|
['agents', 0, 'reasoning_level'],
|
|
['agents', 0, 'reasoningLevel'],
|
|
'same',
|
|
'different',
|
|
],
|
|
[
|
|
'agent tool policy',
|
|
['agents', 0, 'tool_policy'],
|
|
['agents', 0, 'toolPolicy'],
|
|
'same',
|
|
'different',
|
|
],
|
|
[
|
|
'agent persistent persona',
|
|
['agents', 0, 'persistent_persona'],
|
|
['agents', 0, 'persistentPersona'],
|
|
true,
|
|
false,
|
|
],
|
|
[
|
|
'agent reset between tasks',
|
|
['agents', 0, 'reset_between_tasks'],
|
|
['agents', 0, 'resetBetweenTasks'],
|
|
true,
|
|
false,
|
|
],
|
|
[
|
|
'agent kickstart template',
|
|
['agents', 0, 'kickstart_template'],
|
|
['agents', 0, 'kickstartTemplate'],
|
|
'same',
|
|
'different',
|
|
],
|
|
] as const)(
|
|
'rejects conflicting %s aliases and accepts identical aliases',
|
|
(_label, snake, camel, same, different) => {
|
|
const base: Record<string, unknown> = {
|
|
version: 1,
|
|
transport: 'tmux',
|
|
tmux: {},
|
|
defaults: {},
|
|
runtimes: { claude: {} },
|
|
agents: [{ name: 'a', runtime: 'claude', class: 'worker' }],
|
|
};
|
|
const assign = (
|
|
root: Record<string, unknown>,
|
|
path: readonly (string | number)[],
|
|
value: unknown,
|
|
) => {
|
|
let cursor: unknown = root;
|
|
for (const segment of path.slice(0, -1)) {
|
|
cursor = (cursor as Record<string | number, unknown>)[segment];
|
|
}
|
|
(cursor as Record<string | number, unknown>)[path.at(-1)!] = value;
|
|
};
|
|
assign(base, snake, same);
|
|
assign(base, camel, different);
|
|
expect(() => parseFleetRosterV1(JSON.stringify(base), 'json')).toThrow(
|
|
/aliases .* conflict/i,
|
|
);
|
|
assign(base, camel, same);
|
|
expect(() => parseFleetRosterV1(JSON.stringify(base), 'json')).not.toThrow();
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('renderPeerReach — exact same-host/cross-host/socket targeting', () => {
|
|
const send = '/home/u/.config/mosaic/tools/tmux/agent-send.sh';
|
|
const base: FleetAgent = {
|
|
name: 'peer',
|
|
runtime: 'claude',
|
|
className: 'worker',
|
|
};
|
|
|
|
it('renders the global named socket and omits -H for a same-host peer', () => {
|
|
expect(renderPeerReach(base, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', send)).toBe(
|
|
`${send} -L mosaic-fleet -s peer -m "…"`,
|
|
);
|
|
});
|
|
|
|
it('uses only the explicit roster ssh target for a cross-host peer', () => {
|
|
const peer: FleetAgent = {
|
|
...base,
|
|
name: 'coder0-0',
|
|
host: '10.1.10.37',
|
|
ssh: 'jwoltje@10.1.10.37',
|
|
};
|
|
expect(renderPeerReach(peer, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', send)).toBe(
|
|
`${send} -L mosaic-fleet -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`,
|
|
);
|
|
});
|
|
|
|
it('fails closed when a cross-host peer has no explicit roster ssh target', () => {
|
|
const peer: FleetAgent = { ...base, name: 'x', host: '10.0.0.9' };
|
|
expect(() => renderPeerReach(peer, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', send)).toThrow(
|
|
/explicit roster ssh target/i,
|
|
);
|
|
});
|
|
|
|
it('renders only the fleet-wide supported socket', () => {
|
|
const peer: FleetAgent = { ...base, socket: 'mosaic-fleet' };
|
|
expect(renderPeerReach(peer, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', send)).toBe(
|
|
`${send} -L mosaic-fleet -s peer -m "…"`,
|
|
);
|
|
});
|
|
|
|
it('resolves hostless peers against the stable fleet-host baseline, not the viewer host', () => {
|
|
const peer: FleetAgent = { ...base, ssh: 'fleet-user@w-jarvis' };
|
|
expect(renderPeerReach(peer, 'remote-host', 'w-jarvis', 'mosaic-fleet', send)).toBe(
|
|
`${send} -L mosaic-fleet -H fleet-user@w-jarvis -s peer -m "…"`,
|
|
);
|
|
});
|
|
|
|
it('shell-quotes an exact helper path that contains spaces', () => {
|
|
expect(
|
|
renderPeerReach(base, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', '/home/test user/send.sh'),
|
|
).toBe(`'/home/test user/send.sh' -L mosaic-fleet -s peer -m "…"`);
|
|
});
|
|
|
|
it('omits -L only for the literal default socket', () => {
|
|
expect(renderPeerReach(base, 'w-jarvis', 'w-jarvis', '', send)).toBe(`${send} -s peer -m "…"`);
|
|
});
|
|
});
|
|
|
|
describe('buildFleetCommsBlock', () => {
|
|
const send = '/h/.config/mosaic/tools/tmux/agent-send.sh';
|
|
|
|
it('renders authoritative identity, exact rows, generation, and no operational metavariables', () => {
|
|
const block = buildFleetCommsBlock({
|
|
selfName: 'enhancer',
|
|
roster: roster(),
|
|
localHost: 'ignored-process-host',
|
|
agentSendPath: send,
|
|
});
|
|
|
|
expect(block).toContain('# Fleet Comms');
|
|
expect(block).toContain('Host: `w-jarvis`');
|
|
expect(block).toContain('Agent/session: `enhancer`');
|
|
expect(block).toContain('tmux socket: `mosaic-fleet`');
|
|
expect(block).toContain(`Helper: \`${send}\``);
|
|
expect(block).toMatch(/Comms generation: `[a-f0-9]{64}`/);
|
|
expect(block).not.toMatch(/\|\s*enhancer\s*\|/);
|
|
expect(block).toContain(`${send} -L mosaic-fleet -s orchestrator -m "…"`);
|
|
expect(block).toContain(`${send} -L mosaic-fleet -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`);
|
|
expect(block).toContain(`mosaic agent comms-block enhancer`);
|
|
expect(block).toMatch(/Never invent, substitute, or fuzzy-match/i);
|
|
expect(block).not.toMatch(
|
|
/<(?:user@host|src_host|src_session|dst_host|dst_session|target-session)>/,
|
|
);
|
|
expect(block).not.toContain('FLIP the preamble');
|
|
});
|
|
|
|
it('changes the generation when a rendered peer role changes', () => {
|
|
const generation = (block: string) => block.match(/Comms generation: `([a-f0-9]{64})`/)?.[1];
|
|
const before = buildFleetCommsBlock({
|
|
selfName: 'enhancer',
|
|
roster: roster(),
|
|
localHost: 'w-jarvis',
|
|
agentSendPath: send,
|
|
});
|
|
const changedRoster = roster(ROSTER.replace('class: implementer', 'class: reviewer'));
|
|
const after = buildFleetCommsBlock({
|
|
selfName: 'enhancer',
|
|
roster: changedRoster,
|
|
localHost: 'w-jarvis',
|
|
agentSendPath: send,
|
|
});
|
|
expect(generation(before)).toMatch(/^[a-f0-9]{64}$/);
|
|
expect(generation(after)).not.toBe(generation(before));
|
|
});
|
|
|
|
it('fails closed when any rendered cross-host row lacks ssh', () => {
|
|
const bad = roster(ROSTER.replace(' ssh: jwoltje@10.1.10.37\n', ''));
|
|
expect(() =>
|
|
buildFleetCommsBlock({
|
|
selfName: 'enhancer',
|
|
roster: bad,
|
|
localHost: 'w-jarvis',
|
|
agentSendPath: send,
|
|
}),
|
|
).toThrow(/explicit roster ssh target/i);
|
|
});
|
|
|
|
it('still renders authoritative local identity when the agent has no peers', () => {
|
|
const solo = roster(
|
|
[
|
|
'version: 1',
|
|
'transport: tmux',
|
|
'agents:',
|
|
' - name: solo',
|
|
' runtime: claude',
|
|
' class: orchestrator',
|
|
].join('\n'),
|
|
);
|
|
const block = buildFleetCommsBlock({
|
|
selfName: 'solo',
|
|
roster: solo,
|
|
localHost: 'h',
|
|
agentSendPath: send,
|
|
});
|
|
expect(block).toContain('Host: `h`');
|
|
expect(block).toContain('Agent/session: `solo`');
|
|
expect(block).toContain('Role/class: `orchestrator`');
|
|
expect(block).toMatch(/Comms generation: `[a-f0-9]{64}`/);
|
|
expect(block).toContain('This roster has no peers');
|
|
expect(block).toContain('## Solo authority boundaries');
|
|
expect(block).toContain('no peer, orchestrator, or remote communication authority');
|
|
expect(block).toContain('Do not send, infer a target, or claim fleet coordination');
|
|
});
|
|
});
|
|
|
|
describe('resolvePeerCommand', () => {
|
|
const send = '/h/.config/mosaic/tools/tmux/agent-send.sh';
|
|
|
|
it('returns one exact known-peer row', () => {
|
|
const result = resolvePeerCommand(roster(), 'enhancer', 'coder0-0', 'w-jarvis', send);
|
|
expect(result.ok).toBe(true);
|
|
expect(result.command).toContain('-H jwoltje@10.1.10.37 -s coder0-0');
|
|
});
|
|
|
|
it('fails closed for an unknown peer with exact-name discovery guidance', () => {
|
|
const result = resolvePeerCommand(roster(), 'enhancer', 'invented-host', 'w-jarvis', send);
|
|
expect(result.ok).toBe(false);
|
|
expect(result.command).toBe('');
|
|
expect(result.error).toContain('invented-host');
|
|
expect(result.error).toContain('orchestrator, coder0, coder0-0');
|
|
expect(result.error).toContain('mosaic agent comms-block enhancer');
|
|
expect(result.error).not.toContain('tmux ls');
|
|
});
|
|
});
|
|
|
|
describe('readFleetCommsBlock — spawned-agent context', () => {
|
|
let home: string;
|
|
beforeEach(() => {
|
|
home = mkdtempSync(join(tmpdir(), 'mosaic-comms-'));
|
|
mkdirSync(join(home, 'fleet'), { recursive: true });
|
|
mkdirSync(join(home, 'tools', 'tmux'), { recursive: true });
|
|
writeFileSync(join(home, 'fleet', 'roster.yaml'), ROSTER);
|
|
const helper = join(home, 'tools', 'tmux', 'agent-send.sh');
|
|
writeFileSync(helper, '#!/bin/sh\n');
|
|
chmodSync(helper, 0o755);
|
|
});
|
|
afterEach(() => rmSync(home, { recursive: true, force: true }));
|
|
|
|
it('uses the authoritative self host and global socket from the shared roster resolver', () => {
|
|
const result = readFleetCommsBlock(home, 'enhancer', 'process-host-must-not-win');
|
|
expect(result.ok).toBe(true);
|
|
expect(result.output).toContain('Host: `w-jarvis`');
|
|
expect(result.output).toContain('tmux socket: `mosaic-fleet`');
|
|
expect(result.output).toContain('-L mosaic-fleet -s orchestrator');
|
|
});
|
|
|
|
it('fails closed for a requested fleet identity that is absent', () => {
|
|
const result = readFleetCommsBlock(home, 'stranger', 'w-jarvis');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.output).toBe('');
|
|
expect(result.error).toContain('Known exact names');
|
|
});
|
|
|
|
it('resolves a supported JSON-only installed roster', () => {
|
|
rmSync(join(home, 'fleet', 'roster.yaml'));
|
|
writeFileSync(
|
|
join(home, 'fleet', 'roster.json'),
|
|
JSON.stringify({
|
|
version: 1,
|
|
transport: 'tmux',
|
|
tmux: { socket_name: 'mosaic-fleet' },
|
|
agents: [
|
|
{ name: 'enhancer', runtime: 'claude', class: 'enhancer', host: 'w-jarvis' },
|
|
{ name: 'orchestrator', runtime: 'claude', class: 'orchestrator', host: 'w-jarvis' },
|
|
],
|
|
}),
|
|
);
|
|
const result = readFleetCommsBlock(home, 'enhancer', 'process-host-must-not-win');
|
|
expect(result.ok).toBe(true);
|
|
expect(result.output).toContain('-L mosaic-fleet -s orchestrator');
|
|
});
|
|
|
|
it('fails closed on a YAML I/O error instead of falling back to JSON', () => {
|
|
rmSync(join(home, 'fleet', 'roster.yaml'));
|
|
mkdirSync(join(home, 'fleet', 'roster.yaml'));
|
|
writeFileSync(
|
|
join(home, 'fleet', 'roster.json'),
|
|
JSON.stringify({
|
|
version: 1,
|
|
transport: 'tmux',
|
|
agents: [{ name: 'enhancer', runtime: 'claude', class: 'enhancer' }],
|
|
}),
|
|
);
|
|
const result = readFleetCommsBlock(home, 'enhancer', 'w-jarvis');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain('invalid fleet roster at');
|
|
expect(result.error).toContain('roster.yaml');
|
|
});
|
|
|
|
it.each([
|
|
['missing', () => rmSync(join(home, 'tools', 'tmux', 'agent-send.sh'))],
|
|
[
|
|
'directory',
|
|
() => {
|
|
rmSync(join(home, 'tools', 'tmux', 'agent-send.sh'));
|
|
mkdirSync(join(home, 'tools', 'tmux', 'agent-send.sh'));
|
|
},
|
|
],
|
|
[
|
|
'symlink',
|
|
() => {
|
|
const helper = join(home, 'tools', 'tmux', 'agent-send.sh');
|
|
rmSync(helper);
|
|
writeFileSync(join(home, 'real-send.sh'), '#!/bin/sh\n');
|
|
symlinkSync(join(home, 'real-send.sh'), helper);
|
|
},
|
|
],
|
|
['non-executable', () => chmodSync(join(home, 'tools', 'tmux', 'agent-send.sh'), 0o644)],
|
|
])('fails closed for a %s helper with deterministic repair guidance', (_case, mutate) => {
|
|
mutate();
|
|
const result = readFleetCommsBlock(home, 'enhancer', 'w-jarvis');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.output).toBe('');
|
|
expect(result.error).toContain('mosaic update --repair-tools');
|
|
expect(result.error).toContain('no active context or session was rewritten');
|
|
});
|
|
|
|
it('does not rewrite the roster while resolving context', () => {
|
|
const path = join(home, 'fleet', 'roster.yaml');
|
|
const before = readFileSync(path, 'utf8');
|
|
readFleetCommsBlock(home, 'enhancer', 'w-jarvis');
|
|
expect(readFileSync(path, 'utf8')).toBe(before);
|
|
});
|
|
});
|
|
|
|
describe('renderToolsContractStatus — non-mutating install drift', () => {
|
|
let home: string;
|
|
|
|
beforeEach(() => {
|
|
home = mkdtempSync(join(tmpdir(), 'mosaic-tools-status-'));
|
|
mkdirSync(join(home, 'defaults'), { recursive: true });
|
|
writeFileSync(
|
|
join(home, 'defaults', 'TOOLS.md'),
|
|
'# authoritative tools\n<!-- fleet-comms-contract: 1 -->\n',
|
|
);
|
|
});
|
|
afterEach(() => rmSync(home, { recursive: true, force: true }));
|
|
|
|
it('uses the supported repair command when installed TOOLS.md is missing', () => {
|
|
const status = renderToolsContractStatus(home);
|
|
expect(status).toContain('mosaic update --repair-tools');
|
|
expect(status).not.toContain('--reseed');
|
|
expect(status).toContain('authorized operator');
|
|
});
|
|
|
|
it('reports stale preserved content without rewriting it', () => {
|
|
const path = join(home, 'TOOLS.md');
|
|
const stale = '# customized tools\n';
|
|
writeFileSync(path, stale);
|
|
const status = renderToolsContractStatus(home);
|
|
expect(status).toContain('fleet-comms-contract: 1');
|
|
expect(status).toContain('digest-qualified backup');
|
|
expect(status).toContain('mosaic update --repair-tools');
|
|
expect(status).toContain('active context was not rewritten');
|
|
expect(readFileSync(path, 'utf8')).toBe(stale);
|
|
});
|
|
|
|
it('does not accept marker-only customized content as current', () => {
|
|
const path = join(home, 'TOOLS.md');
|
|
writeFileSync(path, '<!-- fleet-comms-contract: 1 -->\ncorrupt\n');
|
|
expect(renderToolsContractStatus(home)).toContain('does not byte-match');
|
|
});
|
|
|
|
it('rejects markerless byte-equal source and installed content', () => {
|
|
const content = '# markerless but equal\n';
|
|
writeFileSync(join(home, 'defaults', 'TOOLS.md'), content);
|
|
writeFileSync(join(home, 'TOOLS.md'), content);
|
|
const status = renderToolsContractStatus(home);
|
|
expect(status).toContain('source contract');
|
|
expect(status).toContain('does not declare the expected');
|
|
});
|
|
|
|
it.each([
|
|
['source', join('defaults', 'TOOLS.md')],
|
|
['installed', 'TOOLS.md'],
|
|
])('rejects a wrong contract version in %s content', (_case, relativePath) => {
|
|
const current = '# authoritative tools\n<!-- fleet-comms-contract: 1 -->\n';
|
|
writeFileSync(join(home, 'TOOLS.md'), current);
|
|
writeFileSync(join(home, 'defaults', 'TOOLS.md'), current);
|
|
writeFileSync(join(home, relativePath), current.replace('contract: 1', 'contract: 2'));
|
|
expect(renderToolsContractStatus(home)).not.toBe('');
|
|
});
|
|
|
|
it('treats installed TOOLS.md symlinks as stale without following or rewriting them', () => {
|
|
const external = join(home, 'external-tools.md');
|
|
const externalContent = '# external\n<!-- fleet-comms-contract: 1 -->\n';
|
|
writeFileSync(external, externalContent);
|
|
symlinkSync(external, join(home, 'TOOLS.md'));
|
|
|
|
const status = renderToolsContractStatus(home);
|
|
|
|
expect(status).toContain('unavailable');
|
|
expect(status).toContain('mosaic update --repair-tools');
|
|
expect(readFileSync(external, 'utf8')).toBe(externalContent);
|
|
});
|
|
|
|
it('treats source TOOLS.md symlinks as unavailable without following them', () => {
|
|
const external = join(home, 'external-source.md');
|
|
const content = '# authoritative tools\n<!-- fleet-comms-contract: 1 -->\n';
|
|
writeFileSync(external, content);
|
|
rmSync(join(home, 'defaults', 'TOOLS.md'));
|
|
symlinkSync(external, join(home, 'defaults', 'TOOLS.md'));
|
|
writeFileSync(join(home, 'TOOLS.md'), content);
|
|
|
|
const status = renderToolsContractStatus(home);
|
|
|
|
expect(status).toContain('source contract');
|
|
expect(status).toContain('unavailable');
|
|
expect(readFileSync(external, 'utf8')).toBe(content);
|
|
});
|
|
|
|
it('accepts byte-equal bounded source and installed contracts', () => {
|
|
const source = readFileSync(join(home, 'defaults', 'TOOLS.md'), 'utf8');
|
|
writeFileSync(join(home, 'TOOLS.md'), source);
|
|
expect(renderToolsContractStatus(home)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('resolveCommsBlock — mosaic agent comms-block', () => {
|
|
let home: string;
|
|
beforeEach(() => {
|
|
home = mkdtempSync(join(tmpdir(), 'mosaic-commsblk-'));
|
|
mkdirSync(join(home, 'fleet'), { recursive: true });
|
|
mkdirSync(join(home, 'tools', 'tmux'), { recursive: true });
|
|
writeFileSync(join(home, 'fleet', 'roster.yaml'), ROSTER);
|
|
const helper = join(home, 'tools', 'tmux', 'agent-send.sh');
|
|
writeFileSync(helper, '#!/bin/sh\n');
|
|
chmodSync(helper, 0o755);
|
|
});
|
|
afterEach(() => rmSync(home, { recursive: true, force: true }));
|
|
|
|
it('returns the exact contract for a roster member', () => {
|
|
const result = resolveCommsBlock(home, 'enhancer');
|
|
expect(result.ok).toBe(true);
|
|
expect(result.output).toContain('Host: `w-jarvis`');
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('fails loud and lists known exact names for a non-member', () => {
|
|
const result = resolveCommsBlock(home, 'stranger');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.output).toBe('');
|
|
expect(result.error).toContain('stranger');
|
|
expect(result.error).toContain('orchestrator');
|
|
expect(result.error).toContain('enhancer');
|
|
});
|
|
|
|
it('fails loud when no roster exists', () => {
|
|
const noRoster = mkdtempSync(join(tmpdir(), 'mosaic-noroster-'));
|
|
mkdirSync(join(noRoster, 'tools', 'tmux'), { recursive: true });
|
|
const helper = join(noRoster, 'tools', 'tmux', 'agent-send.sh');
|
|
writeFileSync(helper, '#!/bin/sh\n');
|
|
chmodSync(helper, 0o755);
|
|
const result = resolveCommsBlock(noRoster, 'orchestrator');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain('no fleet roster');
|
|
rmSync(noRoster, { recursive: true, force: true });
|
|
});
|
|
|
|
it('fails loud for a missing role argument', () => {
|
|
const result = resolveCommsBlock(home, undefined);
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain('requires');
|
|
});
|
|
});
|