@bongos/core 1.25.16 → 1.25.18
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 +200 -75
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +24 -0
- package/clients/bongos-client/index.cjs +24 -0
- package/clients/bongos-client/index.d.ts +36 -2
- package/clients/bongos-client/index.mjs +24 -0
- package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +8 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +1 -0
- package/docs/api/openapi.json +761 -112
- package/docs/api-reference.md +23 -3
- package/docs/copy-inventory.md +118 -108
- package/docs/copy-registry.json +315 -172
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +3 -0
- package/docs/page-readings.json +62 -62
- package/modules/app-host/CLAUDE.md +15 -0
- package/modules/app-host/module.json +20 -0
- package/modules/app-host/public/settings-widget.js +143 -0
- package/modules/app-host/routes/app-host.js +88 -0
- package/modules/government/charter-advice.js +103 -0
- package/modules/government/decision-rules.js +1 -0
- package/modules/government/presets.js +42 -0
- package/modules/government/recommendations.js +102 -7
- package/modules/government/routes/government.js +1 -0
- package/modules/lifecycle/first-read-catalog.js +62 -0
- package/modules/lifecycle/first-read-db.js +190 -0
- package/modules/lifecycle/first-read.js +330 -0
- package/modules/lifecycle/kickoff-checklist.js +12 -3
- package/modules/lifecycle/migrations/lifecycle_022_repo_maps.sql +44 -0
- package/modules/lifecycle/migrations/lifecycle_023_repo_map_dismissed.sql +19 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/first-read.js +101 -0
- package/modules/onboarding/genesis-work.js +101 -4
- package/modules/onboarding/routes/genesis-work.js +2 -2
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/routes/sso-app-status.js +65 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/pollers/app-liveness.js +23 -1
- package/modules/provisioning/render-sandbox.js +113 -0
- package/modules/provisioning/render-standup.js +3 -1
- package/modules/provisioning/routes/provisioning.js +4 -1
- package/modules/provisioning/routes/render-standup.js +49 -1
- package/modules/provisioning/seams.js +32 -1
- package/modules/provisioning/starter-bundles.js +1 -0
- package/modules/public-landing/public/assets/guide-core.js +475 -0
- package/modules/public-landing/public/assets/guide-hall.css +121 -0
- package/modules/public-landing/public/assets/guide-hall.js +164 -0
- package/modules/public-landing/public/assets/guide-scene.css +9 -0
- package/modules/public-landing/public/assets/guide-scene.js +58 -381
- package/modules/public-landing/public/index.html +46 -1
- package/modules/public-landing/public/projects.html +112 -6
- package/modules/ui-design/kit/serve.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/bongos/first-read.js +150 -0
- package/scripts/bongos/genesis.js +21 -1
- package/scripts/bongos/government-walk.js +4 -4
- package/scripts/bongos/provision-render-sandbox.js +171 -0
- package/scripts/bongos/provision-render.js +37 -2
- package/scripts/bongos/provision-teardown.js +1 -1
- package/scripts/bongos/publish-manifest.js +1 -0
- package/scripts/bongos/render-api.js +33 -3
- package/scripts/bongos/render-payload.js +39 -1
- package/scripts/bongos/seam-wiring-guard.js +3 -2
- package/scripts/gds/first-read.js +7 -0
- package/scripts/gds/provision-render-sandbox.js +6 -0
- package/scripts/hall-preview/server.js +2 -0
- package/src/bongos/auth-admission.js +64 -0
- package/src/bongos/auth.js +2 -1
- package/src/bongos/module-scope-map.js +4 -0
- package/src/bongos/routes/core-update.js +3 -16
- package/src/bongos/routes/modules.js +11 -0
- package/src/bongos/serve-internal.js +20 -6
- package/src/module-api.js +6 -1
- package/src/module-loader/loader.js +2 -2
- package/tests/app_host.mjs +413 -0
- package/tests/first_read.mjs +479 -0
- package/tests/genesis_work.mjs +74 -3
- package/tests/government_act_alone_buildings.mjs +2 -0
- package/tests/government_charter_advice.mjs +150 -0
- package/tests/landing_guide_hall.mjs +364 -0
- package/tests/landing_guide_scene.mjs +8 -5
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/npm_release_where.mjs +13 -3
- package/tests/provisioning_app_status.mjs +3 -2
- package/tests/provisioning_render_route.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +50 -0
- package/tests/render_api.mjs +5 -2
- package/tests/render_sandbox.mjs +447 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// tests/first_read.mjs — THE FIRST READ-THROUGH of a project that already exists (task
|
|
2
|
+
// 1005280, goal 1000106).
|
|
3
|
+
//
|
|
4
|
+
// Done-when: an adopted project's Genesis step hands the owner a prompt for their OWN AI
|
|
5
|
+
// session; that session's map comes back in a defined, validated shape; the owner
|
|
6
|
+
// accepts it whole or item by item into DRAFT goals and tasks; a rejection files
|
|
7
|
+
// nothing; a re-run files nothing twice; the shape has room for task 1005281's
|
|
8
|
+
// recommended modules.
|
|
9
|
+
//
|
|
10
|
+
// Everything here RUNS: the real prompt and Genesis checklist, the real validator and
|
|
11
|
+
// acceptance planner, the real store over an in-memory fake pool, and the real router
|
|
12
|
+
// over a socket.
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { test, after } from 'node:test';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const fr = require('../modules/lifecycle/first-read.js');
|
|
20
|
+
const storeModule = require('../modules/lifecycle/first-read-db.js');
|
|
21
|
+
// A snapshot of the real store: the route section below re-points the module's exports
|
|
22
|
+
// at its own pool, and module-level code runs before any test does.
|
|
23
|
+
const store = { ...storeModule };
|
|
24
|
+
const { genesisChecklist } = require('../modules/lifecycle/kickoff-checklist.js');
|
|
25
|
+
|
|
26
|
+
const clone = (o) => JSON.parse(JSON.stringify(o));
|
|
27
|
+
|
|
28
|
+
// ── an in-memory pool that answers exactly the store's statements ─────────────
|
|
29
|
+
|
|
30
|
+
function fakePool() {
|
|
31
|
+
const s = { maps: [], filed: new Map(), goals: [], tasks: [], nextId: 1 };
|
|
32
|
+
const query = async (sql, p = []) => {
|
|
33
|
+
const q = sql.replace(/\s+/g, ' ').trim();
|
|
34
|
+
if (/^(BEGIN|COMMIT|ROLLBACK)$/.test(q) || /pg_advisory_xact_lock/.test(q)) return { rows: [] };
|
|
35
|
+
if (/^INSERT INTO lifecycle_repo_maps/.test(q)) {
|
|
36
|
+
const row = { id: String(s.nextId++), schema: p[0], map: JSON.parse(p[1]), status: 'pending', submitted_by: p[2], submitted_at: new Date() };
|
|
37
|
+
s.maps.push(row); return { rows: [row] };
|
|
38
|
+
}
|
|
39
|
+
if (/FROM lifecycle_repo_maps ORDER BY/.test(q)) return { rows: s.maps.slice(-1) };
|
|
40
|
+
if (/^SELECT .* FROM lifecycle_repo_maps WHERE id = \$1/.test(q)) return { rows: s.maps.filter((m) => m.id === String(p[0])) };
|
|
41
|
+
if (/^SELECT ref, kind, entity_id FROM lifecycle_repo_map_filed/.test(q)) {
|
|
42
|
+
return { rows: p[0].filter((r) => s.filed.has(r)).map((r) => ({ ref: r, ...s.filed.get(r) })) };
|
|
43
|
+
}
|
|
44
|
+
if (/^INSERT INTO lifecycle_repo_map_filed .*'dismissed', NULL/.test(q)) {
|
|
45
|
+
// the owner's no: ON CONFLICT DO NOTHING
|
|
46
|
+
for (const ref of p[0]) if (!s.filed.has(ref)) s.filed.set(ref, { kind: 'dismissed', entity_id: null, map_id: p[1] });
|
|
47
|
+
return { rows: [] };
|
|
48
|
+
}
|
|
49
|
+
if (/^INSERT INTO lifecycle_repo_map_filed .* unnest/.test(q)) {
|
|
50
|
+
s.filedWrites = (s.filedWrites || 0) + 1;
|
|
51
|
+
p[0].forEach((ref, i) => {
|
|
52
|
+
// ON CONFLICT ... DO UPDATE ... WHERE kind = 'dismissed': only a dismissed row is replaced
|
|
53
|
+
if (s.filed.has(ref) && s.filed.get(ref).kind !== 'dismissed') throw Object.assign(new Error('filed twice'), { code: '23505' });
|
|
54
|
+
s.filed.set(ref, { kind: p[1][i], entity_id: p[2][i], map_id: p[3] });
|
|
55
|
+
});
|
|
56
|
+
return { rows: [] };
|
|
57
|
+
}
|
|
58
|
+
if (/^SELECT id FROM versions/.test(q)) return { rows: [{ id: 'V1' }] };
|
|
59
|
+
if (/^UPDATE lifecycle_repo_maps SET status = 'accepted'/.test(q)) { s.maps.find((m) => m.id === String(p[0])).status = 'accepted'; return { rows: [] }; }
|
|
60
|
+
if (/^UPDATE lifecycle_repo_maps SET status = 'rejected'/.test(q)) {
|
|
61
|
+
const m = s.maps.find((x) => x.id === String(p[0]) && x.status === 'pending');
|
|
62
|
+
if (!m) return { rows: [] };
|
|
63
|
+
m.status = 'rejected'; return { rows: [{ id: m.id }] };
|
|
64
|
+
}
|
|
65
|
+
throw new Error('unexpected SQL: ' + q);
|
|
66
|
+
};
|
|
67
|
+
const pool = { query, connect: async () => ({ query, release() {} }), state: s };
|
|
68
|
+
const deps = {
|
|
69
|
+
pool,
|
|
70
|
+
createGoal: async (row, d) => { assert.ok(d && d.client, 'goals are created inside the accept transaction'); const g = { id: String(100 + s.goals.length), ...row }; s.goals.push(g); return g; },
|
|
71
|
+
createTask: async (row, d) => { assert.ok(d && d.client, 'tasks are created inside the accept transaction'); const t = { id: String(500 + s.tasks.length), ...row }; s.tasks.push(t); return t; },
|
|
72
|
+
};
|
|
73
|
+
return { pool, deps, s };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── the prompt ───────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
test('the prompt sends the owner\'s own session to read locally, map in the defined shape, and accept only with a yes', () => {
|
|
79
|
+
const p = fr.firstReadPrompt();
|
|
80
|
+
for (const must of [
|
|
81
|
+
'own AI session', 'Bongos itself never reads this repository', 'Do not upload, paste or send any source code',
|
|
82
|
+
fr.MAP_FILE, fr.SCHEMA, '"goals"', '"next"', '"why"', '"seen_at"', 'errors or mishaps',
|
|
83
|
+
'first-read check', 'first-read submit', 'first-read accept', 'first-read reject', 'first-read dismiss', 'Never accept or dismiss on your own',
|
|
84
|
+
'not filed twice',
|
|
85
|
+
]) assert.ok(p.includes(must), `the prompt says: ${must}`);
|
|
86
|
+
// The example it prints is itself a valid map, so a session that copies it is on the shape.
|
|
87
|
+
assert.equal(fr.validateRepoMap(fr.exampleMap()).ok, true);
|
|
88
|
+
const from = p.indexOf('Use exactly this shape:') + 'Use exactly this shape:'.length;
|
|
89
|
+
const printed = JSON.parse(p.slice(p.indexOf('{', from), p.indexOf('\n - Optional', from)));
|
|
90
|
+
assert.deepEqual(printed, fr.exampleMap());
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('an adopted project\'s Genesis step carries the prompt; a new project\'s does not', () => {
|
|
94
|
+
const existing = genesisChecklist({ existing: true }).scope;
|
|
95
|
+
assert.equal(existing[0].ref, 'genesis:scope:existing', 'the step keeps its ref, so a seeded project is not refiled');
|
|
96
|
+
assert.ok(existing[0].description.includes(fr.firstReadPrompt()), 'the hall copy-prompt hands over the whole prompt');
|
|
97
|
+
assert.ok(/own AI/.test(existing[0].title));
|
|
98
|
+
const fresh = genesisChecklist().scope;
|
|
99
|
+
assert.ok(fresh.every((t) => !t.description.includes(fr.SCHEMA)));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── validation ───────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
test('a good map validates into a flat review list with stable refs', () => {
|
|
105
|
+
const v = fr.validateRepoMap(fr.exampleMap());
|
|
106
|
+
assert.equal(v.ok, true);
|
|
107
|
+
assert.deepEqual(v.items.map((i) => [i.type, i.ref]), [
|
|
108
|
+
['goal', 'goal:take-payments-for-bookings'],
|
|
109
|
+
['task', 'task:take-payments-for-bookings:finish-the-checkout-page'],
|
|
110
|
+
['task', 'task:take-payments-for-bookings:send-a-receipt-by-email'],
|
|
111
|
+
['next', 'next:fix-the-failing-booking-test'],
|
|
112
|
+
]);
|
|
113
|
+
assert.ok(v.items.every((i) => i.why && i.seen_at.length));
|
|
114
|
+
assert.equal(v.items[3].goal_ref, fr.NEXT_GOAL.ref, 'a next item naming no goal lands under the fix-what-was-found goal');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('a section this version does not read rides along without breaking the shape', () => {
|
|
118
|
+
const raw = { ...fr.exampleMap(), later_section: [{ key: 'x', why: 'room to grow' }] };
|
|
119
|
+
const v = fr.validateRepoMap(raw);
|
|
120
|
+
assert.equal(v.ok, true);
|
|
121
|
+
assert.deepEqual(v.map.later_section, raw.later_section);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('a prototype-shaped key in the map is never copied onto it', () => {
|
|
125
|
+
const raw = JSON.parse(`{"schema":"${fr.SCHEMA}","summary":"s","next":[{"title":"t","why":"w","seen_at":["a"]}],"__proto__":{"polluted":true},"constructor":{"x":1}}`);
|
|
126
|
+
const v = fr.validateRepoMap(raw);
|
|
127
|
+
assert.equal(v.ok, true);
|
|
128
|
+
assert.equal(v.map.polluted, undefined);
|
|
129
|
+
assert.equal(Object.getPrototypeOf(v.map), Object.prototype);
|
|
130
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(v.map, 'constructor'));
|
|
131
|
+
assert.equal(({}).polluted, undefined);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('malformed maps are refused, each error naming its field', () => {
|
|
135
|
+
const cases = [
|
|
136
|
+
[null, /JSON object/],
|
|
137
|
+
[[], /JSON object/],
|
|
138
|
+
[{ ...fr.exampleMap(), schema: 'other/1' }, /schema must be/],
|
|
139
|
+
[{ ...fr.exampleMap(), goals: 'lots' }, /goals must be a list/],
|
|
140
|
+
[{ schema: fr.SCHEMA, summary: 'x', goals: [], next: [] }, /no goals and nothing next/],
|
|
141
|
+
[(() => { const m = fr.exampleMap(); delete m.goals[0].why; return m; })(), /goals\[0\]\.why is missing/],
|
|
142
|
+
[(() => { const m = fr.exampleMap(); m.goals[0].tasks[0].seen_at = []; return m; })(), /goals\[0\]\.tasks\[0\]\.seen_at needs at least one place/],
|
|
143
|
+
[(() => { const m = fr.exampleMap(); m.goals[0].tasks[1].status = 'half'; return m; })(), /status must be one of/],
|
|
144
|
+
[(() => { const m = fr.exampleMap(); m.goals[0].tasks[1].title = m.goals[0].tasks[0].title; return m; })(), /repeats another item's title/],
|
|
145
|
+
[(() => { const m = fr.exampleMap(); m.next[0].goal = 'No such goal'; return m; })(), /not a goal in this map/],
|
|
146
|
+
[(() => { const m = fr.exampleMap(); m.next[0].kind = 'oops'; return m; })(), /kind must be one of/],
|
|
147
|
+
[(() => { const m = fr.exampleMap(); m.goals[0].why = 'x'.repeat(fr.LIMITS.WHY + 1); return m; })(), /longer than/],
|
|
148
|
+
[(() => { const m = fr.exampleMap(); m.summary = 'y'.repeat(fr.LIMITS.BYTES); return m; })(), /larger than/],
|
|
149
|
+
[(() => { const m = fr.exampleMap(); m.goals[0].title = '!!!'; return m; })(), /needs letters or numbers/],
|
|
150
|
+
];
|
|
151
|
+
for (const [raw, re] of cases) {
|
|
152
|
+
const v = fr.validateRepoMap(raw);
|
|
153
|
+
assert.equal(v.ok, false, `refused: ${re}`);
|
|
154
|
+
assert.ok(v.errors.some((e) => re.test(e)), `${re} in ${JSON.stringify(v.errors)}`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── the store: submit, accept, reject, re-run ────────────────────────────────
|
|
159
|
+
|
|
160
|
+
test('submitting files nothing: the map waits pending', async () => {
|
|
161
|
+
const { deps, s } = fakePool();
|
|
162
|
+
const out = await store.submitMap({ raw: fr.exampleMap(), builderId: '1' }, deps);
|
|
163
|
+
assert.equal(out.ok, true);
|
|
164
|
+
assert.equal(s.maps[0].status, 'pending');
|
|
165
|
+
assert.equal(s.goals.length + s.tasks.length, 0);
|
|
166
|
+
const bad = await store.submitMap({ raw: { schema: 'nope' } }, deps);
|
|
167
|
+
assert.equal(bad.ok, false);
|
|
168
|
+
assert.equal(s.maps.length, 1, 'a malformed map is not stored');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('accepting the whole map files draft goals and backlog tasks, nothing claimable', async () => {
|
|
172
|
+
const { deps, s } = fakePool();
|
|
173
|
+
const { map_id } = await store.submitMap({ raw: fr.exampleMap(), builderId: '1' }, deps);
|
|
174
|
+
const out = await store.acceptMap({ mapId: map_id, select: 'all', builderId: '1' }, deps);
|
|
175
|
+
assert.equal(out.ok, true);
|
|
176
|
+
assert.deepEqual(s.goals.map((g) => g.title), ['Take payments for bookings', fr.NEXT_GOAL.title]);
|
|
177
|
+
assert.equal(s.tasks.length, 3);
|
|
178
|
+
for (const t of s.tasks) {
|
|
179
|
+
assert.equal(t.status, 'backlog');
|
|
180
|
+
assert.equal(t.source, store.TASK_SOURCE);
|
|
181
|
+
assert.ok(t.goalId, 'every task has a goal home');
|
|
182
|
+
assert.ok(t.description.includes('Seen at:'));
|
|
183
|
+
}
|
|
184
|
+
assert.equal(s.tasks.find((t) => t.title === 'Fix the failing booking test').kind, 'bug');
|
|
185
|
+
assert.equal(s.tasks.find((t) => t.title === 'Fix the failing booking test').goalId, s.goals[1].id);
|
|
186
|
+
assert.equal(s.maps[0].status, 'accepted');
|
|
187
|
+
assert.equal(s.filedWrites, 1, 'every filed ref is remembered in one statement');
|
|
188
|
+
assert.equal(s.filed.size, 5);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('accepting item by item files only what the owner chose (and the goal it needs)', async () => {
|
|
192
|
+
const { deps, s } = fakePool();
|
|
193
|
+
const { map_id } = await store.submitMap({ raw: fr.exampleMap() }, deps);
|
|
194
|
+
const out = await store.acceptMap({ mapId: map_id, select: ['task:take-payments-for-bookings:send-a-receipt-by-email'] }, deps);
|
|
195
|
+
assert.equal(out.ok, true);
|
|
196
|
+
assert.deepEqual(s.goals.map((g) => g.title), ['Take payments for bookings']);
|
|
197
|
+
assert.deepEqual(s.tasks.map((t) => t.title), ['Send a receipt by email']);
|
|
198
|
+
const unknown = await store.acceptMap({ mapId: map_id, select: ['task:nope'] }, deps);
|
|
199
|
+
assert.equal(unknown.code, 'unknown_items');
|
|
200
|
+
const none = await store.acceptMap({ mapId: map_id, select: [] }, deps);
|
|
201
|
+
assert.equal(none.code, 'nothing_selected');
|
|
202
|
+
assert.equal(s.tasks.length, 1, 'a refused accept files nothing');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('a task the map says is done is shown, never filed', async () => {
|
|
206
|
+
const { deps, s } = fakePool();
|
|
207
|
+
const raw = fr.exampleMap();
|
|
208
|
+
raw.goals[0].tasks[0].status = 'done';
|
|
209
|
+
const { map_id } = await store.submitMap({ raw }, deps);
|
|
210
|
+
const out = await store.acceptMap({ mapId: map_id, select: 'all' }, deps);
|
|
211
|
+
assert.ok(out.skipped.some((x) => x.reason === 'already_done'));
|
|
212
|
+
assert.ok(!s.tasks.some((t) => t.title === 'Finish the checkout page'));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('rejecting files nothing, and a rejected map cannot then be accepted', async () => {
|
|
216
|
+
const { deps, s } = fakePool();
|
|
217
|
+
const { map_id } = await store.submitMap({ raw: fr.exampleMap() }, deps);
|
|
218
|
+
assert.equal((await store.rejectMap({ mapId: map_id, builderId: '1' }, deps)).ok, true);
|
|
219
|
+
assert.equal(s.maps[0].status, 'rejected');
|
|
220
|
+
const again = await store.acceptMap({ mapId: map_id, select: 'all' }, deps);
|
|
221
|
+
assert.equal(again.code, 'map_rejected');
|
|
222
|
+
assert.equal(s.goals.length + s.tasks.length, 0);
|
|
223
|
+
assert.equal((await store.rejectMap({ mapId: map_id }, deps)).code, 'not_pending');
|
|
224
|
+
assert.equal((await store.rejectMap({ mapId: '99' }, deps)).code, 'not_found');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('a re-run does not duplicate: the same items are skipped, only new ones are filed', async () => {
|
|
228
|
+
const { deps, s } = fakePool();
|
|
229
|
+
const first = await store.submitMap({ raw: fr.exampleMap() }, deps);
|
|
230
|
+
await store.acceptMap({ mapId: first.map_id, select: 'all' }, deps);
|
|
231
|
+
const goalsBefore = s.goals.length;
|
|
232
|
+
const tasksBefore = s.tasks.length;
|
|
233
|
+
// Accepting the same map again files nothing.
|
|
234
|
+
const twice = await store.acceptMap({ mapId: first.map_id, select: 'all' }, deps);
|
|
235
|
+
assert.equal(twice.filed.goals.length + twice.filed.tasks.length, 0);
|
|
236
|
+
// A second read-through names the same items (reordered, retitled in case) plus one new task.
|
|
237
|
+
const raw = clone(fr.exampleMap());
|
|
238
|
+
raw.goals[0].title = 'TAKE PAYMENTS FOR BOOKINGS';
|
|
239
|
+
raw.goals[0].tasks.reverse();
|
|
240
|
+
raw.goals[0].tasks.push({ title: 'Refund a booking', status: 'not_started', why: 'No refund path exists.', seen_at: ['src/payments/'] });
|
|
241
|
+
const second = await store.submitMap({ raw }, deps);
|
|
242
|
+
const review = await store.reviewMap('latest', deps);
|
|
243
|
+
assert.equal(review.map_id, second.map_id);
|
|
244
|
+
assert.equal(review.items.filter((i) => i.filed).length, 4, 'the review marks what is already in Bongos');
|
|
245
|
+
const out = await store.acceptMap({ mapId: second.map_id, select: 'all' }, deps);
|
|
246
|
+
assert.equal(s.goals.length, goalsBefore, 'no goal filed twice');
|
|
247
|
+
assert.equal(s.tasks.length, tasksBefore + 1);
|
|
248
|
+
assert.deepEqual(out.filed.tasks.map((t) => t.title), ['Refund a booking']);
|
|
249
|
+
assert.equal(out.filed.tasks[0].goal_id, s.goals[0].id, 'the new task joins the goal filed by the first run');
|
|
250
|
+
assert.equal(out.skipped.filter((x) => x.reason === 'already_filed').length, 4);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ── the routes, over a real socket ───────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
const express = require('express');
|
|
256
|
+
const api = require('../src/module-api.js');
|
|
257
|
+
const perms = [];
|
|
258
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: '7', rank: 'archon' }; next(); };
|
|
259
|
+
api.requirePermission = (...p) => { perms.push(p.join(',')); return (_req, _res, next) => next(); };
|
|
260
|
+
api.requireActAlone = (area) => { perms.push('act-alone:' + area); return (_req, _res, next) => next(); };
|
|
261
|
+
|
|
262
|
+
const route = fakePool();
|
|
263
|
+
for (const fn of ['submitMap', 'reviewMap', 'acceptMap', 'rejectMap', 'dismissItems']) {
|
|
264
|
+
storeModule[fn] = (a) => store[fn](a, route.deps);
|
|
265
|
+
}
|
|
266
|
+
const app = express();
|
|
267
|
+
app.use(express.json());
|
|
268
|
+
app.use((req, res, next) => {
|
|
269
|
+
res.fail = (code, status, details) => res.status(status || 400).json({ error: code, ...(details || {}) });
|
|
270
|
+
next();
|
|
271
|
+
});
|
|
272
|
+
app.use('/api/bongos', require('../modules/lifecycle/routes/first-read.js')());
|
|
273
|
+
const server = app.listen(0);
|
|
274
|
+
await new Promise((r) => server.once('listening', r));
|
|
275
|
+
after(() => server.close());
|
|
276
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
277
|
+
const call = async (method, p, body) => {
|
|
278
|
+
const r = await fetch(base + p, { method, headers: { 'content-type': 'application/json' }, body: body ? JSON.stringify(body) : undefined });
|
|
279
|
+
return { status: r.status, body: await r.json().catch(() => null) };
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
test('the routes: filing is gated like creating a goal, and the flow runs end to end', async () => {
|
|
283
|
+
assert.ok(perms.includes('goal.create'));
|
|
284
|
+
assert.ok(perms.includes('act-alone:planning'));
|
|
285
|
+
const prompt = await call('GET', '/first-read/prompt');
|
|
286
|
+
assert.equal(prompt.status, 200);
|
|
287
|
+
// No module list is registered in this test, so the prompt says so (task 1005281).
|
|
288
|
+
assert.equal(prompt.body.prompt, fr.firstReadPrompt({ catalog: null }));
|
|
289
|
+
|
|
290
|
+
const bad = await call('POST', '/first-read/maps', { map: { schema: fr.SCHEMA, summary: 'x', goals: [{ title: 'A' }] } });
|
|
291
|
+
assert.equal(bad.status, 400);
|
|
292
|
+
assert.equal(bad.body.error, 'invalid_map');
|
|
293
|
+
assert.ok(bad.body.errors.length >= 2);
|
|
294
|
+
|
|
295
|
+
const ok = await call('POST', '/first-read/maps', { map: fr.exampleMap() });
|
|
296
|
+
assert.equal(ok.status, 201);
|
|
297
|
+
assert.equal(route.s.tasks.length, 0, 'submit files nothing');
|
|
298
|
+
|
|
299
|
+
const bogus = await call('POST', `/first-read/maps/${ok.body.map_id}/accept`, { items: ['x'], all: true });
|
|
300
|
+
assert.equal(bogus.status, 400);
|
|
301
|
+
const accepted = await call('POST', `/first-read/maps/${ok.body.map_id}/accept`, { items: ['goal:take-payments-for-bookings'] });
|
|
302
|
+
assert.equal(accepted.status, 200);
|
|
303
|
+
assert.equal(accepted.body.filed.goals.length, 1);
|
|
304
|
+
assert.equal(accepted.body.filed.tasks.length, 0);
|
|
305
|
+
|
|
306
|
+
const shown = await call('GET', '/first-read/maps/latest');
|
|
307
|
+
assert.equal(shown.status, 200);
|
|
308
|
+
assert.equal(shown.body.items.find((i) => i.ref === 'goal:take-payments-for-bookings').filed.kind, 'goal');
|
|
309
|
+
assert.equal((await call('GET', '/first-read/maps/999')).status, 404);
|
|
310
|
+
assert.equal((await call('POST', `/first-read/maps/${ok.body.map_id}/reject`)).status, 409, 'an accepted map is not then rejected');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ── recommended modules (task 1005281) ───────────────────────────────────────
|
|
314
|
+
//
|
|
315
|
+
// The owner's own session compares what the code does with this project's live module
|
|
316
|
+
// list and may recommend from it. Only keys on that list are accepted; a recommendation
|
|
317
|
+
// is a suggestion the owner accepts (a draft task to switch it on) or dismisses.
|
|
318
|
+
|
|
319
|
+
const fc = require('../modules/lifecycle/first-read-catalog.js');
|
|
320
|
+
|
|
321
|
+
const LISTED = [
|
|
322
|
+
{ key: 'lifecycle', title: 'Lifecycle', description: 'Tasks and claims.', enabled: true },
|
|
323
|
+
{ key: 'billing', title: 'Billing (internal name)', description: 'Takes card payments through the processor and keeps the ledger. Engineers: see ADR.', enabled: false },
|
|
324
|
+
{ key: 'raise', title: 'Raise', description: 'A fundraising page.', enabled: false },
|
|
325
|
+
];
|
|
326
|
+
const PICKER = { optional: [{ key: 'billing', label: 'Billing', summary: 'Take payments and send receipts.' }] };
|
|
327
|
+
const CATALOG = fc.buildCatalog(LISTED, PICKER);
|
|
328
|
+
|
|
329
|
+
// A repository whose billing code keeps failing: a known weak area.
|
|
330
|
+
function weakBillingMap() {
|
|
331
|
+
return {
|
|
332
|
+
schema: fr.SCHEMA,
|
|
333
|
+
summary: 'A shop whose own card-payment code keeps failing.',
|
|
334
|
+
next: [{ title: 'Payments fail at checkout', kind: 'error', why: 'Half the test charges fail with a timeout.', seen_at: ['src/billing/charge.js:30'] }],
|
|
335
|
+
recommended_modules: [{
|
|
336
|
+
module: 'billing', title: 'Session-chosen words, ignored',
|
|
337
|
+
found: 'The hand-written billing code retries forever and loses receipts.',
|
|
338
|
+
why: 'The Billing module takes payments and sends receipts, so this code can go.',
|
|
339
|
+
seen_at: ['src/billing/'],
|
|
340
|
+
}],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
test('the catalog is this project\'s modules not yet on, in the picker\'s words where it has them', () => {
|
|
345
|
+
assert.deepEqual(CATALOG.map((m) => m.key), ['billing', 'raise'], 'an enabled module is not offered');
|
|
346
|
+
assert.equal(CATALOG[0].title, 'Billing');
|
|
347
|
+
assert.equal(CATALOG[0].summary, 'Take payments and send receipts.');
|
|
348
|
+
assert.equal(CATALOG[1].summary, 'A fundraising page.', 'otherwise the module\'s own first sentence');
|
|
349
|
+
assert.equal(fc.buildCatalog(null, null).length, 0);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('the catalog is read live from the kernel port, and is null when no list can be read', async () => {
|
|
353
|
+
const seams = require('../src/module-seams.js');
|
|
354
|
+
assert.equal(await fc.switchOnCatalog(), null, 'no port, no list: nothing may be recommended');
|
|
355
|
+
// Left registered: the route test below reads the same list.
|
|
356
|
+
seams.registerProvider('modules.catalog', () => LISTED);
|
|
357
|
+
assert.deepEqual((await fc.switchOnCatalog()).map((m) => m.key), ['billing', 'raise']);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('the prompt carries the live list; the Genesis copy tells the session how to get it', () => {
|
|
361
|
+
const p = fr.firstReadPrompt({ catalog: CATALOG });
|
|
362
|
+
assert.ok(p.includes('- billing: Billing. Take payments and send receipts.'));
|
|
363
|
+
assert.ok(p.includes('"recommended_modules"') && p.includes('Recommend only from the list'));
|
|
364
|
+
assert.ok(p.includes('no recommendation is a good answer'));
|
|
365
|
+
assert.ok(fr.firstReadPrompt().includes('first-read catalog'), 'the fixed Genesis text names the live list');
|
|
366
|
+
assert.ok(fr.firstReadPrompt({ catalog: null }).includes('could not be read'));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('a repository with a known weak area gets the matching module, with its reason', async () => {
|
|
370
|
+
const v = fr.validateRepoMap(weakBillingMap(), { catalog: CATALOG });
|
|
371
|
+
assert.equal(v.ok, true, JSON.stringify(v.errors));
|
|
372
|
+
const mod = v.items.find((i) => i.type === 'module');
|
|
373
|
+
assert.equal(mod.ref, 'module:billing');
|
|
374
|
+
assert.equal(mod.title, 'Billing', 'the catalog\'s words, never the session\'s');
|
|
375
|
+
assert.match(mod.found, /retries forever/);
|
|
376
|
+
assert.match(mod.why, /takes payments/);
|
|
377
|
+
assert.deepEqual(v.map.recommended_modules[0].title, 'Billing');
|
|
378
|
+
|
|
379
|
+
const { deps, s } = fakePool();
|
|
380
|
+
const { map_id } = await store.submitMap({ raw: weakBillingMap(), catalog: CATALOG }, deps);
|
|
381
|
+
const out = await store.acceptMap({ mapId: map_id, select: 'all' }, deps);
|
|
382
|
+
assert.equal(out.ok, true);
|
|
383
|
+
assert.ok(s.goals.some((g) => g.title === fr.MODULES_GOAL.title));
|
|
384
|
+
const task = s.tasks.find((t) => t.sourceRef === 'module:billing');
|
|
385
|
+
assert.equal(task.title, 'Switch on the Billing module');
|
|
386
|
+
assert.equal(task.status, 'backlog', 'a draft: nothing is switched on, nothing is claimable');
|
|
387
|
+
assert.match(task.description, /Found: The hand-written billing code/);
|
|
388
|
+
assert.match(task.description, /Why the Billing module/);
|
|
389
|
+
assert.match(task.description, /Seen at: src\/billing\//);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('a repository with nothing to replace gets no recommendation', async () => {
|
|
393
|
+
const raw = { ...fr.exampleMap() };
|
|
394
|
+
const v = fr.validateRepoMap(raw, { catalog: CATALOG });
|
|
395
|
+
assert.equal(v.ok, true);
|
|
396
|
+
assert.equal(v.items.filter((i) => i.type === 'module').length, 0);
|
|
397
|
+
assert.equal(v.map.recommended_modules, undefined);
|
|
398
|
+
const { deps, s } = fakePool();
|
|
399
|
+
const { map_id } = await store.submitMap({ raw, catalog: CATALOG }, deps);
|
|
400
|
+
await store.acceptMap({ mapId: map_id, select: 'all' }, deps);
|
|
401
|
+
assert.ok(!s.goals.some((g) => g.title === fr.MODULES_GOAL.title), 'no modules goal is filed for an empty section');
|
|
402
|
+
assert.ok(!s.tasks.some((t) => /^Switch on the/.test(t.title)));
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('an unknown module, a repeated one, or any module when no list can be read, is refused', async () => {
|
|
406
|
+
const unknown = weakBillingMap();
|
|
407
|
+
unknown.recommended_modules[0].module = 'stripe-magic';
|
|
408
|
+
let v = fr.validateRepoMap(unknown, { catalog: CATALOG });
|
|
409
|
+
assert.equal(v.ok, false);
|
|
410
|
+
assert.ok(v.errors.some((e) => /"stripe-magic" is not a module this project can switch on/.test(e)));
|
|
411
|
+
|
|
412
|
+
const on = weakBillingMap();
|
|
413
|
+
on.recommended_modules[0].module = 'lifecycle';
|
|
414
|
+
assert.equal(fr.validateRepoMap(on, { catalog: CATALOG }).ok, false, 'a module already on is not on the list');
|
|
415
|
+
|
|
416
|
+
const twice = weakBillingMap();
|
|
417
|
+
twice.recommended_modules.push({ ...twice.recommended_modules[0] });
|
|
418
|
+
assert.ok(fr.validateRepoMap(twice, { catalog: CATALOG }).errors.some((e) => /repeats a module/.test(e)));
|
|
419
|
+
|
|
420
|
+
v = fr.validateRepoMap(weakBillingMap(), { catalog: null });
|
|
421
|
+
assert.ok(v.errors.some((e) => /could not be read here/.test(e)));
|
|
422
|
+
|
|
423
|
+
const missing = weakBillingMap();
|
|
424
|
+
delete missing.recommended_modules[0].found;
|
|
425
|
+
assert.ok(fr.validateRepoMap(missing, { catalog: CATALOG }).errors.some((e) => /recommended_modules\[0\]\.found is missing/.test(e)));
|
|
426
|
+
|
|
427
|
+
const { deps, s } = fakePool();
|
|
428
|
+
const out = await store.submitMap({ raw: unknown, catalog: CATALOG }, deps);
|
|
429
|
+
assert.equal(out.ok, false);
|
|
430
|
+
assert.equal(s.maps.length, 0, 'a refused map is not stored');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('dismissing a recommendation files nothing, and a re-run does not raise it again unless named', async () => {
|
|
434
|
+
const { deps, s } = fakePool();
|
|
435
|
+
const first = await store.submitMap({ raw: weakBillingMap(), catalog: CATALOG }, deps);
|
|
436
|
+
const d = await store.dismissItems({ mapId: first.map_id, refs: ['module:billing'] }, deps);
|
|
437
|
+
assert.equal(d.ok, true);
|
|
438
|
+
assert.deepEqual(d.dismissed, ['module:billing']);
|
|
439
|
+
assert.equal(s.tasks.length, 0, 'a no files nothing');
|
|
440
|
+
assert.equal((await store.dismissItems({ mapId: first.map_id, refs: ['module:nope'] }, deps)).code, 'unknown_items');
|
|
441
|
+
|
|
442
|
+
const second = await store.submitMap({ raw: weakBillingMap(), catalog: CATALOG }, deps);
|
|
443
|
+
const review = await store.reviewMap(second.map_id, deps);
|
|
444
|
+
assert.equal(review.items.find((i) => i.ref === 'module:billing').filed.kind, 'dismissed');
|
|
445
|
+
const all = await store.acceptMap({ mapId: second.map_id, select: 'all' }, deps);
|
|
446
|
+
assert.ok(all.skipped.some((x) => x.ref === 'module:billing' && x.reason === 'dismissed'));
|
|
447
|
+
assert.ok(!s.tasks.some((t) => t.sourceRef === 'module:billing'));
|
|
448
|
+
|
|
449
|
+
const named = await store.acceptMap({ mapId: second.map_id, select: ['module:billing'] }, deps);
|
|
450
|
+
assert.equal(named.filed.tasks.length, 1, 'naming a dismissed item files it: the owner changed their mind');
|
|
451
|
+
assert.equal(s.filed.get('module:billing').kind, 'task');
|
|
452
|
+
const again = await store.dismissItems({ mapId: second.map_id, refs: ['module:billing'] }, deps);
|
|
453
|
+
assert.ok(again.skipped.some((x) => x.reason === 'already_filed'), 'a filed item is not un-filed by a dismiss');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('the routes: the catalog route, a submit checked against the live list, and dismiss', async () => {
|
|
457
|
+
const seams = require('../src/module-seams.js');
|
|
458
|
+
if (!seams.hasProvider('modules.catalog')) seams.registerProvider('modules.catalog', () => LISTED);
|
|
459
|
+
const cat = await call('GET', '/first-read/catalog');
|
|
460
|
+
assert.equal(cat.status, 200);
|
|
461
|
+
assert.equal(cat.body.available, true);
|
|
462
|
+
assert.deepEqual(cat.body.modules.map((m) => m.key), ['billing', 'raise']);
|
|
463
|
+
const prompt = await call('GET', '/first-read/prompt');
|
|
464
|
+
assert.ok(prompt.body.prompt.includes('- billing: Billing (internal name). Takes card payments through the processor and keeps the ledger.'));
|
|
465
|
+
|
|
466
|
+
const bad = weakBillingMap();
|
|
467
|
+
bad.recommended_modules[0].module = 'stripe-magic';
|
|
468
|
+
const refused = await call('POST', '/first-read/maps', { map: bad });
|
|
469
|
+
assert.equal(refused.status, 400);
|
|
470
|
+
assert.ok(refused.body.errors.some((e) => /stripe-magic/.test(e)));
|
|
471
|
+
|
|
472
|
+
const ok = await call('POST', '/first-read/maps', { map: weakBillingMap() });
|
|
473
|
+
assert.equal(ok.status, 201);
|
|
474
|
+
assert.ok(ok.body.items.some((i) => i.ref === 'module:billing' && i.type === 'module'));
|
|
475
|
+
const dismissed = await call('POST', `/first-read/maps/${ok.body.map_id}/dismiss`, { items: ['module:billing'] });
|
|
476
|
+
assert.equal(dismissed.status, 200);
|
|
477
|
+
assert.equal((await call('POST', `/first-read/maps/${ok.body.map_id}/dismiss`, { items: 'all' })).status, 400);
|
|
478
|
+
assert.ok(perms.filter((x) => x === 'goal.create').length >= 4, 'dismiss is gated like the rest of the review');
|
|
479
|
+
});
|
package/tests/genesis_work.mjs
CHANGED
|
@@ -222,10 +222,81 @@ if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
|
|
222
222
|
|
|
223
223
|
await test('a stage that has not adopted questions still takes a page and a link', async () => {
|
|
224
224
|
const deps = { store: memStore() };
|
|
225
|
-
const r = await gw.setPage('
|
|
225
|
+
const r = await gw.setPage('legalize', 'Credits only to start.', 1, deps);
|
|
226
226
|
assert.equal(r.work.adopted, false);
|
|
227
|
-
assert.equal(r.work.page, '
|
|
228
|
-
assert.equal((await gw.setAnswer('
|
|
227
|
+
assert.equal(r.work.page, 'Credits only to start.');
|
|
228
|
+
assert.equal((await gw.setAnswer('legalize', 'anything', { text: 'x' }, 1, deps)).reason, 'unknown_question');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── the charter stage (task 1005119) ──────────────────────────────────────
|
|
232
|
+
// A stand-in government port that records what it was handed, so the test sees
|
|
233
|
+
// exactly which answers reach the recommender.
|
|
234
|
+
const fakeGov = () => {
|
|
235
|
+
const calls = [];
|
|
236
|
+
return { calls, async charterAdvice(input) { calls.push(input); return { charter: { key: 'monarchy' }, input }; } };
|
|
237
|
+
};
|
|
238
|
+
const charterDeps = (extra = {}) => ({ store: memStore(), government: fakeGov(), getSetting: async () => null, ...extra });
|
|
239
|
+
|
|
240
|
+
await test('charter: a closed answer must be one of its choices, and a label reads back as its key', async () => {
|
|
241
|
+
const deps = charterDeps();
|
|
242
|
+
const bad = await gw.setAnswer('charter', 'final_say', { text: 'my cat' }, 1, deps);
|
|
243
|
+
assert.equal(bad.reason, 'not_a_choice');
|
|
244
|
+
assert.deepEqual(bad.expected, ['founder', 'trusted', 'everyone', 'council']);
|
|
245
|
+
const r = await gw.setAnswer('charter', 'final_say', { text: 'Everyone on the project' }, 1, deps);
|
|
246
|
+
const q = qOf(r, 'final_say');
|
|
247
|
+
assert.equal(q.answer.text, 'everyone', 'stored as the key');
|
|
248
|
+
assert.equal(q.answer.choice_label, 'Everyone on the project');
|
|
249
|
+
assert.ok(q.choices.some((c) => c.key === 'council'), 'the choices ride the view');
|
|
250
|
+
// prose questions take any words
|
|
251
|
+
const p = await gw.setAnswer('charter', 'feel', { text: 'Calm and careful.' }, 1, deps);
|
|
252
|
+
assert.equal(qOf(p, 'feel').answer.text, 'Calm and careful.');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await test('charter: every question is skippable and answerable "not sure yet"', async () => {
|
|
256
|
+
const deps = charterDeps();
|
|
257
|
+
for (const q of gw.QUESTIONS.charter) {
|
|
258
|
+
assert.equal((await gw.setAnswer('charter', q.id, { state: 'not_sure' }, 1, deps)).ok, true, q.id);
|
|
259
|
+
assert.equal((await gw.setAnswer('charter', q.id, { state: 'skipped' }, 1, deps)).ok, true, q.id);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await test('charter: only given answers reach the recommender, closed as keys and prose as words', async () => {
|
|
264
|
+
const gov = fakeGov();
|
|
265
|
+
const deps = charterDeps({ government: gov, getSetting: async () => ({ value: JSON.stringify({ expected_builders: 6 }) }) });
|
|
266
|
+
await gw.setAnswer('charter', 'final_say', { text: 'trusted' }, 1, deps);
|
|
267
|
+
await gw.setAnswer('charter', 'agreement', { state: 'not_sure' }, 1, deps);
|
|
268
|
+
await gw.setAnswer('charter', 'speed', { state: 'skipped' }, 1, deps);
|
|
269
|
+
await gw.setAnswer('charter', 'leave_behaviour', { text: 'Lying about shipped work.' }, 1, deps);
|
|
270
|
+
const r = await gw.readStage('charter', deps);
|
|
271
|
+
const last = gov.calls[gov.calls.length - 1];
|
|
272
|
+
assert.deepEqual(last.answers, { final_say: 'trusted' }, 'not sure yet and skipped are not sent');
|
|
273
|
+
assert.deepEqual(last.prose, { leave_behaviour: 'Lying about shipped work.' });
|
|
274
|
+
assert.equal(last.headcount, 6, "the founder's expected headcount from the plan");
|
|
275
|
+
assert.equal(r.work.advice.charter.key, 'monarchy', 'the advice rides the stage');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await test('charter: no government module, or a failing one, still reads the stage (advice null)', async () => {
|
|
279
|
+
const none = await gw.readStage('charter', charterDeps({ government: null }));
|
|
280
|
+
assert.equal(none.ok, true);
|
|
281
|
+
assert.equal(none.work.advice, null);
|
|
282
|
+
const broken = await gw.readStage('charter', charterDeps({ government: { charterAdvice: async () => { throw new Error('db down'); } } }));
|
|
283
|
+
assert.equal(broken.work.advice, null);
|
|
284
|
+
const other = await gw.readStage('strategize', charterDeps());
|
|
285
|
+
assert.equal(other.work.advice, undefined, 'only the charter stage carries advice');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await test('charter: the assisted brief lists the keys to record for each closed question', async () => {
|
|
289
|
+
const p = gw.assistPrompt('charter');
|
|
290
|
+
assert.match(p, /\[final_say\]/);
|
|
291
|
+
assert.match(p, /Record one of: founder \(Me, the founder\) · trusted/);
|
|
292
|
+
assert.match(p, /\[leave_behaviour\]/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await test('charter: the planning read path shows a closed answer in words, not its key', async () => {
|
|
296
|
+
const deps = charterDeps();
|
|
297
|
+
await gw.setAnswer('charter', 'agreement', { text: 'consent' }, 1, deps);
|
|
298
|
+
const { markdown } = await gw.readContext(deps);
|
|
299
|
+
assert.match(markdown, /How much agreement should a decision need\?\*\*\n It passes unless someone objects/);
|
|
229
300
|
});
|
|
230
301
|
|
|
231
302
|
await test('an empty project gives the planning read path nothing to print', async () => {
|
|
@@ -216,6 +216,7 @@ for (const rel of [
|
|
|
216
216
|
'modules/lifecycle/routes/goals.js',
|
|
217
217
|
'modules/lifecycle/routes/versions.js',
|
|
218
218
|
'modules/lifecycle/routes/project-settings.js',
|
|
219
|
+
'modules/lifecycle/routes/first-read.js',
|
|
219
220
|
'modules/render-deploy/routes/act.js',
|
|
220
221
|
'modules/npm-release/routes/release.js',
|
|
221
222
|
'modules/provisioning/routes/core-upgrade.js',
|
|
@@ -262,6 +263,7 @@ const REQUESTS = {
|
|
|
262
263
|
'POST /goals/reorder': ['POST', '/goals/reorder', {}],
|
|
263
264
|
'POST /work-categories': ['POST', '/work-categories', {}],
|
|
264
265
|
'PUT /versions/focus': ['PUT', '/versions/focus', { version_id: ' ' }],
|
|
266
|
+
'POST /first-read/maps/:id/accept': ['POST', `/first-read/maps/${BAD}/accept`, { all: true }], // task 1005280
|
|
265
267
|
'POST /autonomy/fence': ['POST', '/autonomy/fence', { enabled: 'yes' }],
|
|
266
268
|
'POST /autonomy/fence/goals': ['POST', '/autonomy/fence/goals', { goal_id: 0 }],
|
|
267
269
|
'DELETE /autonomy/fence/goals/:goalId': ['DELETE', `/autonomy/fence/goals/${BAD}`],
|