|
|
|
|
@@ -246,6 +246,8 @@ describe('fleet roster parsing', () => {
|
|
|
|
|
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe(
|
|
|
|
|
[
|
|
|
|
|
'MOSAIC_AGENT_NAME=coder0',
|
|
|
|
|
// Reflects the roster's non-default `class: implementer` (A3a).
|
|
|
|
|
'MOSAIC_AGENT_CLASS=implementer',
|
|
|
|
|
'MOSAIC_AGENT_RUNTIME=codex',
|
|
|
|
|
'MOSAIC_AGENT_MODEL=',
|
|
|
|
|
'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
|
|
|
|
|
@@ -255,6 +257,40 @@ describe('fleet roster parsing', () => {
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('emits MOSAIC_AGENT_CLASS=worker for an agent that declares no class', async () => {
|
|
|
|
|
cleanup = await tempDir();
|
|
|
|
|
const rosterPath = join(cleanup, 'roster.json');
|
|
|
|
|
await writeFile(
|
|
|
|
|
rosterPath,
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
version: 1,
|
|
|
|
|
transport: 'tmux',
|
|
|
|
|
agents: [{ name: 'coder0', runtime: 'codex' }],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
const roster = await loadFleetRoster(rosterPath);
|
|
|
|
|
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toContain(
|
|
|
|
|
'MOSAIC_AGENT_CLASS=worker\n',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shell-escapes MOSAIC_AGENT_CLASS so a launcher reads it verbatim', async () => {
|
|
|
|
|
cleanup = await tempDir();
|
|
|
|
|
const rosterPath = join(cleanup, 'roster.json');
|
|
|
|
|
await writeFile(
|
|
|
|
|
rosterPath,
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
version: 1,
|
|
|
|
|
transport: 'tmux',
|
|
|
|
|
agents: [{ name: 'coder0', runtime: 'codex', class: 'orchestrator' }],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
const roster = await loadFleetRoster(rosterPath);
|
|
|
|
|
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toContain(
|
|
|
|
|
'MOSAIC_AGENT_CLASS=orchestrator\n',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('preserves site-owned agent EnvironmentFile overrides while refreshing roster keys', () => {
|
|
|
|
|
const generated = [
|
|
|
|
|
'MOSAIC_AGENT_NAME=coder0',
|
|
|
|
|
@@ -286,6 +322,28 @@ describe('fleet roster parsing', () => {
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('updates (does not duplicate) MOSAIC_AGENT_CLASS on re-launch', () => {
|
|
|
|
|
const generated = [
|
|
|
|
|
'MOSAIC_AGENT_NAME=coder0',
|
|
|
|
|
'MOSAIC_AGENT_CLASS=orchestrator',
|
|
|
|
|
'MOSAIC_AGENT_RUNTIME=codex',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n');
|
|
|
|
|
const existing = [
|
|
|
|
|
'MOSAIC_AGENT_NAME=coder0',
|
|
|
|
|
'MOSAIC_AGENT_CLASS=worker',
|
|
|
|
|
'MOSAIC_AGENT_RUNTIME=codex',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n');
|
|
|
|
|
|
|
|
|
|
const merged = mergeAgentEnv(generated, existing);
|
|
|
|
|
// mergeAgentEnv keys by VAR name, so the regenerated CLASS wins and there is
|
|
|
|
|
// exactly one MOSAIC_AGENT_CLASS line (no stale worker value left behind).
|
|
|
|
|
expect(merged).toContain('MOSAIC_AGENT_CLASS=orchestrator');
|
|
|
|
|
expect(merged).not.toContain('MOSAIC_AGENT_CLASS=worker');
|
|
|
|
|
expect(merged.match(/^MOSAIC_AGENT_CLASS=/gm)).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects unknown roster fields instead of silently defaulting', async () => {
|
|
|
|
|
cleanup = await tempDir();
|
|
|
|
|
const rosterPath = join(cleanup, 'roster.yaml');
|
|
|
|
|
|