feat(fleet): add roster v2 structural compiler (#764)
This commit was merged in pull request #764.
This commit is contained in:
242
packages/mosaic/src/fleet/roster-v2.spec.ts
Normal file
242
packages/mosaic/src/fleet/roster-v2.spec.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
ROSTER_V2_JSON_SCHEMA,
|
||||
RosterV2ValidationError,
|
||||
parseRosterV2,
|
||||
renderRosterV2Yaml,
|
||||
} from './roster-v2.js';
|
||||
|
||||
const validRoster = `
|
||||
version: 2
|
||||
generation: 7
|
||||
transport: tmux
|
||||
tmux:
|
||||
socket_name: mosaic-fleet
|
||||
holder_session: _holder
|
||||
defaults:
|
||||
working_directory: ~/src
|
||||
runtime: pi
|
||||
runtimes:
|
||||
pi:
|
||||
reset_command: /new
|
||||
agents:
|
||||
- name: coder0
|
||||
alias: Coder 0
|
||||
class: code
|
||||
runtime: pi
|
||||
provider: openai
|
||||
model: gpt-5.6-sol
|
||||
reasoning: high
|
||||
tool_policy: code
|
||||
working_directory: ~/src
|
||||
persistent_persona: false
|
||||
reset_between_tasks: true
|
||||
lifecycle:
|
||||
enabled: true
|
||||
desired_state: stopped
|
||||
launch:
|
||||
yolo: true
|
||||
`;
|
||||
|
||||
describe('roster v2 structural compiler', (): void => {
|
||||
it('parses YAML into a normalized typed model and renders canonical YAML', (): void => {
|
||||
const roster = parseRosterV2(validRoster, 'yaml');
|
||||
|
||||
expect(roster).toEqual({
|
||||
version: 2,
|
||||
generation: 7,
|
||||
transport: 'tmux',
|
||||
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' },
|
||||
defaults: { workingDirectory: '~/src', runtime: 'pi' },
|
||||
runtimes: { pi: { resetCommand: '/new' } },
|
||||
agents: [
|
||||
{
|
||||
name: 'coder0',
|
||||
alias: 'Coder 0',
|
||||
className: 'code',
|
||||
runtime: 'pi',
|
||||
provider: 'openai',
|
||||
model: 'gpt-5.6-sol',
|
||||
reasoning: 'high',
|
||||
toolPolicy: 'code',
|
||||
workingDirectory: '~/src',
|
||||
persistentPersona: false,
|
||||
resetBetweenTasks: true,
|
||||
lifecycle: { enabled: true, desiredState: 'stopped' },
|
||||
launch: { yolo: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(renderRosterV2Yaml(roster)).toBe(validRoster.trimStart());
|
||||
});
|
||||
|
||||
it('parses JSON and produces the same normalized model', (): void => {
|
||||
const yaml = parseRosterV2(validRoster, 'yaml');
|
||||
const json = JSON.stringify({
|
||||
version: 2,
|
||||
generation: 7,
|
||||
transport: 'tmux',
|
||||
tmux: { socket_name: 'mosaic-fleet', holder_session: '_holder' },
|
||||
defaults: { working_directory: '~/src', runtime: 'pi' },
|
||||
runtimes: { pi: { reset_command: '/new' } },
|
||||
agents: [
|
||||
{
|
||||
name: 'coder0',
|
||||
alias: 'Coder 0',
|
||||
class: 'code',
|
||||
runtime: 'pi',
|
||||
provider: 'openai',
|
||||
model: 'gpt-5.6-sol',
|
||||
reasoning: 'high',
|
||||
tool_policy: 'code',
|
||||
working_directory: '~/src',
|
||||
persistent_persona: false,
|
||||
reset_between_tasks: true,
|
||||
lifecycle: { enabled: true, desired_state: 'stopped' },
|
||||
launch: { yolo: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(parseRosterV2(json, 'json')).toEqual(yaml);
|
||||
});
|
||||
|
||||
it('sorts runtime and agent maps in the deterministic renderer', (): void => {
|
||||
const roster = parseRosterV2(
|
||||
validRoster
|
||||
.replace(
|
||||
'runtimes:\n pi:\n reset_command: /new',
|
||||
'runtimes:\n pi:\n reset_command: /new\n codex:\n reset_command: /clear',
|
||||
)
|
||||
.replace(
|
||||
'agents:\n - name: coder0',
|
||||
'agents:\n - name: reviewer\n alias: Reviewer\n class: review\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: medium\n tool_policy: review\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true\n - name: coder0',
|
||||
),
|
||||
'yaml',
|
||||
);
|
||||
|
||||
const rendered = renderRosterV2Yaml(roster);
|
||||
expect(rendered.indexOf(' codex:')).toBeLessThan(rendered.indexOf(' pi:'));
|
||||
expect(rendered.indexOf(' - name: coder0')).toBeLessThan(
|
||||
rendered.indexOf(' - name: reviewer'),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['v1 document', validRoster.replace('version: 2', 'version: 1'), /v1.*existing v1 path/i],
|
||||
[
|
||||
'unknown connector',
|
||||
`${validRoster}\nconnector:\n kind: matrix\n`,
|
||||
/unsupported field.*connector/i,
|
||||
],
|
||||
[
|
||||
'remote host',
|
||||
validRoster.replace(
|
||||
'runtime: pi\n provider',
|
||||
'runtime: pi\n host: remote\n provider',
|
||||
),
|
||||
/unsupported field.*host/i,
|
||||
],
|
||||
[
|
||||
'secret reference',
|
||||
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n secret_ref: vault://x'),
|
||||
/unsupported field.*secret_ref/i,
|
||||
],
|
||||
[
|
||||
'channel override',
|
||||
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n channels: discord'),
|
||||
/unsupported field.*channels/i,
|
||||
],
|
||||
[
|
||||
'arbitrary command',
|
||||
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n command: whoami'),
|
||||
/unsupported field.*command/i,
|
||||
],
|
||||
[
|
||||
'gateway field',
|
||||
`${validRoster}\ngateway:\n url: https://gateway.example\n`,
|
||||
/unsupported field.*gateway/i,
|
||||
],
|
||||
[
|
||||
'missing required field',
|
||||
validRoster.replace(' model: gpt-5.6-sol\n', ''),
|
||||
/model.*required/i,
|
||||
],
|
||||
[
|
||||
'invalid type',
|
||||
validRoster.replace('generation: 7', 'generation: seven'),
|
||||
/generation.*integer/i,
|
||||
],
|
||||
[
|
||||
'unsafe generation',
|
||||
validRoster.replace('generation: 7', 'generation: 9007199254740992'),
|
||||
/generation.*integer/i,
|
||||
],
|
||||
[
|
||||
'duplicate names',
|
||||
validRoster.replace(
|
||||
' - name: coder0',
|
||||
' - name: coder0\n alias: Duplicate\n class: code\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: high\n tool_policy: code\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true\n - name: coder0',
|
||||
),
|
||||
/duplicate agent name/i,
|
||||
],
|
||||
['invalid name', validRoster.replace('name: coder0', 'name: ../coder0'), /invalid agent name/i],
|
||||
[
|
||||
'invalid transport',
|
||||
validRoster.replace('transport: tmux', 'transport: matrix'),
|
||||
/transport.*tmux/i,
|
||||
],
|
||||
[
|
||||
'invalid runtime',
|
||||
validRoster.replace('runtime: pi', 'runtime: matrix'),
|
||||
/runtime.*supported/i,
|
||||
],
|
||||
[
|
||||
'invalid reasoning',
|
||||
validRoster.replace('reasoning: high', 'reasoning: extreme'),
|
||||
/reasoning.*low.*medium.*high/i,
|
||||
],
|
||||
[
|
||||
'ambiguous socket',
|
||||
validRoster.replace('socket_name: mosaic-fleet', 'socket_name: default/socket'),
|
||||
/socket_name/i,
|
||||
],
|
||||
[
|
||||
'agent socket override',
|
||||
validRoster.replace(
|
||||
'runtime: pi\n provider',
|
||||
'runtime: pi\n socket: another\n provider',
|
||||
),
|
||||
/unsupported field.*socket/i,
|
||||
],
|
||||
])('rejects %s', (_name: string, source: string, expected: RegExp): void => {
|
||||
expect((): void => {
|
||||
parseRosterV2(source, 'yaml');
|
||||
}).toThrow(expected);
|
||||
});
|
||||
|
||||
it('rejects malformed JSON as a validation error', (): void => {
|
||||
expect((): void => {
|
||||
parseRosterV2('{', 'json');
|
||||
}).toThrow(RosterV2ValidationError);
|
||||
});
|
||||
|
||||
it('declares supported runtime map keys in the executable schema', (): void => {
|
||||
expect(ROSTER_V2_JSON_SCHEMA).toMatchObject({
|
||||
properties: {
|
||||
runtimes: { propertyNames: { enum: ['claude', 'codex', 'opencode', 'pi'] } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the checked-in documentation schema structurally identical to the executable schema', async (): Promise<void> => {
|
||||
const schemaPath = fileURLToPath(
|
||||
new URL('../../../../docs/fleet/reference/roster-v2.schema.json', import.meta.url),
|
||||
);
|
||||
const documented = await readFile(schemaPath, 'utf8');
|
||||
|
||||
expect(JSON.parse(documented) as unknown).toEqual(ROSTER_V2_JSON_SCHEMA);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user