feat(roles): coder drops the task.reassign grant the broker always refuses (#1567, row 73)
roles/coder.json crossRole goes from ["task.reassign", "task.scope.change"] to ["task.scope.change"]. The broker refuses a non-pm task.reassign at the writer check before it looks at authority, so the grant could never be used. A coder's task.reassign now classifies as gated and goes to the human. Backed by lead decision 90. Tests: business role and resolve (the narrowing test moves to the pm, which still holds two cross-role actions), a bus test on the shipped role files, a harness test that a coder with a tracker gets no task_reassign, the tasks fixture, and a verbs line showing a coder's task.reassign refuses field-writer with no action.allowed. mutants.sh is Rocko's packet rerun script, already committed. Author: Rocko (packet agents/rocko/work/queue-73/, commit2137dd7f). Candidate manifest sha256 bf4e5ed6, base324c064b, no drift on the candidate paths through8a42e819. Approval: Dewey, round 1, comment 27506 on #1567. Gate (Sage, /tmp/sage-g73 on8a42e819, 23:42-23:48Z): tasks 71, bus 81, business 60, harness 59, cli 113, webui 61, seat 27 node tests, 0 failed; every scripts/test-*.sh exit 0, 0 failed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -4,6 +4,9 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { busBusiness } from '../src/business.mjs';
|
||||
import { resolveInstance, validateBusinessDocument } from '../../business/src/index.mjs';
|
||||
import { businessDoc, REPO_ROLES, systemFor } from '../../business/tests/helpers.mjs';
|
||||
const { Broker } = await import('../src/broker.mjs').catch((e) => {
|
||||
if (e.code === 'ERR_MODULE_NOT_FOUND') return {};
|
||||
throw e;
|
||||
@@ -354,3 +357,26 @@ test('both arbiters require human resolution when their cross-role route is them
|
||||
assert.equal(b.authorize(cap, action, { decision: d.id, target: 'vikunja:3/41' }).class, 'cross-role');
|
||||
}
|
||||
});
|
||||
|
||||
test('the shipped coder role raises task.reassign as gated to the human; task.scope.change stays cross-role', (t) => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'bus-roles-'));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const business = validateBusinessDocument(businessDoc(root, 'demo'), join(root, 'businesses', 'demo.json'), {
|
||||
rolesDir: REPO_ROLES,
|
||||
});
|
||||
const resolved = {};
|
||||
for (const instance of Object.keys(business.roles))
|
||||
resolved[instance] = resolveInstance({ system: systemFor(root), business, instance });
|
||||
const { b, agent, human } = setup(t, { demo: busBusiness(business, resolved) });
|
||||
const c = agent('coder');
|
||||
call(b, c, 'role.claim');
|
||||
const reassign = raise(b, c, 'task.reassign', { target: 'vikunja:3/7' });
|
||||
assert.equal(reassign.class, 'gated');
|
||||
assert.equal(reassign.route_to, 'human');
|
||||
const scope = raise(b, c, 'task.scope.change', { domain: 'technical', target: 'vikunja:3/7' });
|
||||
assert.equal(scope.class, 'cross-role');
|
||||
assert.equal(scope.route_to, 'cto');
|
||||
// Approval passes the broker; the task verb still refuses a coder with field-writer (packages/tasks).
|
||||
call(b, human, 'decision.resolve', { id: reassign.id, choice: 'yes' });
|
||||
assert.equal(b.authorize(c, 'task.reassign', { decision: reassign.id, target: 'vikunja:3/7' }).class, 'gated');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user