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