Files
stack/packages/bus/tests/request-workspace.test.mjs

87 lines
7.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { requestWorkspace } from '../src/request-workspace.mjs';
import { requestFixture } from './request-fixture.mjs';
function fixture(t) {
const dataRoot = mkdtempSync(join(tmpdir(), 'workspace-request-')); t.after(() => rmSync(dataRoot, { recursive: true, force: true }));
const checkout = join(dataRoot, 'workspaces', 'demo', 'builder', 'timer'); mkdirSync(checkout, { recursive: true }); writeFileSync(join(checkout, 'draft.txt'), 'not read');
const state = { business: 'demo', request: 'req1', pm: 'pm', project: 'timer', snapshot: 'snap1', tasks: ['vikunja:2/41'], plan_digest: 'plan', selection: { bindings: [{ digest: 'plan', plan: { business: 'demo', request: 'req1', project: 'timer', staff: { pm: 'pm', coder: 'builder' } } }] } };
const events = [
{ kind: 'task.created', actor_role: 'pm', subject: state.tasks[0], body: { request: 'req1', project_key: 'timer', requirement_snapshot: 'snap1' } },
{ kind: 'task.assigned', actor_role: 'pm', subject: state.tasks[0], body: { role: 'builder' } },
{ kind: 'session.launched', actor_role: 'builder', subject: 'run1', body: {} },
];
const dir = join(dataRoot, 'launches', 'demo', 'run1', 'bundle'); mkdirSync(dir, { recursive: true });
const proof = (outcome = 'on branch main at commit abc when this session started') => {
const prompt = `- Project clones: timer at ${checkout}, ${outcome}.\n`;
writeFileSync(join(dir, 'prompt.md'), prompt);
writeFileSync(join(dir, 'manifest.json'), JSON.stringify({ business: 'demo', instance: 'builder', run: 'run1', projects: ['timer'], files: { 'prompt.md': createHash('sha256').update(prompt).digest('hex') } }));
};
proof(); return { dataRoot, checkout, state, events, proof, dir };
}
test('resolves only a confirmed request and host-proven coder checkout', t => {
const f = fixture(t), read = () => requestWorkspace(f);
assert.deepEqual(read().entries, [{ name: 'draft.txt' }]);
f.state.request = 'other'; assert.equal(read().available, false); f.state.request = '../req1'; assert.equal(read().available, false); f.state.request = 'req1';
f.events[1].body.role = 'other'; assert.equal(read().available, false); f.events[1].body.role = 'builder';
f.proof('not updated at this launch (not-a-clone)'); assert.equal(read().available, false);
f.proof(); writeFileSync(join(f.dir, 'prompt.md'), 'tampered'); assert.equal(read().available, false);
f.events.pop(); assert.equal(read().available, false);
});
test('console capability is required before the workspace read and other requests are refused', async t => {
const f = await requestFixture(t);
const args = { request: f.request };
const result = await f.call('console', 'request.workspace.console', args);
assert.equal(result.available, false);
for (const principal of ['human', 'pm', 'cto', 'coder', 'reader'])
await assert.rejects(async () => f.call(principal, 'request.workspace.console', args), /human-required|read-only|tester-refused/);
await assert.rejects(async () => f.call('console', 'request.workspace.console', { request: 'someone-else' }), /request-not-found/);
await assert.rejects(async () => f.call('console', 'request.workspace.console', { request: '../other' }), /invalid-request|request-not-found/);
await assert.rejects(async () => f.call('console', 'request.workspace.console', { ...args, path: '/etc' }), /invalid-request/);
assert.throws(() => f.bus.broker.bindConsole({ business: 'demo', human: 'other', subject: { provider: 'test', iss: 'test', sub: 'other' } }), /human-required/);
});
test('real broker derives a workspace only after request and coder assignment evidence', async t => {
const f = await requestFixture(t), sub = await f.submit(); f.approve(sub); await f.activate(sub);
const { createDigest, createMarker } = await import('../src/broker.mjs');
const state = f.journal.readRequest('demo', f.request), task = 'vikunja:2/41';
const prepared = { op: 'timer-task', request: f.request, requirement: 'REQ-POMO-1', project_key: 'timer', requirement_snapshot: state.snapshot,
project: 2, creator: 3, decision: null, view: { ids: { todo: 1, review: 2, done: 3 } }, labels: [], relations: [] };
prepared.body = { title: 'Timer', description: createMarker(prepared) }; prepared.body_digest = createDigest(prepared.body);
const { receipt } = f.bus.broker.prepareTaskCreate(f.caps.pm, prepared);
let fields = { ...prepared.body, project_id: 2, bucket: 1, done: false, assignees: [], due_date: null, priority: 0 };
const snapshot = () => ({ task_ref: task, fields, digest: createDigest(fields), updated: new Date().toISOString() });
f.bus.broker.recordRecoveredCreate({ receipt, task_ref: task, snapshot: { ...snapshot(), creator: 3 } });
fields = { ...fields, assignees: [5] };
f.bus.broker.recordTask({ cap: f.caps.pm, business: 'demo', snapshots: [snapshot()], events: [{ kind: 'task.assigned', subject: task, body: { role: 'coder', previous: [] } }] });
await f.call('pm', 'request.advance', { request: f.request, revision: state.revision, op: 'start' });
const checkout = join(f.f.dataRoot, 'workspaces', 'demo', 'coder', 'timer'); mkdirSync(checkout, { recursive: true }); writeFileSync(join(checkout, 'draft.js'), 'no contents read');
const bundle = join(f.f.dataRoot, 'launches', 'demo', 'coder-run', 'bundle'); mkdirSync(bundle, { recursive: true });
const prompt = `- Project clones: timer at ${checkout}, on branch main at commit abc when this session started.\n`;
writeFileSync(join(bundle, 'prompt.md'), prompt);
writeFileSync(join(bundle, 'manifest.json'), JSON.stringify({ business: 'demo', instance: 'coder', run: 'coder-run', projects: ['timer'], files: { 'prompt.md': createHash('sha256').update(prompt).digest('hex') } }));
const result = await f.call('console', 'request.workspace.console', { request: f.request });
assert.equal(result.available, true); assert.deepEqual(result.entries, [{ name: 'draft.js' }]);
});
test('a different request owner is refused before any workspace access', async t => {
const { Store } = await import('../src/store.mjs'); const { Broker } = await import('../src/broker.mjs');
const root = mkdtempSync(join(tmpdir(), 'workspace-owner-')), store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const roles = { pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } } };
let reads = 0;
const state = { business: 'demo', request: 'r1', owner: 'someone-else', pm: 'pm', revision: 1 };
const lifecycle = { get: async () => state };
for (const key of ['open', 'projects', 'propose', 'question', 'answer', 'attach', 'cancel', 'modeConsole', 'draftConsole', 'resume', 'recover']) lifecycle[key] = () => {};
const broker = new Broker({ store, businesses: { demo: { id: 'demo', human: 'jason', roles, arbiters: { technical: 'pm', delivery: 'pm' }, projects: {} } }, requestLifecycle: () => lifecycle, workspaceRead: () => { reads++; return {}; } });
const cap = broker.bindConsole({ business: 'demo', human: 'jason', subject: { provider: 'test', iss: 'test', sub: 'owner' } });
await assert.rejects(async () => broker.request(cap, { verb: 'request.workspace.console', args: { request: 'r1' } }), /request-access-refused/);
assert.equal(reads, 0);
});