@bongos/core 1.20.5 → 1.20.7
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 +113 -53
- package/.claude/skills/goal-review/SKILL.md +16 -24
- package/.claude/skills/goal-uat/SKILL.md +84 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +18 -1
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/adr/0183-criteria-close-themselves.md +1 -1
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0351-a-criterion-closes-on-a-uat.md +73 -0
- package/docs/adr/README.md +28 -0
- package/docs/api/openapi.json +385 -5
- package/docs/api-reference.md +14 -4
- package/docs/architecture.md +7 -0
- package/docs/copy-inventory.md +22 -22
- package/docs/copy-registry.json +23 -23
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +5 -1
- package/docs/page-readings.json +3 -3
- package/modules/hall-ui/public/goals-page.js +8 -3
- package/modules/hall-ui/public/tweak-editor.css +4 -7
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/tweak-editor.js +3 -0
- package/modules/lifecycle/criterion-uat-db.js +267 -0
- package/modules/lifecycle/criterion-uat.js +303 -0
- package/modules/lifecycle/db-goals.js +25 -12
- package/modules/lifecycle/done-when.js +84 -17
- package/modules/lifecycle/migrations/lifecycle_015_criterion_uat.sql +73 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/criterion-uat.js +169 -0
- package/modules/lifecycle/routes/done-when.js +25 -1
- package/modules/npm-release/module.json +3 -1
- package/modules/npm-release/routes/task-where.js +18 -1
- package/modules/npm-release/work.js +38 -7
- package/modules/specialities/routes/specialities.js +8 -1
- package/modules/specialities/specialities.js +26 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -0
- package/scripts/gds/cli-lib.js +4 -1
- package/scripts/gds/find-untested.js +371 -0
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/newcomer-floor.templates.json +1 -1
- package/scripts/gds/status.js +14 -3
- package/scripts/gds/uat.js +120 -0
- package/src/bongos/route-rank-check.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/auto_satisfy_criteria.mjs +4 -2
- package/tests/criterion_uat.mjs +467 -0
- package/tests/criterion_uat_routes.mjs +232 -0
- package/tests/find_untested.mjs +137 -0
- package/tests/fitness.mjs +3 -1
- package/tests/goal_achievement.mjs +4 -2
- package/tests/goal_routes.mjs +4 -2
- package/tests/ideator_full_idea_shapes_space_proof.mjs +3 -2
- package/tests/npm_release_where.mjs +18 -2
- package/tests/speciality_session_skills.mjs +150 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// tests/criterion_uat_routes.mjs — task 1004392 / ADR 0351, the HTTP half.
|
|
2
|
+
//
|
|
3
|
+
// tests/criterion_uat.mjs proves the rules and the sign-off transaction. This
|
|
4
|
+
// file proves the ROUTES consult them and say what they decided, through the
|
|
5
|
+
// REAL Express routers and the REAL error envelope (res.fail drops unknown keys,
|
|
6
|
+
// which is exactly the kind of thing only a route-level test catches: a refusal
|
|
7
|
+
// that names the tasks not yet live must still name them on the wire).
|
|
8
|
+
//
|
|
9
|
+
// What it pins:
|
|
10
|
+
// 1. /satisfy is the override now: no reason → refused and NOTHING written;
|
|
11
|
+
// with a reason → an override row is recorded BEFORE the criterion flips.
|
|
12
|
+
// 2. A sign-off refusal reaches the caller with its code, status and details.
|
|
13
|
+
// 3. The recording upload screens type + bytes, stores under a server-minted
|
|
14
|
+
// name, and a sign-off naming a recording that is not on disk is refused.
|
|
15
|
+
// 4. Backend-only runs the criterion-authoring wall, and its refusal is the
|
|
16
|
+
// wall's own words.
|
|
17
|
+
// 5. Each write asks for the permission route-rank-check pins it to.
|
|
18
|
+
//
|
|
19
|
+
// No DB, no network, no session: the data layer is stubbed at the module
|
|
20
|
+
// boundary (the tests/criterion_goal_attach.mjs pattern).
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/criterion_uat_routes.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import http from 'node:http';
|
|
30
|
+
import { makeRunner } from './helpers.mjs';
|
|
31
|
+
|
|
32
|
+
process.env.NODE_ENV = 'test';
|
|
33
|
+
const recDir = mkdtempSync(join(tmpdir(), 'uat-routes-'));
|
|
34
|
+
process.env.UAT_RECORDINGS_DIR = recDir;
|
|
35
|
+
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const express = require('express');
|
|
38
|
+
const api = require('../src/module-api.js');
|
|
39
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
40
|
+
const { test, summary } = makeRunner();
|
|
41
|
+
|
|
42
|
+
// ---- the world ---------------------------------------------------------------
|
|
43
|
+
const METIC = { id: 700, rank: 'metic' };
|
|
44
|
+
let actor = METIC;
|
|
45
|
+
const asked = []; // every permission a route asked for, in build order
|
|
46
|
+
const log = []; // what the stubbed data layer was asked to do
|
|
47
|
+
|
|
48
|
+
// A pool whose connect() hands out a recording client, so withTx runs for real
|
|
49
|
+
// and a test can assert that the override's two writes share ONE transaction.
|
|
50
|
+
const txLog = [];
|
|
51
|
+
Object.defineProperty(api, 'pool', {
|
|
52
|
+
configurable: true,
|
|
53
|
+
value: {
|
|
54
|
+
query: async () => ({ rows: [] }),
|
|
55
|
+
connect: async () => ({
|
|
56
|
+
query: async (sql) => { txLog.push(String(sql).trim()); return { rows: [] }; },
|
|
57
|
+
release: () => {},
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
api.requireBuilder = (req, _res, next) => { req.builder = actor; next(); };
|
|
63
|
+
api.requirePermission = (...perms) => {
|
|
64
|
+
asked.push(perms.join('+'));
|
|
65
|
+
return (_req, _res, next) => next();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const db = require('../modules/lifecycle/db.js');
|
|
69
|
+
const doneWhen = require('../modules/lifecycle/done-when.js');
|
|
70
|
+
const uatDb = require('../modules/lifecycle/criterion-uat-db.js');
|
|
71
|
+
|
|
72
|
+
doneWhen.getCriterion = async (id) => (Number(id) === 5 ? { id: 5, version_id: 'V', goal_id: 3, satisfied: false } : null);
|
|
73
|
+
doneWhen.markCriterion = async (id, _task, deps = {}) => { log.push(['mark', Number(id), deps.client || null]); return { id: Number(id), goal_id: null, satisfied: true }; };
|
|
74
|
+
db.achieveGoalIfComplete = async () => ({ achieved: false, goal: null });
|
|
75
|
+
db.getVersion = async (id) => ({ id, status: 'building' });
|
|
76
|
+
db.getGoal = async (id) => ({ id, version_id: 'V', status: 'open', created_by: 999 }); // not the actor's
|
|
77
|
+
db.getGoalMember = async () => null;
|
|
78
|
+
uatDb.recordOverride = async (exec, row) => { log.push(['override', row, exec]); return { id: 1, kind: 'override' }; };
|
|
79
|
+
let signOffResult = null;
|
|
80
|
+
uatDb.signOff = async (args) => { log.push(['signOff', args]); return signOffResult; };
|
|
81
|
+
uatDb.setBackendOnly = async ({ authorize, ...rest }) => {
|
|
82
|
+
log.push(['setBackendOnly', rest]);
|
|
83
|
+
const denied = await authorize({}, { id: rest.criterionId, goal_id: 3, version_id: 'V' });
|
|
84
|
+
return denied ? { ok: false, refusal: denied } : { ok: true, backend_only: rest.backendOnly };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const app = express();
|
|
88
|
+
app.use(attachFail);
|
|
89
|
+
app.use(express.json());
|
|
90
|
+
app.use(require('../modules/lifecycle/routes/done-when.js')());
|
|
91
|
+
app.use(require('../modules/lifecycle/routes/criterion-uat.js')());
|
|
92
|
+
const server = http.createServer(app);
|
|
93
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
94
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
95
|
+
|
|
96
|
+
async function call(method, path, { json, raw, type } = {}) {
|
|
97
|
+
const res = await fetch(`${base}${path}`, {
|
|
98
|
+
method,
|
|
99
|
+
headers: raw ? { 'content-type': type } : { 'content-type': 'application/json' },
|
|
100
|
+
body: raw || (json === undefined ? undefined : JSON.stringify(json)),
|
|
101
|
+
});
|
|
102
|
+
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
103
|
+
}
|
|
104
|
+
const errCode = (body) => (body && body.error && (body.error.code || body.error)) || null;
|
|
105
|
+
const reset = () => { log.length = 0; txLog.length = 0; actor = METIC; signOffResult = null; };
|
|
106
|
+
const mp4 = () => { const b = Buffer.alloc(64); b.write('ftyp', 4, 'latin1'); return b; };
|
|
107
|
+
|
|
108
|
+
// ---- 1. the override --------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
await test('SATISFY: without override_reason it is refused and nothing is written', async () => {
|
|
111
|
+
reset();
|
|
112
|
+
const r = await call('POST', '/done-when/5/satisfy', { json: {} });
|
|
113
|
+
assert.equal(r.status, 400);
|
|
114
|
+
assert.equal(errCode(r.body), 'override_reason_required');
|
|
115
|
+
assert.match(JSON.stringify(r.body), /UAT/, 'the refusal explains that criteria close on a UAT');
|
|
116
|
+
assert.deepEqual(log, [], 'no override row, no flip');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('SATISFY: with a reason, the override row is recorded BEFORE the criterion flips', async () => {
|
|
120
|
+
reset();
|
|
121
|
+
const r = await call('POST', '/done-when/5/satisfy', { json: { override_reason: 'met by the old importer, owner confirmed' } });
|
|
122
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
123
|
+
assert.deepEqual(log.map((e) => e[0]), ['override', 'mark']);
|
|
124
|
+
assert.equal(log[0][1].reason, 'met by the old importer, owner confirmed');
|
|
125
|
+
assert.equal(log[0][1].signerId, METIC.id);
|
|
126
|
+
// ONE transaction: both writes ran on the same client, between BEGIN and COMMIT.
|
|
127
|
+
assert.ok(log[0][2] && log[0][2] === log[1][2], 'the override row and the flip share one transaction client');
|
|
128
|
+
assert.equal(txLog[0], 'BEGIN');
|
|
129
|
+
assert.equal(txLog[txLog.length - 1], 'COMMIT');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---- 2. the sign-off --------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
await test('SIGNOFF: a refusal reaches the caller with its code, status AND details', async () => {
|
|
135
|
+
reset();
|
|
136
|
+
signOffResult = { ok: false, refusal: { code: 'work_not_live', status: 409, message: 'not deployed yet', not_live: [{ task_id: '12', stage: 'not_live' }] } };
|
|
137
|
+
const r = await call('POST', '/done-when/5/uat', { json: { kind: 'backend_signoff', live_attested: true } });
|
|
138
|
+
assert.equal(r.status, 409);
|
|
139
|
+
assert.equal(errCode(r.body), 'work_not_live');
|
|
140
|
+
assert.deepEqual(r.body.error.details, { not_live: [{ task_id: '12', stage: 'not_live' }] }, 'the tasks not yet live must be named on the wire');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await test('SIGNOFF: the route hands signOff the authenticated signer, never a body field', async () => {
|
|
144
|
+
reset();
|
|
145
|
+
signOffResult = { ok: true, signoff: { id: 9 }, closed: { criteria: [{ id: 5 }], goals: [{ id: 3 }] } };
|
|
146
|
+
const r = await call('POST', '/done-when/5/uat', { json: { kind: 'backend_signoff', live_attested: true, note: 'checked' } });
|
|
147
|
+
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
148
|
+
assert.equal(r.body.criterion_closed, true);
|
|
149
|
+
assert.deepEqual(r.body.achieved_goals, [3]);
|
|
150
|
+
const args = log.find((e) => e[0] === 'signOff')[1];
|
|
151
|
+
assert.equal(args.signerId, METIC.id);
|
|
152
|
+
assert.equal(args.liveAttested, true);
|
|
153
|
+
const smuggle = await call('POST', '/done-when/5/uat', { json: { kind: 'uat', signer_id: 1 } });
|
|
154
|
+
assert.equal(smuggle.status, 400, 'an unknown field is refused, so a signer id cannot ride the body');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---- 3. the recording -------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
await test('UPLOAD: a real mp4 is stored under a server-minted name for THIS criterion', async () => {
|
|
160
|
+
reset();
|
|
161
|
+
const r = await call('POST', '/done-when/5/uat/recording', { raw: mp4(), type: 'video/mp4' });
|
|
162
|
+
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
163
|
+
assert.match(r.body.recording, /^uat-5-[0-9a-f]{16}\.mp4$/);
|
|
164
|
+
assert.ok(existsSync(join(recDir, r.body.recording)));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await test('UPLOAD: bytes that are not the claimed video are refused in words', async () => {
|
|
168
|
+
reset();
|
|
169
|
+
const r = await call('POST', '/done-when/5/uat/recording', { raw: Buffer.alloc(64, 1), type: 'video/mp4' });
|
|
170
|
+
assert.equal(r.status, 400);
|
|
171
|
+
assert.equal(errCode(r.body), 'sniff_mismatch');
|
|
172
|
+
const png = await call('POST', '/done-when/5/uat/recording', { raw: Buffer.alloc(64, 1), type: 'image/png' });
|
|
173
|
+
assert.equal(png.status, 400, 'a non-video type never reaches the store');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await test('UPLOAD: an unknown criterion is a 404 and stores nothing', async () => {
|
|
177
|
+
reset();
|
|
178
|
+
const r = await call('POST', '/done-when/6/uat/recording', { raw: mp4(), type: 'video/mp4' });
|
|
179
|
+
assert.equal(r.status, 404);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await test('SIGNOFF: a recording that is not on disk is refused before signOff runs', async () => {
|
|
183
|
+
reset();
|
|
184
|
+
const r = await call('POST', '/done-when/5/uat', { json: { kind: 'uat', recording: 'uat-5-0123456789abcdef.mp4' } });
|
|
185
|
+
assert.equal(r.status, 400);
|
|
186
|
+
assert.equal(errCode(r.body), 'recording_not_found');
|
|
187
|
+
assert.ok(!log.some((e) => e[0] === 'signOff'));
|
|
188
|
+
const bad = await call('POST', '/done-when/5/uat', { json: { kind: 'uat', recording: '../../etc/passwd' } });
|
|
189
|
+
assert.equal(bad.status, 400);
|
|
190
|
+
assert.equal(errCode(bad.body), 'bad_recording');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await test('PLAY: a stored recording is served with its type; a bad name is refused', async () => {
|
|
194
|
+
reset();
|
|
195
|
+
const up = await call('POST', '/done-when/5/uat/recording', { raw: mp4(), type: 'video/mp4' });
|
|
196
|
+
const res = await fetch(`${base}/criterion-uats/recordings/${up.body.recording}`);
|
|
197
|
+
assert.equal(res.status, 200);
|
|
198
|
+
assert.match(res.headers.get('content-type'), /^video\/mp4/);
|
|
199
|
+
assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
|
|
200
|
+
const bad = await fetch(`${base}/criterion-uats/recordings/..%2F..%2Fsecret.mp4`);
|
|
201
|
+
assert.equal(bad.status, 400);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---- 4. backend-only --------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
await test('BACKEND-ONLY: the authoring wall runs, and its refusal is the wall\'s own words', async () => {
|
|
207
|
+
reset();
|
|
208
|
+
const r = await call('PUT', '/done-when/5/backend-only', { json: { backend_only: true } });
|
|
209
|
+
assert.equal(r.status, 403);
|
|
210
|
+
assert.equal(errCode(r.body), 'goal_not_yours', 'a Metic who does not own the goal may not decide whether it needs a recording');
|
|
211
|
+
actor = { id: 900, rank: 'archon' };
|
|
212
|
+
const ok = await call('PUT', '/done-when/5/backend-only', { json: { backend_only: true } });
|
|
213
|
+
assert.equal(ok.status, 200, JSON.stringify(ok.body));
|
|
214
|
+
assert.equal(ok.body.backend_only, true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ---- 5. the permissions each write asks for --------------------------------
|
|
218
|
+
|
|
219
|
+
await test('GATES: the writes ask for exactly the atoms route-rank-check pins', () => {
|
|
220
|
+
const { MODULE_EXPECTED_RANKS } = require('../src/bongos/route-rank-check.js');
|
|
221
|
+
const pins = MODULE_EXPECTED_RANKS.lifecycle;
|
|
222
|
+
assert.equal(pins['POST /done-when/:criterionId/uat/recording'], 'perm:criterion.review');
|
|
223
|
+
assert.equal(pins['POST /done-when/:criterionId/uat'], 'perm:criterion.review');
|
|
224
|
+
assert.equal(pins['PUT /done-when/:criterionId/backend-only'], 'perm:criterion.create');
|
|
225
|
+
// criterion-uat.js is built after done-when.js; its three gated routes asked, in
|
|
226
|
+
// order, for review · review · create.
|
|
227
|
+
assert.deepEqual(asked.slice(-3), ['criterion.review', 'criterion.review', 'criterion.create']);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
server.close();
|
|
231
|
+
rmSync(recDir, { recursive: true, force: true });
|
|
232
|
+
summary();
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// tests/find_untested.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit test for scripts/gds/find-untested.js (task 1002458) — the finder the newcomer
|
|
4
|
+
// "add a unit test" chore runs instead of a hardcoded target list. The cases pin the two
|
|
5
|
+
// failures that made the naive scans unusable, measured before it was built:
|
|
6
|
+
// - BASENAME matching falsely marks a common-word module (settings.js) as tested because
|
|
7
|
+
// some unrelated test mentions a different settings.js;
|
|
8
|
+
// - LITERAL-require-only scanning misses tests that reach a module through
|
|
9
|
+
// path.join(ROOT, …), a `const GOV = path.join(…)` prefix, or a spawned script — which is
|
|
10
|
+
// how it once flagged the well-tested memory-map.js as untested.
|
|
11
|
+
// Plus the purity heuristic, the ranking, and one pass over the live tree. Run:
|
|
12
|
+
// node tests/find_untested.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const fu = require('../scripts/gds/find-untested.js');
|
|
19
|
+
|
|
20
|
+
let passed = 0;
|
|
21
|
+
let failed = 0;
|
|
22
|
+
function test(name, fn) {
|
|
23
|
+
try { fn(); passed++; console.log(` ok ${name}`); } catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MODS = new Set([
|
|
27
|
+
'modules/builder-settings/settings.js',
|
|
28
|
+
'modules/hall-ui/settings.js',
|
|
29
|
+
'modules/government/db.js',
|
|
30
|
+
'modules/government/board.js',
|
|
31
|
+
'scripts/gds/memory-map.js',
|
|
32
|
+
'scripts/gds/grade-smoke.js',
|
|
33
|
+
'modules/lifecycle/index.js',
|
|
34
|
+
]);
|
|
35
|
+
const hits = (src, file) => [...fu.extractPathRefs(src, file)].map((r) => fu.resolveRef(r, MODS)).filter(Boolean).sort();
|
|
36
|
+
|
|
37
|
+
test('a basename alone never resolves to a module (the common-word false drop)', () => {
|
|
38
|
+
assert.deepEqual(hits("const s = require('./settings.js'); // settings.js", 'tests/x.mjs'), []);
|
|
39
|
+
assert.deepEqual(hits("import x from 'settings.js'", 'tests/x.mjs'), []);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a full or relative path resolves to exactly that module, not its namesake', () => {
|
|
43
|
+
assert.deepEqual(hits("require('../modules/hall-ui/settings.js')", 'tests/x.mjs'), ['modules/hall-ui/settings.js']);
|
|
44
|
+
assert.deepEqual(hits("require('modules/builder-settings/settings')", 'tests/x.mjs'), ['modules/builder-settings/settings.js']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('path.join(ROOT, …) segments, and a const prefix, are evaluated (the memory-map miss)', () => {
|
|
48
|
+
const src = [
|
|
49
|
+
"const ROOT = path.resolve(__dirname, '..');",
|
|
50
|
+
"const GOV = path.join(ROOT, 'modules', 'government');",
|
|
51
|
+
"const db = require(path.join(GOV, 'db.js'));",
|
|
52
|
+
"const mm = require(path.join(ROOT, 'scripts', 'gds', 'memory-map.js'));",
|
|
53
|
+
].join('\n');
|
|
54
|
+
assert.deepEqual(hits(src, 'tests/x.mjs'), ['modules/government/db.js', 'scripts/gds/memory-map.js']);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('an unknown leading identifier stands for the repo root; one in the middle does not', () => {
|
|
58
|
+
assert.deepEqual(hits("path.join(REPO, 'modules/government/board.js')", 'tests/x.mjs'), ['modules/government/board.js']);
|
|
59
|
+
assert.deepEqual(hits("path.join('modules', someVar, 'board.js')", 'tests/x.mjs'), []);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a spawned script, new URL(…, import.meta.url) and a directory index all count', () => {
|
|
63
|
+
assert.deepEqual(hits("spawnSync(process.execPath, ['scripts/gds/grade-smoke.js', '--dry'])", 'tests/x.mjs'), ['scripts/gds/grade-smoke.js']);
|
|
64
|
+
assert.deepEqual(hits("await import(new URL('../scripts/gds/memory-map.js', import.meta.url))", 'tests/x.mjs'), ['scripts/gds/memory-map.js']);
|
|
65
|
+
assert.deepEqual(hits("require('../modules/lifecycle')", 'tests/x.mjs'), ['modules/lifecycle/index.js']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('template literals with interpolation and URLs are ignored, not mis-resolved', () => {
|
|
69
|
+
assert.deepEqual(hits('require(`../modules/${k}/settings.js`)', 'tests/x.mjs'), []);
|
|
70
|
+
assert.deepEqual(hits("fetch('https://example.com/scripts/gds/memory-map.js')", 'tests/x.mjs'), []);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('extractRequires separates packages from local edges and ignores comment mentions', () => {
|
|
74
|
+
const r = fu.extractRequires("const pg = require('pg');\nconst fs = require('node:fs');\nconst b = require('./board');\n// see ./db.js", 'modules/government/x.js');
|
|
75
|
+
assert.deepEqual([...r.packages].sort(), ['fs', 'pg']);
|
|
76
|
+
assert.deepEqual([...r.local], ['modules/government/board']);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('ownImpurity: pg, a db module, child_process and fetch are hard; fs is soft', () => {
|
|
80
|
+
const imp = (src, file = 'modules/a/x.js') => fu.ownImpurity(src, fu.extractRequires(src, file));
|
|
81
|
+
assert.deepEqual([...imp("require('pg')").hard], ['postgres']);
|
|
82
|
+
assert.deepEqual([...imp("require('./db-claims.js')").hard], ['postgres']);
|
|
83
|
+
assert.deepEqual([...imp("require('node:child_process')").hard], ['subprocess']);
|
|
84
|
+
assert.deepEqual([...imp('const r = await fetch(url)').hard], ['network']);
|
|
85
|
+
const f = imp("require('fs')");
|
|
86
|
+
assert.equal(f.hard.size, 0);
|
|
87
|
+
assert.equal(f.fs, true);
|
|
88
|
+
// A name that merely contains "db" is not a database module.
|
|
89
|
+
assert.equal(imp("require('./feedback.js')").hard.size, 0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('isTestPath covers every test root and *.test.js siblings, and nothing else', () => {
|
|
93
|
+
assert.equal(fu.isTestPath('tests/a.mjs'), true);
|
|
94
|
+
assert.equal(fu.isTestPath('modules/lifecycle/tests/a.mjs'), true);
|
|
95
|
+
assert.equal(fu.isTestPath('scripts/gds/newcomer-floor.test.js'), true);
|
|
96
|
+
assert.equal(fu.isTestPath('scripts/gds/latest.js'), false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('hasExports / countExports read CommonJS and ESM', () => {
|
|
100
|
+
assert.equal(fu.hasExports('function a(){}'), false);
|
|
101
|
+
assert.equal(fu.countExports('module.exports = {\n a,\n b, // note\n c: 1,\n};'), 3);
|
|
102
|
+
assert.equal(fu.countExports('exports.x = 1; module.exports.y = 2;'), 2);
|
|
103
|
+
assert.equal(fu.countExports('export function f(){}\nexport const g = 1;'), 2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('normRel clamps a climb above the root instead of escaping it', () => {
|
|
107
|
+
assert.equal(fu.normRel('tests/../../../scripts/gds/x.js'), 'scripts/gds/x.js');
|
|
108
|
+
assert.equal(fu.normRel('./a//b/./c'), 'a/b/c');
|
|
109
|
+
assert.equal(fu.normRel('..'), '');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('rankCandidates: unreachable first, then no-fs, then smallest', () => {
|
|
113
|
+
const rows = [
|
|
114
|
+
{ path: 'c', reach: 'indirect', fs: false, lines: 10 },
|
|
115
|
+
{ path: 'b', reach: 'unreachable', fs: true, lines: 500 },
|
|
116
|
+
{ path: 'a', reach: 'indirect', fs: true, lines: 5 },
|
|
117
|
+
{ path: 'd', reach: 'indirect', fs: false, lines: 90 },
|
|
118
|
+
];
|
|
119
|
+
assert.deepEqual(fu.rankCandidates(rows).map((r) => r.path), ['b', 'c', 'd', 'a']);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('the live tree: tested modules the naive scans got wrong are NOT candidates', () => {
|
|
123
|
+
const rows = fu.scanTree();
|
|
124
|
+
const by = new Map(rows.map((r) => [r.path, r]));
|
|
125
|
+
// A scan that finds nothing looks exactly like a clean tree, so assert it found a lot.
|
|
126
|
+
assert.ok(rows.length > 300, `expected hundreds of exported modules, scanned ${rows.length}`);
|
|
127
|
+
for (const p of ['scripts/gds/memory-map.js', 'modules/grading/grader-workers/worker-verdict.js', 'scripts/gds/find-untested.js']) {
|
|
128
|
+
assert.ok(by.has(p), `${p} should be scanned`);
|
|
129
|
+
assert.equal(by.get(p).tested, true, `${p} has a test naming it`);
|
|
130
|
+
}
|
|
131
|
+
const untested = rows.filter((r) => !r.tested);
|
|
132
|
+
assert.ok(untested.length > 0 && untested.length < rows.length / 2,
|
|
133
|
+
`untested should be a minority, got ${untested.length} of ${rows.length}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log(`\nfind-untested (task 1002458) tests: ${passed} passed, ${failed} failed`);
|
|
137
|
+
process.exit(failed > 0 ? 1 : 0);
|
package/tests/fitness.mjs
CHANGED
|
@@ -928,7 +928,9 @@ test('write-route validation exemptions are the known dynamic-key prefs routes,
|
|
|
928
928
|
// own parser and read by a checker that refuses anything malformed (docx.readUpload,
|
|
929
929
|
// module-artifact.verifyModuleArtifact). Named here so each raw-body exemption is a
|
|
930
930
|
// reviewed diff.
|
|
931
|
-
|
|
931
|
+
// The UAT recording (task 1004392) is the third: raw mp4/webm bytes, read by
|
|
932
|
+
// criterion-uat.screenRecording.
|
|
933
|
+
const RAW_FILE_UPLOADS = ['POST /copy-desk/pages/:pageId/draft.docx', 'POST /store/modules/:key/versions', 'POST /done-when/:criterionId/uat/recording'];
|
|
932
934
|
const exempt = [...fitness.WRITE_VALIDATION_EXEMPT];
|
|
933
935
|
assert.ok(exempt.length >= 1 && exempt.every((e) => e.startsWith('PATCH /me/') || RAW_FILE_UPLOADS.includes(e)),
|
|
934
936
|
'exemptions are all /me/* prefs patches or a named raw-file upload: ' + exempt.join(', '));
|
|
@@ -118,14 +118,16 @@ await test('achieveGoalIfComplete: unknown goal id → { achieved:false, goal:nu
|
|
|
118
118
|
// =========================================================================
|
|
119
119
|
const PENDING_ROW = {
|
|
120
120
|
id: 10, version_id: 'BONGOS-V1', criterion_id: 'c-one', criterion_md: 'md',
|
|
121
|
-
goal_id: 7, sort_order: 1, task_total: 2, goal_title: 'Goal hierarchy',
|
|
121
|
+
goal_id: 7, sort_order: 1, task_total: 2, shipped_total: 2, goal_title: 'Goal hierarchy',
|
|
122
122
|
goal_status: 'open', is_last_in_goal: true,
|
|
123
123
|
};
|
|
124
124
|
|
|
125
125
|
await test('listPendingReviewCriteria: returns the queue rows; no version → param [null]', async () => {
|
|
126
126
|
const pool = fakePool((s) => (/WITH pending AS/.test(s) ? { rows: [PENDING_ROW] } : null));
|
|
127
127
|
const out = await listPendingReviewCriteria({}, { pool });
|
|
128
|
-
|
|
128
|
+
// task 1004392: each row carries its UAT state; delivered work with no sign-off
|
|
129
|
+
// is Awaiting UAT (the /goal-uat queue).
|
|
130
|
+
assert.deepEqual(out, [{ ...PENDING_ROW, backend_only: false, uat_state: 'awaiting_uat', awaiting_uat: true }]);
|
|
129
131
|
const q = find(pool.queries, /WITH pending AS/);
|
|
130
132
|
assert.deepEqual(q.params, [null], 'an unscoped call binds versionId=null (no filter)');
|
|
131
133
|
});
|
package/tests/goal_routes.mjs
CHANGED
|
@@ -144,7 +144,8 @@ await test('builder_id coercion contract: Number() accepts int + numeric string,
|
|
|
144
144
|
// task 1002164: the projection now carries the per-criterion link counts and the
|
|
145
145
|
// DERIVED pending_review flag, so the hall's goal-detail page can render the
|
|
146
146
|
// third criterion state (met — ready to confirm) without a second round-trip.
|
|
147
|
-
|
|
147
|
+
// task 1004392 aliased the table (d) to read each criterion's UAT columns.
|
|
148
|
+
const CRIT_SQL_RE = /FROM done_when_criteria d WHERE d\.goal_id = \$1/;
|
|
148
149
|
const critRow = (over = {}) => ({
|
|
149
150
|
id: 1, version_id: 'V1', criterion_id: 'c1', criterion_md: 'md', satisfied: false,
|
|
150
151
|
sort_order: 6, goal_id: 3, linked_tasks: 0, unshipped_tasks: 0, ...over,
|
|
@@ -155,7 +156,8 @@ await test('listCriteriaForGoal: SELECT by goal_id, ordered, returns rows', asyn
|
|
|
155
156
|
const ROW = critRow();
|
|
156
157
|
const pool = critPool([ROW]);
|
|
157
158
|
const out = await listCriteriaForGoal(3, { pool });
|
|
158
|
-
|
|
159
|
+
// task 1004392: every row also carries its UAT state (no linked work → open).
|
|
160
|
+
assert.deepEqual(out, [{ ...ROW, backend_only: false, pending_review: false, uat_state: 'open', awaiting_uat: false }]);
|
|
159
161
|
const sel = find(pool.queries, CRIT_SQL_RE);
|
|
160
162
|
assert.equal(sel.params[0], 3);
|
|
161
163
|
assert.match(sel.sql, /ORDER BY sort_order, id/);
|
|
@@ -216,11 +216,12 @@ function answer(sql, params) {
|
|
|
216
216
|
// was answering a question the read had stopped asking.
|
|
217
217
|
return { rows: g ? [project(g, s)] : [] };
|
|
218
218
|
}
|
|
219
|
-
|
|
219
|
+
// task 1004392: the read aliases the table (d) and carries the UAT columns.
|
|
220
|
+
if (/WITH crit AS \( SELECT d\.id, d\.version_id, d\.criterion_id, d\.criterion_md, d\.satisfied, d\.sort_order, d\.goal_id, .*FROM done_when_criteria d WHERE d\.goal_id = \$1/i.test(s)) {
|
|
220
221
|
const rows = db.criteria
|
|
221
222
|
.filter((c) => c.goal_id === String(params[0]))
|
|
222
223
|
.sort((a, b) => a.sort_order - b.sort_order || String(a.id).localeCompare(String(b.id)))
|
|
223
|
-
.map((c) => ({ ...c, linked_tasks: 0, unshipped_tasks: 0 }));
|
|
224
|
+
.map((c) => ({ ...c, uat_kind: null, backend_only: false, linked_tasks: 0, unshipped_tasks: 0, shipped_tasks: 0 }));
|
|
224
225
|
return { rows };
|
|
225
226
|
}
|
|
226
227
|
|
|
@@ -22,7 +22,7 @@ import { npmTarball, npmPackument, fakeHttpsGet } from './helpers.mjs';
|
|
|
22
22
|
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
-
const { readTaskWhere } = require('../modules/npm-release/work.js');
|
|
25
|
+
const { readTaskWhere, readTasksWhere } = require('../modules/npm-release/work.js');
|
|
26
26
|
const buildRouter = require('../modules/npm-release/routes/task-where.js');
|
|
27
27
|
|
|
28
28
|
const ORIGIN = 'https://registry.npmjs.org';
|
|
@@ -61,6 +61,9 @@ function fakePool() {
|
|
|
61
61
|
const t = TASKS[params[0]];
|
|
62
62
|
return { rows: t ? [{ id: params[0], ...t }] : [] };
|
|
63
63
|
}
|
|
64
|
+
if (/FROM tasks WHERE id = ANY\(\$1::bigint\[\]\)/.test(sql)) {
|
|
65
|
+
return { rows: params[0].filter((id) => TASKS[id]).map((id) => ({ id, ...TASKS[id] })) };
|
|
66
|
+
}
|
|
64
67
|
throw new Error(`unexpected query: ${sql}`);
|
|
65
68
|
},
|
|
66
69
|
};
|
|
@@ -77,7 +80,7 @@ function setup({ latest = '1.0.1', pkStatus = 200 } = {}) {
|
|
|
77
80
|
const pool = fakePool();
|
|
78
81
|
const store = freshStore();
|
|
79
82
|
const read = (id, over = {}) => readTaskWhere({ pool, id, pkg: PKG, live: '1.0.2', get: fg.get, store, installedNotes: () => null, now: () => Date.parse('2026-09-26T13:00:00Z'), ...over });
|
|
80
|
-
return { read, pool, asked: fg.asked };
|
|
83
|
+
return { read, pool, asked: fg.asked, get: fg.get };
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
// ---- the reading ---------------------------------------------------------------------
|
|
@@ -135,6 +138,19 @@ test('the offered flag follows the deploy page: published before this hall ran i
|
|
|
135
138
|
assert.equal(w.offered, true);
|
|
136
139
|
});
|
|
137
140
|
|
|
141
|
+
test('the batch reading (task 1004392) places many tasks with ONE query and ONE versions read, and agrees with the single reading', async () => {
|
|
142
|
+
const single = setup();
|
|
143
|
+
const batch = setup();
|
|
144
|
+
const ids = [11, 12, 13, 30, 999];
|
|
145
|
+
const out = await readTasksWhere({ pool: batch.pool, ids, pkg: PKG, live: '1.0.2', get: (...a) => batch.get(...a), store: freshStore(), installedNotes: () => null, now: () => Date.parse('2026-09-26T13:00:00Z') }).catch((e) => { throw e; });
|
|
146
|
+
assert.equal(batch.pool.queries.length, 1, 'one task query for every id');
|
|
147
|
+
assert.deepEqual(batch.asked.map(String).sort(), [`${ORIGIN}/@bongos%2fcore`, `${ORIGIN}/@bongos/core/-/core-1.0.4.tgz`].sort(),
|
|
148
|
+
'the packument and the notes are fetched once for all three shipped tasks');
|
|
149
|
+
for (const id of [11, 12, 13, 30]) assert.deepEqual(out[String(id)], await single.read(id), 'task ' + id);
|
|
150
|
+
assert.ok(!('999' in out), 'an unknown id is absent, not an error');
|
|
151
|
+
assert.deepEqual(await readTasksWhere({ pool: batch.pool, ids: ['1; DROP', ''] }), {}, 'bad ids are never queried');
|
|
152
|
+
});
|
|
153
|
+
|
|
138
154
|
// ---- the route -----------------------------------------------------------------------
|
|
139
155
|
|
|
140
156
|
function layers(read) {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// tests/speciality_session_skills.mjs — a builder's ENABLED skills reach their
|
|
2
|
+
// session through the existing injection path (task 1004074, part 3 of 3 of
|
|
3
|
+
// ADR 0310).
|
|
4
|
+
//
|
|
5
|
+
// The whole path, not its middle: the adoption row as db.listAdoptions returns
|
|
6
|
+
// it → the real kernel port (describeActiveFor, registered by the real router
|
|
7
|
+
// factory) → the real Conductor's evaluateTriggers. A hand-built contract fed
|
|
8
|
+
// straight to the Conductor would stay green if the port stopped reading
|
|
9
|
+
// enabled_skills, so the port is always in the loop here.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/speciality_session_skills.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { makeRunner } from './helpers.mjs';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const S = require('../modules/specialities/specialities.js');
|
|
23
|
+
const skills = require('../modules/specialities/skills.js');
|
|
24
|
+
const db = require('../modules/specialities/db.js');
|
|
25
|
+
const api = require('../src/module-api.js');
|
|
26
|
+
const conductor = require(path.join(ROOT, '.claude', 'hooks', 'conductor.js'));
|
|
27
|
+
const { test, summary } = makeRunner();
|
|
28
|
+
|
|
29
|
+
const LEAN = /chose to lean on these skills/;
|
|
30
|
+
|
|
31
|
+
// ---- the pure line ----------------------------------------------------------
|
|
32
|
+
const row = (over = {}) => ({ id: 9, slug: 'economics', name: 'Economics', discipline: 'ideator', summary: '', contract_md: '', skills: ['recall', 'status'], enabled_skills: [], active: true, ...over });
|
|
33
|
+
|
|
34
|
+
await test('enabled skills are named in the contract', () => {
|
|
35
|
+
const d = S.describeForApi(row({ enabled_skills: ['recall', 'status'] }));
|
|
36
|
+
assert.match(d.contract, LEAN);
|
|
37
|
+
assert.match(d.contract, /\/recall, \/status/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await test('nothing enabled says nothing — silent, not an empty list', () => {
|
|
41
|
+
const none = S.describeForApi(row({ enabled_skills: [] }));
|
|
42
|
+
const absent = S.describeForApi(row({ enabled_skills: undefined }));
|
|
43
|
+
const baseline = S.describeForApi(row({ skills: [], enabled_skills: [] }));
|
|
44
|
+
assert.doesNotMatch(none.contract, LEAN);
|
|
45
|
+
assert.equal(none.contract, baseline.contract, 'an adoption with nothing on reads exactly like one with no skills at all');
|
|
46
|
+
assert.equal(absent.contract, baseline.contract);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await test('a skill no longer offered, or no longer installed, is not named', () => {
|
|
50
|
+
const d = S.describeForApi(row({ skills: ['recall'], enabled_skills: ['recall', 'status'] }));
|
|
51
|
+
assert.match(d.contract, /\/recall\./);
|
|
52
|
+
assert.doesNotMatch(d.contract, /\/status/, 'the author dropped it from the offer');
|
|
53
|
+
const gone = S.describeForApi(row({ enabled_skills: ['recall', 'status'] }), { installed: new Map([['status', {}]]) });
|
|
54
|
+
assert.match(gone.contract, /\/status\./);
|
|
55
|
+
assert.doesNotMatch(gone.contract, /\/recall/, 'a deploy removed it');
|
|
56
|
+
const allGone = S.describeForApi(row({ enabled_skills: ['recall'] }), { installed: new Map() });
|
|
57
|
+
assert.doesNotMatch(allGone.contract, LEAN, 'filtered to nothing is silent too');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await test('the line is advice and says so — it grants nothing', () => {
|
|
61
|
+
const d = S.describeForApi(row({ enabled_skills: ['recall'] }));
|
|
62
|
+
assert.match(d.contract, /advice, not permission/);
|
|
63
|
+
assert.match(d.contract, /only if the builder's rank allows it/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await test('the skills line sits AFTER the provenance fence — it is the adopter\'s choice, not the author\'s prose', () => {
|
|
67
|
+
const d = S.describeForApi(row({ contract_md: 'Speak in elasticities.', enabled_skills: ['recall'] }), { selfAuthored: false, authorLabel: 'Ada' });
|
|
68
|
+
const lines = d.contract.split('\n');
|
|
69
|
+
assert.match(lines[0], /stated working preferences of Ada/);
|
|
70
|
+
assert.match(lines[lines.length - 1], LEAN);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await test('a malformed name in the stored set is never relayed', () => {
|
|
74
|
+
const d = S.describeForApi(row({ skills: ['recall', 'Bad Name; rm -rf'], enabled_skills: ['recall', 'Bad Name; rm -rf'] }));
|
|
75
|
+
assert.doesNotMatch(d.contract, /rm -rf/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---- the whole path: row → real port → real Conductor -----------------------
|
|
79
|
+
const store = { adoptions: [] };
|
|
80
|
+
db.listAdoptions = async () => store.adoptions.map((a) => ({ ...a }));
|
|
81
|
+
db.countTraining = async () => ({ documentCount: 0, learningCount: 0 });
|
|
82
|
+
db.getSpeciality = async (id) => ({ id, owner_kind: 'builder', owner_builder_id: 1 });
|
|
83
|
+
skills.installedSkillsCached = () => new Map([['recall', {}], ['status', {}]]);
|
|
84
|
+
require('../modules/specialities/routes/specialities.js')();
|
|
85
|
+
const port = api.resolveOptional('specialities');
|
|
86
|
+
const claim = (discipline) => ({ task_id: '1', title: 't', discipline });
|
|
87
|
+
|
|
88
|
+
async function sessionFor(adoptions, claims) {
|
|
89
|
+
store.adoptions = adoptions;
|
|
90
|
+
const specialities = await port.describeActiveFor(1);
|
|
91
|
+
return conductor.evaluateTriggers({ activeClaims: claims, specialities });
|
|
92
|
+
}
|
|
93
|
+
const specEntries = (out) => out.filter((a) => String(a.id).startsWith('speciality:'));
|
|
94
|
+
|
|
95
|
+
await test('the port is the one the router registered', () => {
|
|
96
|
+
assert.equal(typeof port.describeActiveFor, 'function');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await test('an adoption with enabled skills reaches the session in its craft', async () => {
|
|
100
|
+
const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('ideator')]);
|
|
101
|
+
const sp = specEntries(out);
|
|
102
|
+
assert.equal(sp.length, 1);
|
|
103
|
+
assert.match(sp[0].text, /Economics/);
|
|
104
|
+
assert.match(sp[0].text, /\/recall/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await test('with nothing enabled the session gets the contract and not a word more', async () => {
|
|
108
|
+
const out = await sessionFor([row({ enabled_skills: [] })], [claim('ideator')]);
|
|
109
|
+
const sp = specEntries(out);
|
|
110
|
+
assert.equal(sp.length, 1, 'the speciality itself is still there');
|
|
111
|
+
assert.doesNotMatch(sp[0].text, LEAN);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await test('craft-scoped like the contract: an engineering session hears nothing of an ideator\'s skills', async () => {
|
|
115
|
+
const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('engineer')]);
|
|
116
|
+
assert.equal(specEntries(out).length, 0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('an inactive adoption relays nothing, skills included', async () => {
|
|
120
|
+
const out = await sessionFor([row({ active: false, enabled_skills: ['recall'] })], [claim('ideator')]);
|
|
121
|
+
assert.equal(specEntries(out).length, 0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await test('a failed skills read degrades to the offered filter, never a lost contract', async () => {
|
|
125
|
+
const saved = skills.installedSkillsCached;
|
|
126
|
+
skills.installedSkillsCached = () => { throw new Error('disk gone'); };
|
|
127
|
+
try {
|
|
128
|
+
const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('ideator')]);
|
|
129
|
+
assert.match(specEntries(out)[0].text, /\/recall/);
|
|
130
|
+
} finally { skills.installedSkillsCached = saved; }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await test('the real adoption read selects both sets the line needs', () => {
|
|
134
|
+
// listAdoptions is stubbed above, so pin the SQL it stands in for: drop either
|
|
135
|
+
// column and the line goes silent for everyone, with nothing erroring.
|
|
136
|
+
const sql = fs.readFileSync(path.join(ROOT, 'modules/specialities/db.js'), 'utf8');
|
|
137
|
+
const read = sql.slice(sql.indexOf('async function listAdoptions'), sql.indexOf('async function taskDiscipline'));
|
|
138
|
+
assert.match(read, /s\.skills/);
|
|
139
|
+
assert.match(read, /a\.enabled_skills/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---- grants nothing: nothing that decides what runs reads the enabled set ----
|
|
143
|
+
await test('no permission or materialisation surface reads enabled_skills', () => {
|
|
144
|
+
for (const rel of ['scripts/gds/claude-materialize.js', 'src/bongos/auth.js', 'src/bongos/permission-path-check.js', '.claude/hooks/conductor.js']) {
|
|
145
|
+
const text = fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
146
|
+
assert.doesNotMatch(text, /enabled_skills/, `${rel} must not key anything off the enabled set`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
summary();
|