@bongos/core 1.25.14 → 1.25.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +71 -26
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +20 -0
- package/clients/bongos-client/index.cjs +20 -0
- package/clients/bongos-client/index.d.ts +24 -0
- package/clients/bongos-client/index.mjs +20 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +60 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +544 -2
- package/docs/api-reference.md +12 -2
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/kickoff-checklist.js +1 -1
- package/modules/onboarding/genesis-work.js +364 -0
- package/modules/onboarding/migrations/onboarding_002_genesis_work.sql +76 -0
- package/modules/onboarding/routes/genesis-work.js +85 -0
- package/modules/onboarding/routes/onboarding.js +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/bongos/genesis.js +178 -0
- package/scripts/bongos/run-unit-tests.js +3 -0
- package/scripts/gds/genesis.js +7 -0
- package/src/module-api.js +1 -1
- package/tests/genesis_work.mjs +255 -0
- package/tests/genesis_work_db.mjs +73 -0
- package/tests/genesis_work_routes.mjs +130 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// tests/genesis_work_routes.mjs — the genesis stage-work HTTP surface (task 1005116)
|
|
2
|
+
//
|
|
3
|
+
// Drives the REAL routes (modules/onboarding/routes/genesis-work.js) over a real express
|
|
4
|
+
// server and an in-memory store. Auth is stubbed at the doorway the way other route
|
|
5
|
+
// tests do it: a request names its builder in `x-test-builder`, and only the builder
|
|
6
|
+
// named in `x-test-owner` holds `project.settings.manage`. What it proves: any builder
|
|
7
|
+
// reads, only the owner writes, a refusal is a 4xx with its code, un-answer is DELETE,
|
|
8
|
+
// and the planning read path comes back over the wire.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/genesis_work_routes.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { memStore } from './genesis_work.mjs';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const express = require('express');
|
|
18
|
+
const api = require('../src/module-api.js');
|
|
19
|
+
|
|
20
|
+
const OWNER = 1;
|
|
21
|
+
const OTHER = 2;
|
|
22
|
+
api.requireBuilder = (req, res, next) => {
|
|
23
|
+
const id = Number(req.get('x-test-builder'));
|
|
24
|
+
if (!id) return res.status(401).json({ error: { code: 'unauthenticated' } });
|
|
25
|
+
req.builder = { id };
|
|
26
|
+
next();
|
|
27
|
+
};
|
|
28
|
+
api.requirePermission = () => (req, res, next) => (req.builder.id === OWNER ? next() : res.status(403).json({ error: { code: 'forbidden' } }));
|
|
29
|
+
|
|
30
|
+
const mount = require('../modules/onboarding/routes/genesis-work.js');
|
|
31
|
+
const store = memStore();
|
|
32
|
+
const app = express();
|
|
33
|
+
app.use(express.json());
|
|
34
|
+
const router = express.Router();
|
|
35
|
+
mount(router, { store });
|
|
36
|
+
app.use('/api/bongos', router);
|
|
37
|
+
const server = app.listen(0);
|
|
38
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
39
|
+
|
|
40
|
+
async function call(method, path, { as = OWNER, body } = {}) {
|
|
41
|
+
const r = await fetch(base + path, {
|
|
42
|
+
method,
|
|
43
|
+
headers: { 'content-type': 'application/json', ...(as ? { 'x-test-builder': String(as) } : {}) },
|
|
44
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
45
|
+
});
|
|
46
|
+
return { status: r.status, data: await r.json() };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let passed = 0;
|
|
50
|
+
let failed = 0;
|
|
51
|
+
async function test(name, fn) {
|
|
52
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
53
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await test('reads need a signed-in builder', async () => {
|
|
58
|
+
assert.equal((await call('GET', '/founding/work', { as: null })).status, 401);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await test('the owner answers, and any builder reads the answer back', async () => {
|
|
62
|
+
const w = await call('PUT', '/founding/work/strategize/answers/big_pieces', { body: { text: 'Ledger first.' } });
|
|
63
|
+
assert.equal(w.status, 200);
|
|
64
|
+
assert.equal(w.data.changed, true);
|
|
65
|
+
const r = await call('GET', '/founding/work/strategize', { as: OTHER });
|
|
66
|
+
assert.equal(r.status, 200);
|
|
67
|
+
assert.equal(r.data.work.questions.find((q) => q.id === 'big_pieces').answer.text, 'Ledger first.');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await test('a builder who is not the owner cannot write', async () => {
|
|
71
|
+
for (const [m, p, body] of [
|
|
72
|
+
['PUT', '/founding/work/strategize/answers/risks', { text: 'x' }],
|
|
73
|
+
['DELETE', '/founding/work/strategize/answers/big_pieces'],
|
|
74
|
+
['PUT', '/founding/work/strategize/door', { door: 'open' }],
|
|
75
|
+
['PUT', '/founding/work/strategize/page', { page: 'x' }],
|
|
76
|
+
['PUT', '/founding/work/strategize/link', { url: 'https://example.com' }],
|
|
77
|
+
]) assert.equal((await call(m, p, { as: OTHER, body })).status, 403, `${m} ${p}`);
|
|
78
|
+
assert.equal(store.answers.size, 1, 'nothing was written');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await test('"not sure yet" over the wire, then un-answer back to empty', async () => {
|
|
82
|
+
const ns = await call('PUT', '/founding/work/strategize/answers/risks', { body: { state: 'not_sure' } });
|
|
83
|
+
assert.equal(ns.data.work.questions.find((q) => q.id === 'risks').answer.state, 'not_sure');
|
|
84
|
+
const del = await call('DELETE', '/founding/work/strategize/answers/risks');
|
|
85
|
+
assert.equal(del.status, 200);
|
|
86
|
+
assert.equal(del.data.cleared, true);
|
|
87
|
+
assert.equal(del.data.work.questions.find((q) => q.id === 'risks').answer, null);
|
|
88
|
+
const h = await call('GET', '/founding/work/strategize/answers/risks/history', { as: OTHER });
|
|
89
|
+
assert.deepEqual(h.data.history.map((x) => x.state), ['cleared', 'not_sure']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test('refusals come back as 4xx with their code', async () => {
|
|
93
|
+
const cases = [
|
|
94
|
+
['PUT', '/founding/work/strategize/answers/nope', { text: 'x' }, 404, 'unknown_question'],
|
|
95
|
+
['GET', '/founding/work/decide', undefined, 404, 'unknown_stage'],
|
|
96
|
+
['PUT', '/founding/work/strategize/answers/risks', { text: ' ' }, 400, 'empty_answer'],
|
|
97
|
+
['PUT', '/founding/work/strategize/door', { door: 'autopilot' }, 400, 'bad_door'],
|
|
98
|
+
['PUT', '/founding/work/strategize/link', { url: 'ftp://x' }, 400, 'bad_link'],
|
|
99
|
+
];
|
|
100
|
+
for (const [m, p, body, status, code] of cases) {
|
|
101
|
+
const r = await call(m, p, { body });
|
|
102
|
+
assert.equal(r.status, status, `${m} ${p}`);
|
|
103
|
+
assert.equal(r.data.error.code, code);
|
|
104
|
+
}
|
|
105
|
+
const unknownField = await call('PUT', '/founding/work/strategize/door', { body: { door: 'open', extra: 1 } });
|
|
106
|
+
assert.equal(unknownField.status, 400, 'an unknown field is refused');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await test('switching door over the wire keeps the answers', async () => {
|
|
110
|
+
await call('PUT', '/founding/work/strategize/page', { body: { page: 'Bet on the ledger.' } });
|
|
111
|
+
const r = await call('PUT', '/founding/work/strategize/door', { body: { door: 'open' } });
|
|
112
|
+
assert.equal(r.data.work.door, 'open');
|
|
113
|
+
assert.equal(r.data.work.page, 'Bet on the ledger.');
|
|
114
|
+
assert.equal(r.data.work.counts.answered, 1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await test('the planning read path and the assisted brief are readable by any builder', async () => {
|
|
118
|
+
const c = await call('GET', '/founding/context', { as: OTHER });
|
|
119
|
+
assert.equal(c.status, 200);
|
|
120
|
+
assert.match(c.data.markdown, /Ledger first\./);
|
|
121
|
+
assert.match(c.data.markdown, /Bet on the ledger\./);
|
|
122
|
+
const a = await call('GET', '/founding/work/strategize/assist', { as: OTHER });
|
|
123
|
+
assert.match(a.data.prompt, /I am the author/);
|
|
124
|
+
});
|
|
125
|
+
} finally {
|
|
126
|
+
server.close();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(`\ngenesis_work_routes: ${passed} passed, ${failed} failed`);
|
|
130
|
+
if (failed) process.exit(1);
|