@bongos/core 1.25.13 → 1.25.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +118 -53
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +20 -0
- package/clients/bongos-client/index.cjs +20 -0
- package/clients/bongos-client/index.d.ts +26 -2
- package/clients/bongos-client/index.mjs +20 -0
- package/docs/adr/0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md +1 -1
- package/docs/adr/0407-a-project-grant-is-made-by-the-owner-not-witnessed-at-sign-in.md +124 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +60 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +550 -2
- package/docs/api-reference.md +14 -4
- package/docs/copy-inventory.md +28 -34
- package/docs/copy-registry.json +41 -125
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +1 -0
- package/docs/page-readings.json +3 -3
- package/modules/front-door-guide/db.js +13 -5
- package/modules/front-door-guide/guide.js +8 -2
- package/modules/front-door-guide/migrations/front_door_guide_003_starting_point.sql +12 -0
- package/modules/front-door-guide/module.json +2 -2
- package/modules/front-door-guide/plan.js +15 -6
- package/modules/front-door-guide/routes/front-door-guide-plan.js +3 -2
- package/modules/front-door-guide/routes/front-door-guide.js +17 -4
- package/modules/front-door-guide/starting-point.js +55 -0
- package/modules/hall-ui/public/genesis-home.js +29 -2
- package/modules/hall-ui/public/goals-page.js +3 -29
- package/modules/hall-ui/public/index.html +2 -1
- package/modules/hall-ui/public/task-lib.js +45 -0
- package/modules/hall-ui/public/work.js +3 -47
- package/modules/lifecycle/kickoff-checklist.js +1 -1
- package/modules/lifecycle/kickoff-seed.js +9 -5
- package/modules/onboarding/genesis-home.js +1 -1
- package/modules/onboarding/genesis-work.js +364 -0
- package/modules/onboarding/migrations/onboarding_002_genesis_work.sql +76 -0
- package/modules/onboarding/routes/genesis-work.js +85 -0
- package/modules/onboarding/routes/onboarding.js +3 -0
- package/package-lock.json +306 -3
- package/package.json +4 -2
- package/release-notes.json +24 -0
- package/scripts/bongos/genesis.js +178 -0
- package/scripts/bongos/package-core.js +4 -2
- package/scripts/bongos/run-unit-tests.js +3 -0
- package/scripts/gds/genesis.js +7 -0
- package/src/bongos/module-scope-map.js +1 -0
- package/src/module-api.js +1 -1
- package/tests/cutover_codemod.mjs +1 -1
- package/tests/front_door_guide_db.mjs +6 -1
- package/tests/front_door_guide_existing.mjs +238 -0
- package/tests/genesis_home.mjs +80 -2
- package/tests/genesis_work.mjs +255 -0
- package/tests/genesis_work_db.mjs +73 -0
- package/tests/genesis_work_routes.mjs +130 -0
- package/tests/package_core.mjs +11 -7
- package/tests/pruned_lockfile_integrity.mjs +4 -2
- package/tests/status_page_no_dead_uptime.mjs +1 -1
- package/tests/task_lib.mjs +46 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// tests/genesis_work_db.mjs — the genesis stage-work contract against a REAL Postgres
|
|
2
|
+
// (task 1005116).
|
|
3
|
+
//
|
|
4
|
+
// tests/genesis_work.mjs runs the contract against an in-memory store in the DB-free
|
|
5
|
+
// unit lane. This runs the SAME contract (imported, not copied) through the real
|
|
6
|
+
// Postgres store (genesis-work.js pgStore) over the real migration, so the one-statement
|
|
7
|
+
// answer-plus-history writes, the no-op guard, the if-empty carry-over and the CHECK
|
|
8
|
+
// constraints are proven where they live. Integration lane (run-unit-tests.js).
|
|
9
|
+
//
|
|
10
|
+
// Needs a MIGRATED database (scripts/migrate.sh). SKIPS (exit 0) with no reachable
|
|
11
|
+
// Postgres, like every *_db test.
|
|
12
|
+
//
|
|
13
|
+
// Run: DATABASE_URL=postgres://... node tests/genesis_work_db.mjs
|
|
14
|
+
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import url from 'node:url';
|
|
20
|
+
import { contract } from './genesis_work.mjs';
|
|
21
|
+
import { insertTestBuilders } from './helpers.mjs';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const { Pool } = require('pg');
|
|
26
|
+
const gw = require('../modules/onboarding/genesis-work.js');
|
|
27
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await pool.query('SELECT 1');
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.log(`genesis_work_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
|
|
33
|
+
await pool.end().catch(() => {});
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let passed = 0;
|
|
38
|
+
let failed = 0;
|
|
39
|
+
async function test(name, fn) {
|
|
40
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
41
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const MIGRATION = fs.readFileSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_002_genesis_work.sql'), 'utf8');
|
|
45
|
+
const wipe = () => pool.query('DELETE FROM onboarding_genesis_answers; DELETE FROM onboarding_genesis_answer_history; DELETE FROM onboarding_genesis_work;');
|
|
46
|
+
|
|
47
|
+
await test('postgres: the migration re-applies cleanly over the migrated schema (idempotent)', async () => {
|
|
48
|
+
await pool.query(MIGRATION);
|
|
49
|
+
await pool.query(MIGRATION);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const A = 910000201;
|
|
53
|
+
await insertTestBuilders(pool, [{ id: A, login: 'genesis-a' }]);
|
|
54
|
+
await wipe();
|
|
55
|
+
try {
|
|
56
|
+
await contract(test, 'postgres', { store: gw.pgStore(pool) }, A);
|
|
57
|
+
await test('postgres: the CHECKs refuse a wordless answer, a bad state, origin, door and link', async () => {
|
|
58
|
+
const bad = (sql, params) => assert.rejects(pool.query(sql, params), (e) => e.code === '23514');
|
|
59
|
+
await bad("INSERT INTO onboarding_genesis_answers (stage, question_id, state, answer_md) VALUES ('scope', 'what', 'answered', ' ')");
|
|
60
|
+
await bad("INSERT INTO onboarding_genesis_answers (stage, question_id, state) VALUES ('scope', 'what', 'maybe')");
|
|
61
|
+
await bad("INSERT INTO onboarding_genesis_answers (stage, question_id, state, origin) VALUES ('scope', 'what', 'skipped', 'model')");
|
|
62
|
+
await bad("INSERT INTO onboarding_genesis_answers (stage, question_id, state) VALUES ('decide', 'what', 'skipped')");
|
|
63
|
+
await bad("INSERT INTO onboarding_genesis_work (stage, door) VALUES ('charter', 'autopilot')");
|
|
64
|
+
await bad("INSERT INTO onboarding_genesis_work (stage, link_url) VALUES ('charter', 'javascript:alert(1)')");
|
|
65
|
+
});
|
|
66
|
+
} finally {
|
|
67
|
+
await wipe().catch(() => {});
|
|
68
|
+
await pool.query('DELETE FROM builders WHERE id = $1', [A]).catch(() => {});
|
|
69
|
+
await pool.end().catch(() => {});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`\ngenesis_work_db: ${passed} passed, ${failed} failed`);
|
|
73
|
+
if (failed) process.exit(1);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// tests/genesis_work_routes.mjs — the genesis stage-work HTTP surface (task 1005116)
|
|
2
|
+
//
|
|
3
|
+
// Drives the REAL routes (modules/onboarding/routes/genesis-work.js) over a real express
|
|
4
|
+
// server and an in-memory store. Auth is stubbed at the doorway the way other route
|
|
5
|
+
// tests do it: a request names its builder in `x-test-builder`, and only the builder
|
|
6
|
+
// named in `x-test-owner` holds `project.settings.manage`. What it proves: any builder
|
|
7
|
+
// reads, only the owner writes, a refusal is a 4xx with its code, un-answer is DELETE,
|
|
8
|
+
// and the planning read path comes back over the wire.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/genesis_work_routes.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { memStore } from './genesis_work.mjs';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const express = require('express');
|
|
18
|
+
const api = require('../src/module-api.js');
|
|
19
|
+
|
|
20
|
+
const OWNER = 1;
|
|
21
|
+
const OTHER = 2;
|
|
22
|
+
api.requireBuilder = (req, res, next) => {
|
|
23
|
+
const id = Number(req.get('x-test-builder'));
|
|
24
|
+
if (!id) return res.status(401).json({ error: { code: 'unauthenticated' } });
|
|
25
|
+
req.builder = { id };
|
|
26
|
+
next();
|
|
27
|
+
};
|
|
28
|
+
api.requirePermission = () => (req, res, next) => (req.builder.id === OWNER ? next() : res.status(403).json({ error: { code: 'forbidden' } }));
|
|
29
|
+
|
|
30
|
+
const mount = require('../modules/onboarding/routes/genesis-work.js');
|
|
31
|
+
const store = memStore();
|
|
32
|
+
const app = express();
|
|
33
|
+
app.use(express.json());
|
|
34
|
+
const router = express.Router();
|
|
35
|
+
mount(router, { store });
|
|
36
|
+
app.use('/api/bongos', router);
|
|
37
|
+
const server = app.listen(0);
|
|
38
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
39
|
+
|
|
40
|
+
async function call(method, path, { as = OWNER, body } = {}) {
|
|
41
|
+
const r = await fetch(base + path, {
|
|
42
|
+
method,
|
|
43
|
+
headers: { 'content-type': 'application/json', ...(as ? { 'x-test-builder': String(as) } : {}) },
|
|
44
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
45
|
+
});
|
|
46
|
+
return { status: r.status, data: await r.json() };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let passed = 0;
|
|
50
|
+
let failed = 0;
|
|
51
|
+
async function test(name, fn) {
|
|
52
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
53
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await test('reads need a signed-in builder', async () => {
|
|
58
|
+
assert.equal((await call('GET', '/founding/work', { as: null })).status, 401);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await test('the owner answers, and any builder reads the answer back', async () => {
|
|
62
|
+
const w = await call('PUT', '/founding/work/strategize/answers/big_pieces', { body: { text: 'Ledger first.' } });
|
|
63
|
+
assert.equal(w.status, 200);
|
|
64
|
+
assert.equal(w.data.changed, true);
|
|
65
|
+
const r = await call('GET', '/founding/work/strategize', { as: OTHER });
|
|
66
|
+
assert.equal(r.status, 200);
|
|
67
|
+
assert.equal(r.data.work.questions.find((q) => q.id === 'big_pieces').answer.text, 'Ledger first.');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await test('a builder who is not the owner cannot write', async () => {
|
|
71
|
+
for (const [m, p, body] of [
|
|
72
|
+
['PUT', '/founding/work/strategize/answers/risks', { text: 'x' }],
|
|
73
|
+
['DELETE', '/founding/work/strategize/answers/big_pieces'],
|
|
74
|
+
['PUT', '/founding/work/strategize/door', { door: 'open' }],
|
|
75
|
+
['PUT', '/founding/work/strategize/page', { page: 'x' }],
|
|
76
|
+
['PUT', '/founding/work/strategize/link', { url: 'https://example.com' }],
|
|
77
|
+
]) assert.equal((await call(m, p, { as: OTHER, body })).status, 403, `${m} ${p}`);
|
|
78
|
+
assert.equal(store.answers.size, 1, 'nothing was written');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await test('"not sure yet" over the wire, then un-answer back to empty', async () => {
|
|
82
|
+
const ns = await call('PUT', '/founding/work/strategize/answers/risks', { body: { state: 'not_sure' } });
|
|
83
|
+
assert.equal(ns.data.work.questions.find((q) => q.id === 'risks').answer.state, 'not_sure');
|
|
84
|
+
const del = await call('DELETE', '/founding/work/strategize/answers/risks');
|
|
85
|
+
assert.equal(del.status, 200);
|
|
86
|
+
assert.equal(del.data.cleared, true);
|
|
87
|
+
assert.equal(del.data.work.questions.find((q) => q.id === 'risks').answer, null);
|
|
88
|
+
const h = await call('GET', '/founding/work/strategize/answers/risks/history', { as: OTHER });
|
|
89
|
+
assert.deepEqual(h.data.history.map((x) => x.state), ['cleared', 'not_sure']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test('refusals come back as 4xx with their code', async () => {
|
|
93
|
+
const cases = [
|
|
94
|
+
['PUT', '/founding/work/strategize/answers/nope', { text: 'x' }, 404, 'unknown_question'],
|
|
95
|
+
['GET', '/founding/work/decide', undefined, 404, 'unknown_stage'],
|
|
96
|
+
['PUT', '/founding/work/strategize/answers/risks', { text: ' ' }, 400, 'empty_answer'],
|
|
97
|
+
['PUT', '/founding/work/strategize/door', { door: 'autopilot' }, 400, 'bad_door'],
|
|
98
|
+
['PUT', '/founding/work/strategize/link', { url: 'ftp://x' }, 400, 'bad_link'],
|
|
99
|
+
];
|
|
100
|
+
for (const [m, p, body, status, code] of cases) {
|
|
101
|
+
const r = await call(m, p, { body });
|
|
102
|
+
assert.equal(r.status, status, `${m} ${p}`);
|
|
103
|
+
assert.equal(r.data.error.code, code);
|
|
104
|
+
}
|
|
105
|
+
const unknownField = await call('PUT', '/founding/work/strategize/door', { body: { door: 'open', extra: 1 } });
|
|
106
|
+
assert.equal(unknownField.status, 400, 'an unknown field is refused');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await test('switching door over the wire keeps the answers', async () => {
|
|
110
|
+
await call('PUT', '/founding/work/strategize/page', { body: { page: 'Bet on the ledger.' } });
|
|
111
|
+
const r = await call('PUT', '/founding/work/strategize/door', { body: { door: 'open' } });
|
|
112
|
+
assert.equal(r.data.work.door, 'open');
|
|
113
|
+
assert.equal(r.data.work.page, 'Bet on the ledger.');
|
|
114
|
+
assert.equal(r.data.work.counts.answered, 1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await test('the planning read path and the assisted brief are readable by any builder', async () => {
|
|
118
|
+
const c = await call('GET', '/founding/context', { as: OTHER });
|
|
119
|
+
assert.equal(c.status, 200);
|
|
120
|
+
assert.match(c.data.markdown, /Ledger first\./);
|
|
121
|
+
assert.match(c.data.markdown, /Bet on the ledger\./);
|
|
122
|
+
const a = await call('GET', '/founding/work/strategize/assist', { as: OTHER });
|
|
123
|
+
assert.match(a.data.prompt, /I am the author/);
|
|
124
|
+
});
|
|
125
|
+
} finally {
|
|
126
|
+
server.close();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(`\ngenesis_work_routes: ${passed} passed, ${failed} failed`);
|
|
130
|
+
if (failed) process.exit(1);
|
package/tests/package_core.mjs
CHANGED
|
@@ -363,15 +363,15 @@ t('TAR_ROOT is the npm-required "package/" (not the ARTIFACT_NAME dir)', () => {
|
|
|
363
363
|
assert.equal(TAR_ROOT, 'package/');
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
-
t('buildPackageJson: prunes to
|
|
366
|
+
t('buildPackageJson: prunes to the core deps, keeps the source ranges + the undici override', () => {
|
|
367
367
|
const pkg = buildPackageJson({
|
|
368
368
|
version: '1.14.0',
|
|
369
|
-
sourceDeps: { express: '^4.21.2', pg: '^8.20.0', pino: '^9.14.0', phaser: '^3.87.0', 'discord.js': '^14.26.4' },
|
|
369
|
+
sourceDeps: { express: '^4.21.2', pg: '^8.20.0', pino: '^9.14.0', '@simplewebauthn/browser': '^13.3.0', '@simplewebauthn/server': '^13.3.3', phaser: '^3.87.0', 'discord.js': '^14.26.4' },
|
|
370
370
|
sourceOverrides: { undici: '^6.27.0' },
|
|
371
371
|
});
|
|
372
372
|
assert.equal(pkg.name, '@bongos/core');
|
|
373
373
|
assert.equal(pkg.version, '1.14.0');
|
|
374
|
-
assert.deepEqual(pkg.dependencies, { express: '^4.21.2', pg: '^8.20.0', pino: '^9.14.0' });
|
|
374
|
+
assert.deepEqual(pkg.dependencies, { express: '^4.21.2', pg: '^8.20.0', pino: '^9.14.0', '@simplewebauthn/browser': '^13.3.0', '@simplewebauthn/server': '^13.3.3' });
|
|
375
375
|
assert.equal(pkg.dependencies.phaser, undefined);
|
|
376
376
|
assert.equal(pkg.dependencies['discord.js'], undefined);
|
|
377
377
|
assert.deepEqual(pkg.overrides, { undici: '^6.27.0' });
|
|
@@ -450,7 +450,7 @@ t('the publish lane sets no long-lived npm credential (OIDC only)', () => {
|
|
|
450
450
|
});
|
|
451
451
|
|
|
452
452
|
t('buildPackageJson: omits `overrides` when the source has none', () => {
|
|
453
|
-
const pkg = buildPackageJson({ version: '1.0.0', sourceDeps: { express: '^4.0.0', pg: '^8.0.0', pino: '^9.0.0' } });
|
|
453
|
+
const pkg = buildPackageJson({ version: '1.0.0', sourceDeps: { express: '^4.0.0', pg: '^8.0.0', pino: '^9.0.0', '@simplewebauthn/browser': '^13.0.0', '@simplewebauthn/server': '^13.0.0' } });
|
|
454
454
|
assert.equal(pkg.overrides, undefined);
|
|
455
455
|
});
|
|
456
456
|
|
|
@@ -458,8 +458,10 @@ t('buildPackageJson: throws when a required core dep is missing from the source'
|
|
|
458
458
|
assert.throws(() => buildPackageJson({ version: '1.0.0', sourceDeps: { express: '^4.0.0' } }), /missing required core dependency "pg"/);
|
|
459
459
|
});
|
|
460
460
|
|
|
461
|
-
|
|
462
|
-
|
|
461
|
+
// The security-card libraries are core runtime deps: routes/my-passkeys.js serves the browser bundle
|
|
462
|
+
// and requires the server lib. Left out, every packaged instance answers bundle_missing (task 1005269).
|
|
463
|
+
t('CORE_DEP_NAMES is exactly [express, pg, pino, @simplewebauthn/browser, @simplewebauthn/server]', () => {
|
|
464
|
+
assert.deepEqual(CORE_DEP_NAMES, ['express', 'pg', 'pino', '@simplewebauthn/browser', '@simplewebauthn/server']);
|
|
463
465
|
});
|
|
464
466
|
|
|
465
467
|
// A tiny synthetic lockfile fixture with a THREE-level chain (root -> a -> b) and
|
|
@@ -542,7 +544,9 @@ t('prunePackageLock: on the real repo lockfile, drops the vast majority of packa
|
|
|
542
544
|
});
|
|
543
545
|
const keptCount = Object.keys(pruned.packages).length - 1; // exclude the root '' entry
|
|
544
546
|
const sourceCount = Object.keys(realLock.packages).length - 1;
|
|
545
|
-
|
|
547
|
+
// Under two thirds: the card libraries (task 1005269) added their certificate-parsing closure, taking the
|
|
548
|
+
// kept share from ~44% to ~55%. That the game deps stay out is asserted by the closure test above.
|
|
549
|
+
assert.ok(keptCount < sourceCount * 2 / 3, `expected a real prune (kept ${keptCount} of ${sourceCount})`);
|
|
546
550
|
assert.ok(keptCount > 0);
|
|
547
551
|
});
|
|
548
552
|
|
|
@@ -34,7 +34,7 @@ const isRegistryResolved = (v) => typeof v.resolved === 'string' && /^https?:\/\
|
|
|
34
34
|
|
|
35
35
|
const pruned = prunePackageLock(realLock, {
|
|
36
36
|
name: '@bongos/core', version: '0.0.0-test',
|
|
37
|
-
rootRanges: { express: '^4.21.2', pg: '^8.20.0' },
|
|
37
|
+
rootRanges: { express: '^4.21.2', pg: '^8.20.0', pino: '^9.14.0', '@simplewebauthn/browser': '^13.3.0', '@simplewebauthn/server': '^13.3.3' },
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
t('pruned lockfile: non-trivial and closes over the core deps (express + pg present)', () => {
|
|
@@ -42,6 +42,8 @@ t('pruned lockfile: non-trivial and closes over the core deps (express + pg pres
|
|
|
42
42
|
assert.ok(keys.length > 2, `pruned lockfile is suspiciously empty (${keys.length} entries)`);
|
|
43
43
|
assert.ok(pruned.packages['node_modules/express'], 'express missing from the pruned closure');
|
|
44
44
|
assert.ok(pruned.packages['node_modules/pg'], 'pg missing from the pruned closure');
|
|
45
|
+
assert.ok(pruned.packages['node_modules/@simplewebauthn/browser'], 'card browser library missing from the pruned closure');
|
|
46
|
+
assert.ok(pruned.packages['node_modules/@simplewebauthn/server'], 'card server library missing from the pruned closure');
|
|
45
47
|
});
|
|
46
48
|
|
|
47
49
|
t('pruned lockfile: EVERY registry-resolved package keeps a well-formed integrity hash (R88 guard)', () => {
|
|
@@ -72,7 +74,7 @@ t('pruned lockfile: integrity hashes match the SOURCE lockfile verbatim (not rew
|
|
|
72
74
|
});
|
|
73
75
|
|
|
74
76
|
t('CORE_DEP_NAMES: the pin domain is exactly the runtime core deps (express + pg + pino)', () => {
|
|
75
|
-
assert.ok(Array.isArray(CORE_DEP_NAMES) && CORE_DEP_NAMES.includes('express') && CORE_DEP_NAMES.includes('pg') && CORE_DEP_NAMES.includes('pino'));
|
|
77
|
+
assert.ok(Array.isArray(CORE_DEP_NAMES) && CORE_DEP_NAMES.includes('express') && CORE_DEP_NAMES.includes('pg') && CORE_DEP_NAMES.includes('pino') && CORE_DEP_NAMES.includes('@simplewebauthn/browser') && CORE_DEP_NAMES.includes('@simplewebauthn/server'));
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
console.log(`\npruned_lockfile_integrity: ${passed} passed, ${failed} failed`);
|
|
@@ -69,7 +69,7 @@ test('the server no longer registers GET /public/uptime', () => {
|
|
|
69
69
|
|
|
70
70
|
test('the core package exports only files that exist, and the codemod maps only exported subpaths', () => {
|
|
71
71
|
const { buildPackageJson } = require('../scripts/bongos/package-core.js');
|
|
72
|
-
const pkg = buildPackageJson({ version: '1.0.0', sourceDeps: { express: '^4.0.0', pg: '^8.0.0', pino: '^9.0.0' } });
|
|
72
|
+
const pkg = buildPackageJson({ version: '1.0.0', sourceDeps: { express: '^4.0.0', pg: '^8.0.0', pino: '^9.0.0', '@simplewebauthn/browser': '^13.0.0', '@simplewebauthn/server': '^13.0.0' } });
|
|
73
73
|
for (const [sub, target] of Object.entries(pkg.exports)) {
|
|
74
74
|
assert.ok(existsSync(join(ROOT, target)), `export ${sub} points at missing ${target}`);
|
|
75
75
|
}
|
package/tests/task_lib.mjs
CHANGED
|
@@ -136,5 +136,51 @@ t('conflictChipHtml: empty for none, count + capped list for many', () => {
|
|
|
136
136
|
assert.doesNotMatch(many, /#6/);
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
// task 1005109 — the one Copy prompt click, shared by the Tasks board, a goal's to-do
|
|
140
|
+
// list and the Genesis stage list. Runs against fake browser globals.
|
|
141
|
+
async function at(name, fn) {
|
|
142
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
143
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
144
|
+
}
|
|
145
|
+
function fakeBtn() {
|
|
146
|
+
const icon = { textContent: '📋' };
|
|
147
|
+
const attrs = {};
|
|
148
|
+
return { icon, attrs, querySelector: (sel) => (sel === '.task__copy-icon' ? icon : null), setAttribute: (k, v) => { attrs[k] = v; } };
|
|
149
|
+
}
|
|
150
|
+
const savedGlobals = { navigator: globalThis.navigator, location: globalThis.location, setTimeout: globalThis.setTimeout };
|
|
151
|
+
function stubGlobals(writeText) {
|
|
152
|
+
Object.defineProperty(globalThis, 'navigator', { value: { clipboard: { writeText } }, configurable: true, writable: true });
|
|
153
|
+
globalThis.location = { hostname: 'builders.example.test', origin: 'https://builders.example.test' };
|
|
154
|
+
globalThis.setTimeout = () => 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await at('copyTaskPrompt copies the task\'s own words, flips the button, says one sentence', async () => {
|
|
158
|
+
const copied = [];
|
|
159
|
+
const toasts = [];
|
|
160
|
+
stubGlobals(async (x) => { copied.push(x); });
|
|
161
|
+
const btn = fakeBtn();
|
|
162
|
+
const ok = await T.copyTaskPrompt({ id: 42, title: 'Write the scope', description: 'Answer the questions.' }, btn, (m) => toasts.push(m));
|
|
163
|
+
assert.equal(ok, true);
|
|
164
|
+
assert.equal(copied.length, 1);
|
|
165
|
+
assert.match(copied[0], /Answer the questions\./);
|
|
166
|
+
assert.doesNotMatch(copied[0], /\(no description\)/);
|
|
167
|
+
assert.equal(btn.icon.textContent, '✓');
|
|
168
|
+
assert.equal(btn.attrs['data-copied'], '1');
|
|
169
|
+
assert.deepEqual(toasts, ['Prompt for task 42 copied to your clipboard.']);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await at('copyTaskPrompt: a refused clipboard with no fallback says so and leaves the button alone', async () => {
|
|
173
|
+
const toasts = [];
|
|
174
|
+
stubGlobals(async () => { throw new Error('denied'); });
|
|
175
|
+
const btn = fakeBtn();
|
|
176
|
+
const ok = await T.copyTaskPrompt({ id: 7, title: 'x', description: 'y' }, btn, (m) => toasts.push(m));
|
|
177
|
+
assert.equal(ok, false, 'no document under Node, so the textarea fallback fails too');
|
|
178
|
+
assert.equal(btn.icon.textContent, '📋');
|
|
179
|
+
assert.deepEqual(toasts, ['Could not copy to the clipboard. Try again.']);
|
|
180
|
+
});
|
|
181
|
+
Object.defineProperty(globalThis, 'navigator', { value: savedGlobals.navigator, configurable: true, writable: true });
|
|
182
|
+
globalThis.location = savedGlobals.location;
|
|
183
|
+
globalThis.setTimeout = savedGlobals.setTimeout;
|
|
184
|
+
|
|
139
185
|
console.log(`\ntask-lib tests: ${passed} passed, ${failed} failed`);
|
|
140
186
|
if (failed) process.exit(1);
|