Files
stack/packages/harness/tests/rails.test.mjs

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/);
});