@bongos/core 1.25.12 → 1.25.14
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 +106 -51
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +13 -4
- package/clients/bongos-client/index.mjs +6 -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/README.md +1 -0
- package/docs/api/openapi.json +198 -11
- package/docs/api-reference.md +7 -4
- package/docs/copy-inventory.md +28 -34
- package/docs/copy-registry.json +41 -125
- package/docs/file-map.md +1 -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/claude.js +11 -7
- package/modules/front-door-guide/context.js +54 -0
- package/modules/front-door-guide/db.js +79 -6
- package/modules/front-door-guide/guide.js +18 -3
- package/modules/front-door-guide/migrations/front_door_guide_002_plan_and_account.sql +37 -0
- package/modules/front-door-guide/migrations/front_door_guide_003_starting_point.sql +12 -0
- package/modules/front-door-guide/module.json +7 -3
- package/modules/front-door-guide/plan.js +102 -0
- package/modules/front-door-guide/pollers/retention-sweep.js +67 -0
- package/modules/front-door-guide/routes/front-door-guide-plan.js +145 -0
- package/modules/front-door-guide/routes/front-door-guide.js +36 -33
- package/modules/front-door-guide/settings.js +4 -0
- 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-seed.js +9 -5
- package/modules/onboarding/genesis-home.js +1 -1
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/seams.js +10 -0
- package/package-lock.json +306 -3
- package/package.json +4 -2
- package/release-notes.json +24 -0
- package/scripts/bongos/package-core.js +4 -2
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/src/bongos/module-scope-map.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/cutover_codemod.mjs +1 -1
- package/tests/front_door_guide_db.mjs +115 -0
- package/tests/front_door_guide_existing.mjs +238 -0
- package/tests/front_door_guide_plan.mjs +344 -0
- package/tests/genesis_home.mjs +80 -2
- 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
package/tests/genesis_home.mjs
CHANGED
|
@@ -73,6 +73,23 @@ test('the board read groups genesis tasks by the stage their ref names', async (
|
|
|
73
73
|
assert.deepEqual(out.scope.map((t) => t.id), ['1', '2']);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
test('the board read carries each genesis task\'s description, for its Copy prompt (task 1005109)', async () => {
|
|
77
|
+
let seen = '';
|
|
78
|
+
const pool = { query: async (sql) => {
|
|
79
|
+
seen = sql;
|
|
80
|
+
return { rows: [
|
|
81
|
+
{ id: 7, title: 'Write the scope', status: 'ready', description: 'Answer the five scope questions.', source_ref: 'genesis:scope' },
|
|
82
|
+
{ id: 8, title: 'Old row', status: 'ready', description: null, source_ref: 'genesis:charter' },
|
|
83
|
+
] };
|
|
84
|
+
} };
|
|
85
|
+
const out = await seed.genesisStageTasks({ pool });
|
|
86
|
+
assert.match(seen, /\bdescription\b/, 'the SELECT reads the description');
|
|
87
|
+
assert.equal(out.scope[0].description, 'Answer the five scope questions.');
|
|
88
|
+
assert.equal(out.charter[0].description, '', 'a null description is an empty string, never "null"');
|
|
89
|
+
const g = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan, tasks: out });
|
|
90
|
+
assert.equal(g.stages[0].tasks[0].description, 'Answer the five scope questions.', 'the genesis view passes it through');
|
|
91
|
+
});
|
|
92
|
+
|
|
76
93
|
// ── the plan ─────────────────────────────────────────────────────────────────
|
|
77
94
|
|
|
78
95
|
test('an empty plan is the recommended defaults: the solo fast-track is one click', () => {
|
|
@@ -361,25 +378,60 @@ test('every genesis write is the project owner\'s; the read is any signed-in bui
|
|
|
361
378
|
|
|
362
379
|
const CARD = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis-home.js'), 'utf8');
|
|
363
380
|
|
|
381
|
+
const TASK_LIB = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'task-lib.js'), 'utf8');
|
|
382
|
+
|
|
364
383
|
function bootCard({ founding, data = { genesis: null } }) {
|
|
365
384
|
const calls = [];
|
|
385
|
+
const copied = [];
|
|
386
|
+
const toasts = [];
|
|
366
387
|
let widget = null;
|
|
367
388
|
let observer = null;
|
|
368
389
|
const attrs = founding ? { 'data-founding': 'on' } : {};
|
|
369
390
|
const sandbox = {
|
|
370
391
|
window: {
|
|
371
|
-
OTB: { escapeHtml: (s) => String(s), statusPill: (s) => `<span>${s}</span>`, contributeHallWidget: (w) => { widget = w; } },
|
|
392
|
+
OTB: { escapeHtml: (s) => String(s), statusPill: (s) => `<span>${s}</span>`, contributeHallWidget: (w) => { widget = w; }, toast: (m) => toasts.push(m) },
|
|
372
393
|
BongosClient: { createClient: () => ({ request: async (m, p) => { calls.push(`${m} ${p}`); return { ok: true, status: 200, data }; } }) },
|
|
373
394
|
},
|
|
395
|
+
navigator: { clipboard: { writeText: async (t) => { copied.push(t); } } },
|
|
396
|
+
location: { hostname: 'builders.example.test', origin: 'https://builders.example.test' },
|
|
374
397
|
document: { documentElement: { getAttribute: (k) => attrs[k] || null } },
|
|
375
398
|
MutationObserver: class { constructor(fn) { observer = fn; } observe() {} disconnect() {} },
|
|
399
|
+
setTimeout: () => 0,
|
|
376
400
|
Promise,
|
|
377
401
|
};
|
|
378
402
|
vm.createContext(sandbox);
|
|
403
|
+
// The hall's real prompt builder, loaded as index.html loads it (before the card).
|
|
404
|
+
vm.runInContext(TASK_LIB, sandbox, { filename: 'task-lib.js' });
|
|
379
405
|
vm.runInContext(CARD, sandbox, { filename: 'genesis-home.js' });
|
|
380
406
|
const host = { hidden: true, innerHTML: '', addEventListener() {}, querySelector: () => null, querySelectorAll: () => [] };
|
|
381
407
|
widget.mount(host);
|
|
382
|
-
return { widget, host, calls, attrs, fire: () => observer && observer() };
|
|
408
|
+
return { widget, host, calls, copied, toasts, attrs, fire: () => observer && observer() };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Re-mount a booted card so its click handler is captured; returns a click(btn) driver.
|
|
412
|
+
async function mountClickable(b) {
|
|
413
|
+
let onClick = null;
|
|
414
|
+
b.host.addEventListener = (type, fn) => { if (type === 'click') onClick = fn; };
|
|
415
|
+
b.host.contains = () => true;
|
|
416
|
+
b.widget.mount(b.host);
|
|
417
|
+
await new Promise((r) => setImmediate(r));
|
|
418
|
+
await new Promise((r) => setImmediate(r));
|
|
419
|
+
return async (btn) => {
|
|
420
|
+
onClick({ target: { closest: () => btn } });
|
|
421
|
+
for (let i = 0; i < 4; i++) await new Promise((r) => setImmediate(r));
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// A fake button carrying one data attribute, as the click handler reads it.
|
|
426
|
+
function fakeButton(attr, value) {
|
|
427
|
+
const set = {};
|
|
428
|
+
return {
|
|
429
|
+
set,
|
|
430
|
+
hasAttribute: (k) => k === attr,
|
|
431
|
+
getAttribute: (k) => (k === attr ? value : null),
|
|
432
|
+
setAttribute: (k, v) => { set[k] = v; },
|
|
433
|
+
querySelector: () => null,
|
|
434
|
+
};
|
|
383
435
|
}
|
|
384
436
|
|
|
385
437
|
test('the card registers in the Home banner, ahead of the welcome, hidden by default', () => {
|
|
@@ -459,6 +511,32 @@ test('the card lists every stage vote waiting on the viewer, and votes yes on al
|
|
|
459
511
|
], 'one yes per stage, earliest first');
|
|
460
512
|
});
|
|
461
513
|
|
|
514
|
+
test('each open stage task has Copy prompt, and it copies the task\'s own words (task 1005109)', async () => {
|
|
515
|
+
const genesis = gh.shapeGenesis({
|
|
516
|
+
mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 },
|
|
517
|
+
plan: gh.parsePlan({}).plan,
|
|
518
|
+
tasks: { scope: [
|
|
519
|
+
{ id: '201', title: 'Gather what already exists', status: 'shipped', description: 'Link the documents.' },
|
|
520
|
+
{ id: '202', title: 'Write the scope', status: 'ready', description: 'Answer the five scope questions in your own words.' },
|
|
521
|
+
] },
|
|
522
|
+
});
|
|
523
|
+
const b = bootCard({ founding: true, data: { genesis, viewer_id: '1' } });
|
|
524
|
+
const click = await mountClickable(b);
|
|
525
|
+
const html = b.host.innerHTML;
|
|
526
|
+
assert.match(html, /data-genesis-copy="202"/, 'the open task offers Copy prompt');
|
|
527
|
+
assert.doesNotMatch(html, /data-genesis-copy="201"/, 'a shipped task offers nothing');
|
|
528
|
+
assert.doesNotMatch(html, /task__claim|>Claim</, 'Copy prompt only: no Claim button (owner, 2026-11-07)');
|
|
529
|
+
const btn = fakeButton('data-genesis-copy', '202');
|
|
530
|
+
await click(btn);
|
|
531
|
+
assert.equal(b.copied.length, 1, 'one click, one copy');
|
|
532
|
+
const text = b.copied[0];
|
|
533
|
+
assert.doesNotMatch(text, /\(no description\)/, 'the copied prompt is never the empty fallback');
|
|
534
|
+
assert.match(text, /Answer the five scope questions in your own words\./, 'it carries the task\'s description');
|
|
535
|
+
assert.match(text, /Bongos task 202 \(Write the scope\)/, 'built by the hall\'s one prompt builder');
|
|
536
|
+
assert.equal(btn.set['data-copied'], '1');
|
|
537
|
+
assert.match(b.toasts.join(' '), /copied/i);
|
|
538
|
+
});
|
|
539
|
+
|
|
462
540
|
test('the hall carries the five life plates byte-identical to the world\'s', () => {
|
|
463
541
|
const world = path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'world');
|
|
464
542
|
const hall = path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis');
|
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);
|