@bongos/core 1.21.124 → 1.21.126
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 +91 -41
- package/bin/bongos.js +9 -0
- package/docs/api/openapi.json +4 -4
- package/docs/copy-inventory.md +128 -126
- package/docs/copy-registry.json +188 -170
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +152 -143
- package/modules/hall-ui/public/goal-forest-lib.js +36 -0
- package/modules/hall-ui/public/goal-forest-render.js +50 -10
- package/modules/hall-ui/public/goals-page.js +19 -22
- package/modules/hall-ui/public/goals-render.js +6 -16
- package/modules/hall-ui/public/goals.css +13 -4
- package/modules/hall-ui/public/goals.html +22 -24
- package/modules/hall-ui/public/goals.states.json +19 -1
- package/modules/hall-ui/records/goal-dependency-forest.md +4 -3
- package/modules/onboarding/howto-guide.js +259 -0
- package/modules/platform-identity/invite-authz.js +1 -1
- package/modules/provisioning/catalog-bridge.js +1 -1
- package/modules/provisioning/provisioning.js +1 -1
- package/modules/provisioning/route-helpers.js +50 -0
- package/modules/provisioning/routes/disconnect.js +6 -5
- package/modules/provisioning/routes/env-manifest.js +11 -27
- package/modules/provisioning/routes/provisioning.js +58 -75
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -0
- package/scripts/gds/core-pin-sync.js +233 -0
- package/scripts/gds/module-assess-tests.js +40 -11
- package/scripts/gds/module-assess-version.js +10 -2
- package/scripts/gds/module-test-runner/README.md +44 -0
- package/scripts/gds/module-test-runner/module-tests.yml +87 -0
- package/scripts/gds/module-test-runner.js +358 -0
- package/src/module-api.js +1 -1
- package/tests/core_pin_sync.mjs +237 -0
- package/tests/goal_forest_lib.mjs +28 -0
- package/tests/goal_forest_page.mjs +106 -11
- package/tests/howto_guide.mjs +96 -0
- package/tests/module_assess_price_parity.mjs +2 -2
- package/tests/module_assess_score.mjs +13 -3
- package/tests/module_test_runner.mjs +294 -0
- package/tests/project_catalog_routes.mjs +1 -1
- package/tests/provisioning_module_selection.mjs +1 -1
- package/tests/provisioning_project_owner_gate.mjs +271 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// tests/core_pin_sync.mjs — `bongos <verb>` runs the core the checkout pins (task 1004632)
|
|
2
|
+
//
|
|
3
|
+
// The bug: an instance's task worktree has no node_modules of its own, so after a
|
|
4
|
+
// core-update task lands the next task's `bongos ship` resolved the MAIN checkout's
|
|
5
|
+
// older core and failed on a bug the new pin had already fixed. These cases pin the
|
|
6
|
+
// decision (planCoreSync), the pin reader, and the install-then-re-run path with an
|
|
7
|
+
// injected runner — no npm, no network. The last case is end to end on a real temp
|
|
8
|
+
// git repo with a fake installed core.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/core_pin_sync.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const sync = require('../scripts/gds/core-pin-sync.js');
|
|
21
|
+
|
|
22
|
+
let passed = 0;
|
|
23
|
+
let failed = 0;
|
|
24
|
+
function test(name, fn) {
|
|
25
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
26
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CORE = { name: '@bongos/core', version: '1.21.20' };
|
|
30
|
+
|
|
31
|
+
test('versionFromSpec reads a vendored tarball and a registry semver', () => {
|
|
32
|
+
assert.equal(sync.versionFromSpec('file:vendor/bongos-core-1.21.71.tgz'), '1.21.71');
|
|
33
|
+
assert.equal(sync.versionFromSpec('^1.21.71'), '1.21.71');
|
|
34
|
+
assert.equal(sync.versionFromSpec('1.21.71'), '1.21.71');
|
|
35
|
+
assert.equal(sync.versionFromSpec('file:../somewhere'), null);
|
|
36
|
+
assert.equal(sync.versionFromSpec(undefined), null);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('parseSpec: only the shapes the core writes itself are safe to auto-install', () => {
|
|
40
|
+
assert.equal(sync.parseSpec('file:vendor/bongos-core-1.21.71.tgz').safe, true);
|
|
41
|
+
assert.equal(sync.parseSpec('1.21.71').safe, true);
|
|
42
|
+
assert.equal(sync.parseSpec('^1.21.71').safe, false, 'a range is not an exact pin');
|
|
43
|
+
assert.equal(sync.parseSpec('file:../evil/bongos-core-1.21.71.tgz').safe, false);
|
|
44
|
+
assert.equal(sync.parseSpec('git+https://x/y.git').safe, false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('planCoreSync: a pin that has not landed on main refuses — a branch cannot choose what gets installed', () => {
|
|
48
|
+
const p = sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: '9.9.9', specSafe: true, landed: false, env: {} });
|
|
49
|
+
assert.equal(p.action, 'refuse');
|
|
50
|
+
assert.equal(p.reason, 'pin-not-landed');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('planCoreSync: an unrecognised spec refuses with the remedy, never installs', () => {
|
|
54
|
+
const p = sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: '1.21.71', specSafe: false, env: {} });
|
|
55
|
+
assert.equal(p.action, 'refuse');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('installArgs: never runs install scripts; ci with a lockfile, install without', () => {
|
|
59
|
+
assert.deepEqual(sync.installArgs(true).slice(0, 2), ['ci', '--ignore-scripts']);
|
|
60
|
+
assert.deepEqual(sync.installArgs(false).slice(0, 2), ['install', '--ignore-scripts']);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('planCoreSync: the core repo itself is never touched', () => {
|
|
64
|
+
const p = sync.planCoreSync({ runningPkg: { name: 'cloud-bongos', version: '0.0.2' }, root: '/r', pinned: '1.21.71', env: {} });
|
|
65
|
+
assert.equal(p.action, 'none');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('planCoreSync: in sync, no git, no pin → carry on', () => {
|
|
69
|
+
assert.equal(sync.planCoreSync({ runningPkg: { ...CORE, version: '1.21.71' }, root: '/r', pinned: '1.21.71', env: {} }).action, 'none');
|
|
70
|
+
assert.equal(sync.planCoreSync({ runningPkg: CORE, root: null, pinned: '1.21.71', env: {} }).action, 'none');
|
|
71
|
+
assert.equal(sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: null, env: {} }).action, 'none');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('planCoreSync: the task-49 shape (pinned 1.21.71, running 1.21.20) syncs', () => {
|
|
75
|
+
const p = sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: '1.21.71', specSafe: true, landed: true, env: {} });
|
|
76
|
+
assert.equal(p.action, 'sync');
|
|
77
|
+
assert.equal(p.pinned, '1.21.71');
|
|
78
|
+
assert.equal(p.running, '1.21.20');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('planCoreSync: a re-run that is STILL mismatched refuses instead of looping', () => {
|
|
82
|
+
const p = sync.planCoreSync({ runningPkg: CORE, root: '/r', pinned: '1.21.71', env: { [sync.SYNCED_ENV]: '1' } });
|
|
83
|
+
assert.equal(p.action, 'refuse');
|
|
84
|
+
assert.match(sync.remedyLine(p), /npm ci/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function tmpCheckout({ pinSpec, lockVersion }) {
|
|
88
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'core-pin-sync-'));
|
|
89
|
+
fs.writeFileSync(path.join(dir, 'package.json'), '' + JSON.stringify({ name: 'inst', dependencies: { '@bongos/core': pinSpec } }));
|
|
90
|
+
if (lockVersion) {
|
|
91
|
+
fs.writeFileSync(path.join(dir, 'package-lock.json'), JSON.stringify({ packages: { 'node_modules/@bongos/core': { version: lockVersion } } }));
|
|
92
|
+
}
|
|
93
|
+
return dir;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test('pinnedCoreVersion prefers the lockfile, falls back to package.json (BOM tolerated)', () => {
|
|
97
|
+
const a = tmpCheckout({ pinSpec: 'file:vendor/bongos-core-1.21.71.tgz', lockVersion: '1.21.72' });
|
|
98
|
+
assert.equal(sync.pinnedCoreVersion(a), '1.21.72');
|
|
99
|
+
const b = tmpCheckout({ pinSpec: 'file:vendor/bongos-core-1.21.71.tgz' });
|
|
100
|
+
assert.equal(fs.readFileSync(path.join(b, 'package.json'), 'utf8').charCodeAt(0), 0xFEFF, 'fixture really carries a BOM');
|
|
101
|
+
assert.equal(sync.pinnedCoreVersion(b), '1.21.71');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('syncAndRerun: npm ci in the checkout, then re-run the same argv under its own core with the loop guard', () => {
|
|
105
|
+
const root = tmpCheckout({ pinSpec: 'file:vendor/bongos-core-1.21.71.tgz', lockVersion: '1.21.71' });
|
|
106
|
+
const calls = [];
|
|
107
|
+
const run = (cmd, args, opts) => {
|
|
108
|
+
calls.push({ cmd, args, opts });
|
|
109
|
+
if (cmd === 'npm') {
|
|
110
|
+
const d = path.join(root, 'node_modules', '@bongos', 'core', 'bin');
|
|
111
|
+
fs.mkdirSync(d, { recursive: true });
|
|
112
|
+
fs.writeFileSync(path.join(d, 'bongos.js'), '');
|
|
113
|
+
return { status: 0 };
|
|
114
|
+
}
|
|
115
|
+
return { status: 7 };
|
|
116
|
+
};
|
|
117
|
+
const code = sync.syncAndRerun({ root, pinned: '1.21.71', running: '1.21.20' }, { argv: ['node', 'bongos', 'ship', '49'], run, log: () => {}, env: {}, platform: 'linux', npmCli: null });
|
|
118
|
+
assert.equal(code, 7, 'the re-run exit code is passed through');
|
|
119
|
+
assert.equal(calls[0].cmd, 'npm');
|
|
120
|
+
assert.equal(calls[0].args[0], 'ci');
|
|
121
|
+
assert.equal(path.resolve(calls[0].opts.cwd), path.resolve(root));
|
|
122
|
+
assert.equal(calls[1].cmd, process.execPath);
|
|
123
|
+
assert.deepEqual(calls[1].args.slice(1), ['ship', '49']);
|
|
124
|
+
assert.ok(calls[1].args[0].includes(path.join('node_modules', '@bongos', 'core', 'bin', 'bongos.js')));
|
|
125
|
+
assert.equal(calls[1].opts.env[sync.SYNCED_ENV], '1');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('syncAndRerun: a failed install exits 1 with the one-line remedy, and never re-runs', () => {
|
|
129
|
+
const root = tmpCheckout({ pinSpec: '1.21.71' });
|
|
130
|
+
const logs = [];
|
|
131
|
+
const calls = [];
|
|
132
|
+
const run = (cmd, args) => { calls.push(cmd); return { status: 1 }; };
|
|
133
|
+
const code = sync.syncAndRerun({ root, pinned: '1.21.71', running: '1.21.20' }, { argv: ['node', 'bongos', 'ship'], run, log: (s) => logs.push(s), env: {}, platform: 'linux', npmCli: null });
|
|
134
|
+
assert.equal(code, 1);
|
|
135
|
+
assert.deepEqual(calls, ['npm']);
|
|
136
|
+
assert.equal(calls.length, 1);
|
|
137
|
+
assert.ok(logs.some((l) => /Run `npm install` in /.test(l)), 'no lockfile → remedy names npm install: ' + logs.join('\n'));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('syncAndRerun: a node_modules LINK is detached first, never installed through', () => {
|
|
141
|
+
const root = tmpCheckout({ pinSpec: '1.21.71', lockVersion: '1.21.71' });
|
|
142
|
+
const shared = fs.mkdtempSync(path.join(os.tmpdir(), 'core-pin-shared-'));
|
|
143
|
+
fs.writeFileSync(path.join(shared, 'keep.txt'), 'shared');
|
|
144
|
+
fs.symlinkSync(shared, path.join(root, 'node_modules'), 'junction');
|
|
145
|
+
const run = (cmd) => {
|
|
146
|
+
if (cmd === 'npm') {
|
|
147
|
+
assert.ok(!fs.existsSync(path.join(root, 'node_modules')), 'link must be gone before npm runs');
|
|
148
|
+
return { status: 1 };
|
|
149
|
+
}
|
|
150
|
+
return { status: 0 };
|
|
151
|
+
};
|
|
152
|
+
sync.syncAndRerun({ root, pinned: '1.21.71', running: '1.21.20' }, { argv: ['node', 'bongos', 'ship'], run, log: () => {}, env: {}, npmCli: null });
|
|
153
|
+
assert.equal(fs.readFileSync(path.join(shared, 'keep.txt'), 'utf8'), 'shared', 'the shared install is untouched');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
function git(cwd, ...args) {
|
|
157
|
+
const r = spawnSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], { cwd, encoding: 'utf8' });
|
|
158
|
+
assert.equal(r.status, 0, r.stderr);
|
|
159
|
+
return r.stdout;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A real repo: main carries the 1.21.71 pin (the core update landed); the checkout is
|
|
163
|
+
// on a task branch. The running core is the older 1.21.20.
|
|
164
|
+
function landedRepo() {
|
|
165
|
+
const root = tmpCheckout({ pinSpec: 'file:vendor/bongos-core-1.21.71.tgz', lockVersion: '1.21.71' });
|
|
166
|
+
fs.mkdirSync(path.join(root, 'vendor'));
|
|
167
|
+
fs.writeFileSync(path.join(root, 'vendor', 'bongos-core-1.21.71.tgz'), 'tarball-bytes');
|
|
168
|
+
git(root, 'init', '-q', '-b', 'main');
|
|
169
|
+
git(root, 'add', '-A');
|
|
170
|
+
git(root, 'commit', '-qm', 'pin 1.21.71');
|
|
171
|
+
git(root, 'checkout', '-qb', 'task-49');
|
|
172
|
+
const coreRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'core-pin-running-'));
|
|
173
|
+
fs.writeFileSync(path.join(coreRoot, 'package.json'), JSON.stringify(CORE));
|
|
174
|
+
const installs = [];
|
|
175
|
+
const run = (cmd, args, opts) => {
|
|
176
|
+
if (cmd === 'git') return spawnSync(cmd, args, opts);
|
|
177
|
+
if (cmd === 'npm' || String(args[0] || '').endsWith('npm-cli.js')) installs.push(args);
|
|
178
|
+
return { status: 1 };
|
|
179
|
+
};
|
|
180
|
+
return { root, coreRoot, installs, run };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test('ensurePinnedCore end to end: a pin that LANDED on main gets installed', () => {
|
|
184
|
+
const { root, coreRoot, installs, run } = landedRepo();
|
|
185
|
+
assert.equal(sync.pinLanded(root, { run }), true);
|
|
186
|
+
const code = sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: () => {} });
|
|
187
|
+
assert.equal(code, 1, 'the fake install fails, so the remedy exit');
|
|
188
|
+
assert.equal(installs.length, 1, 'npm ran once');
|
|
189
|
+
// in sync → null (carry on), no install
|
|
190
|
+
fs.writeFileSync(path.join(coreRoot, 'package.json'), JSON.stringify({ ...CORE, version: '1.21.71' }));
|
|
191
|
+
installs.length = 0;
|
|
192
|
+
assert.equal(sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: () => {} }), null);
|
|
193
|
+
assert.equal(installs.length, 0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('ensurePinnedCore end to end: a branch-swapped tarball (committed or not) is refused, never installed', () => {
|
|
197
|
+
const { root, coreRoot, installs, run } = landedRepo();
|
|
198
|
+
fs.writeFileSync(path.join(root, 'vendor', 'bongos-core-1.21.71.tgz'), 'attacker-bytes');
|
|
199
|
+
assert.equal(sync.pinLanded(root, { run }), false, 'uncommitted swap');
|
|
200
|
+
git(root, 'commit', '-qam', 'swap');
|
|
201
|
+
assert.equal(sync.pinLanded(root, { run }), false, 'committed on the branch only');
|
|
202
|
+
const logs = [];
|
|
203
|
+
const code = sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: (l) => logs.push(l) });
|
|
204
|
+
assert.equal(code, 1);
|
|
205
|
+
assert.equal(installs.length, 0);
|
|
206
|
+
assert.ok(logs.some((l) => /Run `npm ci`/.test(l)), logs.join('\n'));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('ensurePinnedCore end to end: a branch-only version bump is refused, never installed', () => {
|
|
210
|
+
const { root, coreRoot, installs, run } = landedRepo();
|
|
211
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'inst', dependencies: { '@bongos/core': '9.9.9' } }));
|
|
212
|
+
fs.rmSync(path.join(root, 'package-lock.json'));
|
|
213
|
+
git(root, 'commit', '-qam', 'bump');
|
|
214
|
+
assert.equal(sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: () => {} }), 1);
|
|
215
|
+
assert.equal(installs.length, 0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('ensurePinnedCore end to end: deleting the lockfile or adding an .npmrc on the branch is refused', () => {
|
|
219
|
+
for (const tamper of [
|
|
220
|
+
(root) => fs.rmSync(path.join(root, 'package-lock.json')),
|
|
221
|
+
(root) => fs.writeFileSync(path.join(root, '.npmrc'), 'registry=https://evil.example/\n'),
|
|
222
|
+
]) {
|
|
223
|
+
const { root, coreRoot, installs, run } = landedRepo();
|
|
224
|
+
tamper(root);
|
|
225
|
+
assert.equal(sync.pinLanded(root, { run }), false);
|
|
226
|
+
assert.equal(sync.ensurePinnedCore({ coreRoot, argv: ['node', 'bongos', 'ship'], cwd: root, env: {}, run, log: () => {} }), 1);
|
|
227
|
+
assert.equal(installs.length, 0);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('the dispatcher runs the check for ship only', () => {
|
|
232
|
+
const src = fs.readFileSync(new URL('../bin/bongos.js', import.meta.url), 'utf8');
|
|
233
|
+
assert.match(src, /if \(!COMPILED && verb === 'ship'\) \{\s*const synced = require\('\.\.\/scripts\/gds\/core-pin-sync\.js'\)\.ensurePinnedCore/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
237
|
+
if (failed) process.exit(1);
|
|
@@ -141,6 +141,34 @@ await test('describe states the numbers plainly, and an empty forest says so', (
|
|
|
141
141
|
assert.equal(L.describe(L.analyse({})), 'No task in this goal depends on another task in this goal.');
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
// ---- the band card's summary (task 1004738) ---------------------------------------
|
|
145
|
+
|
|
146
|
+
await test('brief is the short form of describe, and falls back to it for an empty forest', () => {
|
|
147
|
+
const a = L.analyse({ nodes: [1, 2, 3, 4, 5].map((i) => node(i)), edges: [edge(2, 1), edge(3, 2), edge(5, 4)] });
|
|
148
|
+
assert.equal(L.brief(a), '5 tasks linked by dependencies, in 2 chains.');
|
|
149
|
+
assert.equal(L.brief(L.analyse({})), L.describe(L.analyse({})));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await test('shipFirst ranks the open tasks nothing open blocks by how many open tasks wait on them, down the chain', () => {
|
|
153
|
+
// 1 ← 2 ← 3 and 1 ← 4: task 1 unblocks three. 5 ← 6: task 5 unblocks one.
|
|
154
|
+
const a = L.analyse({ nodes: [1, 2, 3, 4, 5, 6].map((i) => node(i)), edges: [edge(2, 1), edge(3, 2), edge(4, 1), edge(6, 5)] });
|
|
155
|
+
assert.deepEqual(L.shipFirst(a, 3).map((r) => [r.id, r.unblocks]), [['1', 3], ['5', 1]],
|
|
156
|
+
'a task still waiting on an open one is never "first"; one that unblocks nothing is left off');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await test('shipFirst: a shipped dependency frees what waits on it, and closed work is never listed or counted', () => {
|
|
160
|
+
const a = L.analyse({
|
|
161
|
+
nodes: [node(1, { status: 'shipped' }), node(2), node(3), node(4, { status: 'abandoned' })],
|
|
162
|
+
edges: [edge(2, 1), edge(3, 2), edge(4, 2)],
|
|
163
|
+
});
|
|
164
|
+
assert.deepEqual(L.shipFirst(a, 3).map((r) => [r.id, r.unblocks]), [['2', 1]], 'task 2 is up next and unblocks only the open task 3');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await test('shipFirst stops at the limit and breaks ties by task id', () => {
|
|
168
|
+
const a = L.analyse({ nodes: [1, 2, 3, 4, 5, 6, 7, 8].map((i) => node(i)), edges: [edge(5, 4), edge(6, 3), edge(7, 2), edge(8, 1)] });
|
|
169
|
+
assert.deepEqual(L.shipFirst(a, 3).map((r) => r.id), ['1', '2', '3']);
|
|
170
|
+
});
|
|
171
|
+
|
|
144
172
|
// ---- purity -----------------------------------------------------------------------
|
|
145
173
|
|
|
146
174
|
await test('the lib is pure: no DOM, no fetch, no cross-goal input', () => {
|
|
@@ -36,10 +36,11 @@ const settle = async () => { for (let i = 0; i < 20; i++) await tick(); };
|
|
|
36
36
|
const t = (id, extra = {}) => ({ task_id: String(id), title: `Task ${id}`, status: 'ready', priority: 3, ...extra });
|
|
37
37
|
const e = (from, to) => ({ from: String(from), to: String(to) });
|
|
38
38
|
|
|
39
|
-
async function boot({ status = 200, fail = false, payload = { nodes: [], edges: [] } } = {}) {
|
|
39
|
+
async function boot({ status = 200, fail = false, payload = { nodes: [], edges: [] }, conflictPairs = null, view = 'dependencies' } = {}) {
|
|
40
40
|
const root = makeEl('div');
|
|
41
41
|
const reg = (id) => { const el = makeEl('div', { id }); root.appendChild(el); return el; };
|
|
42
|
-
['goal-deps-h', 'goal-deps-count', 'goal-deps-lede', 'goal-deps-body'
|
|
42
|
+
['goal-deps-h', 'goal-deps-count', 'goal-deps-lede', 'goal-deps-body', 'goal-deps-back',
|
|
43
|
+
'goal-deps-card-count', 'goal-deps-card-lede', 'goal-deps-card-body', 'goal-deps-card-foot'].forEach(reg);
|
|
43
44
|
|
|
44
45
|
const documentObj = {
|
|
45
46
|
getElementById: (id) => walk(root, (el) => (el.id === id ? el : null)),
|
|
@@ -83,7 +84,7 @@ async function boot({ status = 200, fail = false, payload = { nodes: [], edges:
|
|
|
83
84
|
vm.runInContext(read('bulk-toolbar.js'), sandbox, { filename: 'bulk-toolbar.js' }); // task 1003141
|
|
84
85
|
vm.runInContext(RENDER_JS, sandbox, { filename: 'goals-render.js' });
|
|
85
86
|
|
|
86
|
-
const state = { current: 7, forest: null, forestError: false };
|
|
87
|
+
const state = { current: 7, view, forest: null, forestError: false, conflictPairs };
|
|
87
88
|
const ctx = {
|
|
88
89
|
API: '/api/bongos',
|
|
89
90
|
state,
|
|
@@ -96,7 +97,10 @@ async function boot({ status = 200, fail = false, payload = { nodes: [], edges:
|
|
|
96
97
|
},
|
|
97
98
|
};
|
|
98
99
|
const renderers = windowObj.OTBGoalsRender(ctx);
|
|
99
|
-
|
|
100
|
+
// The band card's renderer is the forest module's own export — goals-render.js
|
|
101
|
+
// reaches it through its private `forestRender`, so the test makes its own.
|
|
102
|
+
const card = windowObj.OTBGoalForestRender(ctx);
|
|
103
|
+
return { $: documentObj.getElementById, requests, state, renderers, card };
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
// ---- the honest states (ADR 0227) ---------------------------------------------
|
|
@@ -217,14 +221,105 @@ await test('renderDetail calls loadDependencyForest, beside loadGoalTasks, for t
|
|
|
217
221
|
assert.match(RENDER_JS, /loadGoalTasks\(g\.id\);\s*\n\s*loadDependencyForest\(g\.id\);/);
|
|
218
222
|
});
|
|
219
223
|
|
|
220
|
-
|
|
224
|
+
// ---- the band card + the full view (task 1004738) ----------------------------
|
|
225
|
+
|
|
226
|
+
const sectionOf = (html, id) => {
|
|
227
|
+
const start = html.indexOf(`<section id="${id}"`);
|
|
228
|
+
return start < 0 ? '' : html.slice(start, html.indexOf('</section>', start));
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
await test('the band\'s third column is the compact Dependencies card, where Conflicts was — no long block before Tasks', () => {
|
|
221
232
|
const html = fs.readFileSync(path.join(HALL, 'goals.html'), 'utf8');
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
assert.
|
|
226
|
-
assert.
|
|
227
|
-
assert.equal(
|
|
233
|
+
const detail = sectionOf(html, 'goal-detail-view');
|
|
234
|
+
const cols = detail.split('class="goal-band__col"').slice(1);
|
|
235
|
+
assert.equal(cols.length, 3, 'still three columns');
|
|
236
|
+
assert.match(cols[2], /^\s*id="goal-deps-card"/, 'Dependencies takes the third slot');
|
|
237
|
+
assert.equal(/>Conflicts</.test(cols.join('')), false, 'no Conflicts column left in the band');
|
|
238
|
+
assert.equal(detail.includes('id="goal-deps-scroll"'), false, 'the forest is off the goal page');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await test('the full picture is #/goal/N/dependencies: the forest, then the whole conflict set; the old /conflicts lands there', () => {
|
|
242
|
+
const html = fs.readFileSync(path.join(HALL, 'goals.html'), 'utf8');
|
|
243
|
+
const view = sectionOf(html, 'goal-dependencies-view');
|
|
244
|
+
const depsAt = view.indexOf('id="goal-deps-scroll"');
|
|
245
|
+
const conflictsAt = view.indexOf('id="goal-conflicts-full"');
|
|
246
|
+
assert.ok(view.includes('id="goal-deps-back"') && depsAt > -1 && depsAt < conflictsAt, 'back link, forest, then conflicts');
|
|
247
|
+
assert.equal(sectionOf(html, 'goal-conflicts-view'), '', 'no separate conflicts view any more');
|
|
248
|
+
const page = read('goals-page.js');
|
|
249
|
+
assert.match(page, /dependencies: '#goal-dependencies-view'/);
|
|
250
|
+
assert.match(page, /\(dependencies\|conflicts\|members\|completion\)/);
|
|
251
|
+
assert.match(page, /mSub\[2\] === 'conflicts' \? 'dependencies'/, 'the inbox\'s /conflicts links keep working');
|
|
252
|
+
assert.match(RENDER_JS, /function renderConflicts\(\) \{[\s\S]*?forestRender\.renderDependencyCard\(\);/, 'the card re-renders when the conflicts land');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await test('the card states the shape, lists what to ship first, counts conflicts, and links to the full view', async () => {
|
|
256
|
+
const h = await boot({
|
|
257
|
+
payload: { nodes: [t(1, { title: 'Base' }), t(2), t(3), t(4, { title: 'Other root' }), t(5)], edges: [e(2, 1), e(3, 2), e(5, 4)] },
|
|
258
|
+
conflictPairs: [{ a: 2, b: 5 }, { a: 3, b: 5 }],
|
|
259
|
+
});
|
|
260
|
+
await h.renderers.loadDependencyForest(7);
|
|
261
|
+
assert.equal(h.$('goal-deps-card-count').hidden, false);
|
|
262
|
+
assert.equal(h.$('goal-deps-card-count').textContent, '5');
|
|
263
|
+
assert.equal(h.$('goal-deps-card-lede').textContent, '5 tasks linked by dependencies, in 2 chains.');
|
|
264
|
+
const body = h.$('goal-deps-card-body');
|
|
265
|
+
const rows = body.querySelectorAll('.gf-brief__row');
|
|
266
|
+
assert.deepEqual(rows.map((r) => r.querySelector('a').getAttribute('href')), ['#/task/1', '#/task/4']);
|
|
267
|
+
assert.deepEqual(rows.map((r) => r.querySelector('.gf-brief__n').textContent), ['unblocks 2', 'unblocks 1']);
|
|
268
|
+
const line = body.querySelector('.gf-brief__conflicts');
|
|
269
|
+
assert.equal(line.textContent, '2 file conflicts between open tasks.');
|
|
270
|
+
assert.match(line.className, /gf-brief__conflicts--live/);
|
|
271
|
+
const foot = h.$('goal-deps-card-foot');
|
|
272
|
+
assert.equal(foot.hidden, false);
|
|
273
|
+
assert.equal(foot.querySelector('a').getAttribute('href'), '#/goal/7/dependencies');
|
|
274
|
+
assert.equal(h.$('goal-deps-back').getAttribute('href'), '#/goal/7');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await test('on the goal page only the card is drawn — the forest waits for its own view, where its titles can be measured', async () => {
|
|
278
|
+
const h = await boot({ view: 'detail', payload: { nodes: [t(1), t(2)], edges: [e(2, 1)] } });
|
|
279
|
+
await h.renderers.loadDependencyForest(7);
|
|
280
|
+
assert.equal(h.$('goal-deps-body').querySelector('svg'), null, 'no forest drawn into the hidden view');
|
|
281
|
+
assert.equal(h.$('goal-deps-body').innerHTML, '');
|
|
282
|
+
assert.equal(h.$('goal-deps-card-lede').textContent, '2 tasks linked by dependencies, in 1 chain.');
|
|
283
|
+
h.state.view = 'dependencies';
|
|
284
|
+
h.renderers.renderDependencyForest();
|
|
285
|
+
assert.ok(h.$('goal-deps-body').querySelector('svg'), 'opening the view draws it');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await test('the card: no conflicts says so, unknown conflicts says nothing, and an empty goal has no door to an empty page', async () => {
|
|
289
|
+
const none = await boot({ payload: { nodes: [], edges: [] }, conflictPairs: [] });
|
|
290
|
+
await none.renderers.loadDependencyForest(7);
|
|
291
|
+
assert.match(none.$('goal-deps-card-lede').textContent, /No task in this goal depends on another/);
|
|
292
|
+
assert.equal(none.$('goal-deps-card-count').hidden, true);
|
|
293
|
+
assert.equal(none.$('goal-deps-card-body').querySelector('.gf-brief__conflicts').textContent, 'No file conflicts between open tasks.');
|
|
294
|
+
assert.equal(none.$('goal-deps-card-foot').hidden, true);
|
|
295
|
+
|
|
296
|
+
const unknown = await boot({ payload: { nodes: [], edges: [] }, conflictPairs: null });
|
|
297
|
+
await unknown.renderers.loadDependencyForest(7);
|
|
298
|
+
assert.equal(unknown.$('goal-deps-card-body').querySelector('.gf-brief__conflicts'), null, 'a failed or pending conflicts read draws no line, not a zero');
|
|
299
|
+
|
|
300
|
+
const onlyConflicts = await boot({ payload: { nodes: [], edges: [] }, conflictPairs: [{ a: 1, b: 2 }] });
|
|
301
|
+
await onlyConflicts.renderers.loadDependencyForest(7);
|
|
302
|
+
assert.equal(onlyConflicts.$('goal-deps-card-foot').hidden, false, 'conflicts alone still earn the full view');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await test('the card: while reading, and after a failed read, it says so — and a late conflicts list re-renders it', async () => {
|
|
306
|
+
const h = await boot({ fail: true });
|
|
307
|
+
const p = h.renderers.loadDependencyForest(7);
|
|
308
|
+
assert.equal(h.$('goal-deps-card-lede').textContent, 'Reading…');
|
|
309
|
+
await p;
|
|
310
|
+
assert.match(h.$('goal-deps-card-lede').textContent, /Couldn.t load this goal.s task dependencies/);
|
|
311
|
+
h.state.conflictPairs = [{ a: 1, b: 2 }];
|
|
312
|
+
h.card.renderDependencyCard();
|
|
313
|
+
assert.equal(h.$('goal-deps-card-body').querySelector('.gf-brief__conflicts').textContent, '1 file conflict between open tasks.');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await test('a "ship first" title is builder-authored text and never becomes markup in the card either', async () => {
|
|
317
|
+
const evil = '<img src=x onerror=alert(1)>';
|
|
318
|
+
const h = await boot({ payload: { nodes: [t(1, { title: evil }), t(2)], edges: [e(2, 1)] } });
|
|
319
|
+
await h.renderers.loadDependencyForest(7);
|
|
320
|
+
const body = h.$('goal-deps-card-body');
|
|
321
|
+
assert.equal(body.querySelector('img'), null);
|
|
322
|
+
assert.ok(body.textContent.includes(evil));
|
|
228
323
|
});
|
|
229
324
|
|
|
230
325
|
summary();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// tests/howto_guide.mjs — the in-session how-to guide's shape + plain-language rule (task 1004627)
|
|
2
|
+
//
|
|
3
|
+
// The guide's step ids are stored as per-person read marks (task 1004634) and matched to
|
|
4
|
+
// moments (task 1004635), so they must be unique, stable-shaped, and every step must
|
|
5
|
+
// belong to a known moment and carry all three parts (what / when / try). It is written
|
|
6
|
+
// for people who may not be technical, so no step may use the accessibility scorer's
|
|
7
|
+
// jargon list (the same list tests/plain_cards.mjs holds the failure cards to).
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/howto_guide.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const guide = require('../modules/onboarding/howto-guide.js');
|
|
16
|
+
const { JARGON_TERMS } = require('../modules/sessions/accessibility.js');
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
function test(name, fn) {
|
|
21
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
22
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('about 25 steps, each with a unique snake_case id', () => {
|
|
26
|
+
assert.ok(guide.STEPS.length >= 20 && guide.STEPS.length <= 32, `got ${guide.STEPS.length}`);
|
|
27
|
+
const ids = guide.STEPS.map((s) => s.id);
|
|
28
|
+
assert.equal(new Set(ids).size, ids.length, 'duplicate step id');
|
|
29
|
+
for (const id of ids) assert.match(id, /^[a-z][a-z0-9_]{2,39}$/, id);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('every step has a known moment and all three parts', () => {
|
|
33
|
+
for (const s of guide.STEPS) {
|
|
34
|
+
assert.ok(guide.MOMENT_IDS.has(s.moment), `${s.id}: unknown moment ${s.moment}`);
|
|
35
|
+
for (const f of ['title', 'what', 'when', 'try']) assert.ok(String(s[f] || '').trim().length > 5, `${s.id}.${f} empty`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('every moment has at least one step and a trigger', () => {
|
|
40
|
+
for (const m of guide.MOMENTS) {
|
|
41
|
+
assert.ok(guide.stepsForMoment(m.id).length > 0, `${m.id} has no steps`);
|
|
42
|
+
assert.ok(m.trigger, `${m.id} has no trigger`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('the moments the owner named are covered', () => {
|
|
47
|
+
for (const m of ['first_open', 'setup', 'goal', 'task', 'find_work', 'first_ship', 'invite', 'settings']) {
|
|
48
|
+
assert.ok(guide.MOMENT_IDS.has(m), m);
|
|
49
|
+
}
|
|
50
|
+
assert.ok(guide.stepById('where_to_start'), 'the "where to start" step exists');
|
|
51
|
+
assert.ok(guide.stepById('ship_mode'), 'settings covers the ship mode');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('steps are short and in plain language (no jargon)', () => {
|
|
55
|
+
const hits = [];
|
|
56
|
+
for (const s of guide.STEPS) {
|
|
57
|
+
for (const f of ['title', 'what', 'when', 'try']) {
|
|
58
|
+
const text = s[f];
|
|
59
|
+
if (f !== 'try') assert.ok(text.length <= 240, `${s.id}.${f} is ${text.length} chars — keep a step short`);
|
|
60
|
+
for (const term of JARGON_TERMS) {
|
|
61
|
+
const re = new RegExp(`(^|[^A-Za-z])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^A-Za-z]|$)`, /^[A-Z+-]+$/.test(term) ? '' : 'i');
|
|
62
|
+
if (re.test(text)) hits.push(`${s.id}.${f}: "${term}"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
assert.deepEqual(hits, []);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('every slash command a step names is a core skill an instance gets', async () => {
|
|
70
|
+
const fs = await import('node:fs');
|
|
71
|
+
const { isPublishable } = require('../scripts/gds/publish-manifest.js');
|
|
72
|
+
for (const s of guide.STEPS) {
|
|
73
|
+
for (const m of s.try.matchAll(/\/([a-z][a-z-]+)/g)) {
|
|
74
|
+
const skill = `.claude/skills/${m[1]}/SKILL.md`;
|
|
75
|
+
assert.ok(fs.existsSync(new URL(`../${skill}`, import.meta.url)), `${s.id}: /${m[1]} is not a skill`);
|
|
76
|
+
assert.ok(isPublishable(skill), `${s.id}: /${m[1]} does not ship to an instance`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('nextUnread walks the guide in order and ends at null', () => {
|
|
82
|
+
assert.equal(guide.nextUnread([]).id, guide.STEPS[0].id);
|
|
83
|
+
assert.equal(guide.nextUnread([guide.STEPS[0].id]).id, guide.STEPS[1].id);
|
|
84
|
+
assert.equal(guide.nextUnread(guide.STEPS.map((s) => s.id)), null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('formatGuide renders every step, and one moment on request', () => {
|
|
88
|
+
const all = guide.formatGuide();
|
|
89
|
+
for (const s of guide.STEPS) assert.ok(all.includes(`[${s.id}]`), s.id);
|
|
90
|
+
const one = guide.formatGuide({ moment: 'first_ship' });
|
|
91
|
+
assert.ok(one.includes('[what_ship_does]'));
|
|
92
|
+
assert.ok(!one.includes('[welcome]'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
96
|
+
if (failed) process.exit(1);
|
|
@@ -87,7 +87,7 @@ function fakeDb() {
|
|
|
87
87
|
async query(sql, params = []) {
|
|
88
88
|
sqlSeen.push(sql);
|
|
89
89
|
if (PRICE_WORD.test(sql)) throw new Error(`the assessment read price: ${sql.slice(0, 120)}`);
|
|
90
|
-
if (/^SELECT id, module_key, version FROM store_module_versions WHERE id/.test(sql)) {
|
|
90
|
+
if (/^SELECT id, module_key, version(, artifact_path)? FROM store_module_versions WHERE id/.test(sql)) {
|
|
91
91
|
const v = CASES.find((c) => String(c.id) === String(params[0]));
|
|
92
92
|
return { rows: v ? [{ id: v.id, module_key: v.module_key, version: v.version }] : [] };
|
|
93
93
|
}
|
|
@@ -162,7 +162,7 @@ test('assessVersion on publish runs the same steps for every case, and never rea
|
|
|
162
162
|
const recordDocs = async (id, { db: d }) => { steps.push(['docs', id]); return { ok: true, signal: d.add(id, 'docs', 'scored', 50) }; };
|
|
163
163
|
const out = [];
|
|
164
164
|
for (const c of CASES) {
|
|
165
|
-
const res = await assessVersion(c.id, { db, recordSecurity, recordDocs });
|
|
165
|
+
const res = await assessVersion(c.id, { db, dispatchTests: async () => null, recordSecurity, recordDocs });
|
|
166
166
|
assert.equal(res.ok, true, c.module_key);
|
|
167
167
|
out.push(shape(res.score));
|
|
168
168
|
}
|
|
@@ -18,6 +18,9 @@ const { composeScore, recomposeScore } = require('../scripts/gds/module-assess-s
|
|
|
18
18
|
const { assessVersion, queueAssessment, QUEUE_MAX } = require('../scripts/gds/module-assess-version.js');
|
|
19
19
|
|
|
20
20
|
const sig = (id, part, outcome, score = null) => ({ id, part, outcome, score });
|
|
21
|
+
// Every assessVersion call here stubs the test-runner dispatch, so a shell with
|
|
22
|
+
// MODULE_TEST_REPO/MODULE_TEST_TOKEN set can never make this suite reach GitHub.
|
|
23
|
+
const noDispatch = async () => null;
|
|
21
24
|
|
|
22
25
|
// ---------------------------------------------------------------------------
|
|
23
26
|
// composeScore — the rule, pure
|
|
@@ -98,7 +101,7 @@ function fakeDb({ versions = [{ id: 7, module_key: 'weather', version: '1.0.0' }
|
|
|
98
101
|
const db = {
|
|
99
102
|
signals, scores,
|
|
100
103
|
async query(sql, params = []) {
|
|
101
|
-
if (/^SELECT id, module_key, version FROM store_module_versions WHERE id/.test(sql)) {
|
|
104
|
+
if (/^SELECT id, module_key, version(, artifact_path)? FROM store_module_versions WHERE id/.test(sql)) {
|
|
102
105
|
return { rows: versions.filter((v) => String(v.id) === String(params[0])) };
|
|
103
106
|
}
|
|
104
107
|
if (/SELECT DISTINCT ON \(part\)/.test(sql)) {
|
|
@@ -158,7 +161,7 @@ test('assessVersion on publish: Tests pending, the Security gate, Docs, then the
|
|
|
158
161
|
const seen = [];
|
|
159
162
|
const recordSecurity = async (id, { db: d }) => { seen.push(id); const row = d.add(id, 'security', 'passed'); return { ok: true, signal: row }; };
|
|
160
163
|
const recordDocs = async (id, { db: d }) => { seen.push(`docs:${id}`); return { ok: true, signal: d.add(id, 'docs', 'scored', 20) }; };
|
|
161
|
-
const res = await assessVersion(7, { db, recordSecurity, recordDocs });
|
|
164
|
+
const res = await assessVersion(7, { db, dispatchTests: noDispatch, recordSecurity, recordDocs });
|
|
162
165
|
assert.equal(res.ok, true);
|
|
163
166
|
assert.deepEqual(seen, [7, 'docs:7'], 'Security, then Docs');
|
|
164
167
|
assert.equal(res.docs.part, 'docs');
|
|
@@ -173,11 +176,18 @@ test('assessVersion on publish: Tests pending, the Security gate, Docs, then the
|
|
|
173
176
|
test('re-assessing never hides a real Tests score behind a newer "pending"', async () => {
|
|
174
177
|
const db = fakeDb();
|
|
175
178
|
db.add(7, 'tests', 'scored', 85);
|
|
176
|
-
const res = await assessVersion(7, { db, recordSecurity: async (id, { db: d }) => ({ ok: true, signal: d.add(id, 'security', 'passed') }), recordDocs: async () => null });
|
|
179
|
+
const res = await assessVersion(7, { db, dispatchTests: noDispatch, recordSecurity: async (id, { db: d }) => ({ ok: true, signal: d.add(id, 'security', 'passed') }), recordDocs: async () => null });
|
|
177
180
|
assert.equal(db.signals.filter((s) => s.part === 'tests').length, 1);
|
|
178
181
|
assert.equal(res.score.overall, 85);
|
|
179
182
|
});
|
|
180
183
|
|
|
184
|
+
test('assessVersion hands the version to the test-runner dispatch before recording Tests pending', async () => {
|
|
185
|
+
const db = fakeDb();
|
|
186
|
+
const sent = [];
|
|
187
|
+
await assessVersion(7, { db, dispatchTests: async (ver, { db: d }) => { sent.push(ver.id); assert.equal(d, db); return null; }, recordSecurity: async () => null, recordDocs: async () => null });
|
|
188
|
+
assert.deepEqual(sent, [7]);
|
|
189
|
+
});
|
|
190
|
+
|
|
181
191
|
test('assessVersion on an unknown version writes nothing', async () => {
|
|
182
192
|
const db = fakeDb({ versions: [] });
|
|
183
193
|
const res = await assessVersion(99, { db, recordSecurity: async () => { throw new Error('must not run'); } });
|