63 lines
3.6 KiB
JavaScript
63 lines
3.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync, mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { buildBundle } from '../src/bundle.mjs';
|
|
import { typedTools } from '../src/tools.mjs';
|
|
import { resolvedFor, scratch, HARNESS, REPO_SKILLS } from './helpers.mjs';
|
|
import { ownRails, railsText, decisionAutonomy } from '../../organization/src/rails.mjs';
|
|
|
|
const SECRET = 'sk-test-NOT-A-SECRET-1234';
|
|
test('coder prompt lists every held action with its route and only service/scope names', t => {
|
|
const resolved = resolvedFor(t, 'coder');
|
|
const root = scratch(t), workspace = join(root, 'ws'); mkdirSync(workspace);
|
|
const bundle = buildBundle({ dir: join(root, 'bundle'), resolved,
|
|
contract: '# Contract', harness: HARNESS.pi, skillsDir: REPO_SKILLS,
|
|
session: { run: 'r1', workspace, toolSocket: join(root, 'tool.sock'), turnMarker: join(root, 'turn.done') } });
|
|
const prompt = readFileSync(bundle.files.prompt, 'utf8');
|
|
assert.ok(prompt.includes(railsText(resolved.rails)));
|
|
for (const action of resolved.limits.authority.withinRole) assert.ok(prompt.includes(`- ${action}: alone.`));
|
|
assert.match(prompt, /task.scope.change: delivery \(default\): arbiter instance "pm"; technical: arbiter instance "cto"/);
|
|
assert.match(prompt, /Network: none/);
|
|
assert.match(prompt, /Credential service gitea; scopes:.*write:repository/);
|
|
assert.ok(!railsText(resolved.rails).includes(resolved.credentials.gitea.file));
|
|
assert.ok(!railsText(resolved.rails).includes(SECRET));
|
|
assert.match(prompt, /a refusal carries its code, the rail it enforces and the next step/);
|
|
});
|
|
|
|
test('all roles including creative roles receive a parameterless caller-only my_rails tool', t => {
|
|
for (const instance of ['coder', 'chief', 'reviewer']) {
|
|
const tool = typedTools({ resolved: resolvedFor(t, instance) }).find(t => t.name === 'my_rails');
|
|
assert.equal(tool.verb, 'my_rails');
|
|
assert.deepEqual(tool.parameters, { type: 'object', properties: {}, required: [], additionalProperties: false });
|
|
}
|
|
const creative = typedTools({ resolved: { ...resolvedFor(t, 'coder'), definition: 'companion' } });
|
|
assert.ok(creative.some(t => t.name === 'my_rails'));
|
|
});
|
|
|
|
test('resolve rights name their classes and fixed levels through the AU extension point', t => {
|
|
const { rails } = resolvedFor(t, 'cto');
|
|
assert.deepEqual(rails.resolve, [{ right: 'decision.resolve.technical', classes: [
|
|
{ name: 'cross-role', level: decisionAutonomy('cross-role') }, { name: 'merge', level: decisionAutonomy('merge') },
|
|
] }]);
|
|
assert.match(railsText(rails), /merge: out of the loop/);
|
|
});
|
|
|
|
test('fixed domains, arbiter self-raises and hard waits describe existing routes only', () => {
|
|
const rails = ownRails({ instance: 'cto', authority: { withinRole: ['deploy'],
|
|
crossRole: ['git.merge.protected', 'policy.change'] }, arbiters: { technical: 'cto', delivery: 'pm' } });
|
|
assert.deepEqual(rails.actions, [
|
|
{ action: 'deploy', routes: [{ to: 'human' }] },
|
|
{ action: 'git.merge.protected', routes: [{ domain: 'technical', to: 'human' }] },
|
|
{ action: 'policy.change', routes: [{ domain: 'delivery', to: 'pm' }] },
|
|
]);
|
|
});
|
|
|
|
test('hire and fire are unavailable to a chief that is not the configured launcher', t => {
|
|
const { rails } = resolvedFor(t, 'chief', doc => { delete doc.launch; });
|
|
for (const action of ['role.hire', 'role.fire']) {
|
|
assert.deepEqual(rails.actions.find(a => a.action === action).routes, [{ to: 'unavailable' }]);
|
|
}
|
|
assert.match(railsText(rails), /role.hire: unavailable here; only the configured chief can hire or fire/);
|
|
});
|