@bongos/core 1.19.375 → 1.19.377
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 +94 -39
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +5 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +76 -5
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +128 -127
- package/docs/copy-registry.json +175 -156
- package/migrations/core_227_task_created_by.sql +83 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/goals-lib.js +5 -5
- package/modules/hall-ui/public/goals-page.js +54 -23
- package/modules/hall-ui/public/goals-render.js +15 -0
- package/modules/hall-ui/public/task-lib.js +29 -0
- package/modules/hall-ui/public/work.js +117 -5
- package/modules/ideas/inbox.js +4 -0
- package/modules/lifecycle/db-tasks.js +61 -5
- package/modules/lifecycle/goal-rollup.js +118 -0
- package/modules/lifecycle/kickoff-checklist.js +138 -0
- package/modules/lifecycle/kickoff-seed.js +69 -0
- package/modules/lifecycle/module.json +4 -2
- package/modules/lifecycle/routes/goals.js +26 -0
- package/modules/lifecycle/routes/kickoff-seed.js +29 -0
- package/modules/lifecycle/routes/task-write-routes.js +3 -0
- package/modules/lifecycle/routes/tasks.js +47 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +1 -0
- package/scripts/gds/init.js +9 -79
- package/scripts/gds/test-path-guard.js +99 -0
- package/src/bongos/auth-admission.js +23 -0
- package/src/module-api.js +5 -1
- package/tests/first_admin_kickoff_seed.mjs +56 -0
- package/tests/goal_sort_staleness.mjs +286 -0
- package/tests/goal_subtitle.mjs +6 -1
- package/tests/goals_lib.mjs +16 -4
- package/tests/kickoff_checklist.mjs +107 -0
- package/tests/kickoff_seed.mjs +93 -0
- package/tests/provisioning_onboard_mode.mjs +3 -4
- package/tests/search_ingest.mjs +3 -2
- package/tests/setup_smoke.mjs +4 -2
- package/tests/test_path_guard.mjs +126 -0
package/scripts/gds/init.js
CHANGED
|
@@ -218,18 +218,11 @@ function provisioningRequest(spec) {
|
|
|
218
218
|
return body;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
// The
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
title: 'Production topology: TLS, real origins, managed Postgres',
|
|
228
|
-
kind: 'infra',
|
|
229
|
-
description: 'For a real (non-localhost) launch: front the instance with TLS (Caddy/Cloudflare/nginx), point ' +
|
|
230
|
-
'publicOrigin/buildersOrigin/statusOrigin at the real domain (the OAuth callback depends on this), and move off the bundled container DB volume to managed/backed-up Postgres.',
|
|
231
|
-
};
|
|
232
|
-
}
|
|
221
|
+
// The kickoff checklist itself (and the production-topology step the provisioning
|
|
222
|
+
// module can automate) now live in modules/lifecycle/kickoff-checklist.js — ONE list shared
|
|
223
|
+
// with the lifecycle module, which files the same steps at the founder's first
|
|
224
|
+
// sign-in on the control-plane path (task 1003239). init keeps its names.
|
|
225
|
+
const { kickoffChecklist, productionTopologyKickoff } = require('../../modules/lifecycle/kickoff-checklist');
|
|
233
226
|
|
|
234
227
|
// The lean instance branding.json: ONLY the fields the owner supplied. Everything
|
|
235
228
|
// else (theme, copy, rank labels, box subdomain pattern) resolves from
|
|
@@ -325,74 +318,11 @@ function mergeCoreDependency(pkg, { corePkg = CORE_PKG, coreRange, name = 'cloud
|
|
|
325
318
|
return { pkg: next, action: 'added' };
|
|
326
319
|
}
|
|
327
320
|
|
|
328
|
-
// The kickoff checklist, as tasks
|
|
329
|
-
//
|
|
330
|
-
// the
|
|
331
|
-
// a real, claimable task so it's tracked, never silently skipped. Per-module
|
|
332
|
-
// prerequisites are appended only for the modules the owner enabled.
|
|
321
|
+
// The kickoff checklist, as tasks — the steps init CANNOT do for you (the shared
|
|
322
|
+
// list, self-host shape). Production topology is omitted when the owner opted into
|
|
323
|
+
// provisioning: the control-plane runner covers that leg (ADR 0111 §3).
|
|
333
324
|
function kickoffTasks(spec) {
|
|
334
|
-
|
|
335
|
-
const tasks = [
|
|
336
|
-
{
|
|
337
|
-
title: 'Register a GitHub OAuth app and set its credentials',
|
|
338
|
-
kind: 'infra',
|
|
339
|
-
description: 'Auth is the hard prerequisite for every sign-in — without it the server returns 503 ' +
|
|
340
|
-
'auth_not_configured and nobody (including you) can log in. Create a GitHub OAuth app, set ' +
|
|
341
|
-
`<ENVPREFIX>_GITHUB_CLIENT_ID/SECRET, tick "Enable Device Flow" (a separate save), and set the callback URL to ${origin}/api/gds/auth/web/callback.`,
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
title: `Sign in once as ${spec.firstAdmin} to seat the founding owner`,
|
|
345
|
-
kind: 'infra',
|
|
346
|
-
description: `A fresh database has no owner. Signing in as the configured firstAdmin (${spec.firstAdmin}) ` +
|
|
347
|
-
'the first time auto-seats that account as the founding Archon (owner). Config alone does nothing — you must actually sign in.',
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
title: 'Confirm the GitHub repo binding and deploy mode',
|
|
351
|
-
kind: 'infra',
|
|
352
|
-
description: `This instance ships work to ${spec.repo.owner}/${spec.repo.name}. Confirm the git remote / repo binding ` +
|
|
353
|
-
'and pick deploy mode in config/deploy.json (ci = server-mediated PR auto-merge, or laptop). A wrong/absent repo binding lets a task grade but never merge.',
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
title: 'Set the admission posture and invite the first builders',
|
|
357
|
-
kind: 'infra',
|
|
358
|
-
description: 'Enrollment is invite-only by default. Beyond you, no one can join until you approve an access request ' +
|
|
359
|
-
'or set <ENVPREFIX>_OPEN_XENOS_ENROLLMENT=1. Decide who can join and invite/approve the first builders.',
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
title: 'Arm the merge gate before a second builder joins (ci deploy mode)',
|
|
363
|
-
kind: 'infra',
|
|
364
|
-
description: 'For a shared instance on ci deploy mode, enable branch protection on main + required checks ' +
|
|
365
|
-
'(unit / secret-scan / gate-review), so untrusted builders cannot push straight to production. No review requirement — gates are CI checks, not human approvals (ADR 0162). Skippable for a solo/local instance.',
|
|
366
|
-
},
|
|
367
|
-
];
|
|
368
|
-
// Production topology is either AUTOMATED (the owner opted into provisioning — the
|
|
369
|
-
// control-plane runner stands up TLS/DNS/managed-DB) or a tracked manual step.
|
|
370
|
-
// Keep it in its infra slot, before the look step, when it's still manual. (ADR 0111 §3)
|
|
371
|
-
if (!provisioningRequest(spec)) tasks.push(productionTopologyKickoff());
|
|
372
|
-
tasks.push({
|
|
373
|
-
title: 'Fill the instance look with your own LLM',
|
|
374
|
-
// 'ideator' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
|
|
375
|
-
// kind and the task silently vanished from the checklist (the one non-infra
|
|
376
|
-
// kickoff step). Deciding the instance look is a decision by an ideator.
|
|
377
|
-
kind: 'decision',
|
|
378
|
-
discipline: 'ideator',
|
|
379
|
-
description: 'init wrote a lean config/branding.json and left the LOOK to you (a locked project rule: brand with your ' +
|
|
380
|
-
'own LLM, not a generator). Hand docs/branding-fill-prompt.md to your AI to design a palette/fonts/wordmark, or keep the vanilla look.',
|
|
381
|
-
});
|
|
382
|
-
const enabled = new Set(Array.isArray(spec.modules) ? spec.modules : []);
|
|
383
|
-
if (enabled.has('art-pipeline')) {
|
|
384
|
-
tasks.push({ title: 'Provide a Google Gemini API key for the art pipeline', kind: 'infra',
|
|
385
|
-
description: 'The art-pipeline module needs a Google AI Studio (Gemini) key (GOOGLE_AI_STUDIO_KEY, a shared key, or per-builder keys) or it cannot generate art.' });
|
|
386
|
-
}
|
|
387
|
-
if (enabled.has('discord')) {
|
|
388
|
-
tasks.push({ title: 'Provide Discord bot token + webhooks', kind: 'infra',
|
|
389
|
-
description: 'The discord module needs a bot token + webhook URLs + DISCORD_BOT_ENABLED=true. Note: outbound/link surfaces fire on credentials even if the module flag is later turned off.' });
|
|
390
|
-
}
|
|
391
|
-
if (enabled.has('dev-box')) {
|
|
392
|
-
tasks.push({ title: 'Provide a DigitalOcean API token for dev boxes', kind: 'infra',
|
|
393
|
-
description: 'The dev-box module needs a DigitalOcean API token (and optionally Cloudflare) on the operator control plane before any builder box can be provisioned.' });
|
|
394
|
-
}
|
|
395
|
-
return tasks;
|
|
325
|
+
return kickoffChecklist(spec, { provisioned: !!provisioningRequest(spec) });
|
|
396
326
|
}
|
|
397
327
|
|
|
398
328
|
// Normalize a raw spec (from --from or the interview) to canonical shape +
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/test-path-guard.js — a test must not build a filesystem path from
|
|
3
|
+
// `URL.pathname` (task 1002889).
|
|
4
|
+
//
|
|
5
|
+
// THE BUG THIS EXISTS TO STOP. `new URL('..', import.meta.url).pathname` is a URL
|
|
6
|
+
// path, not a filesystem path: percent-encoding survives (a space stays `%20`) and
|
|
7
|
+
// on Windows it carries a leading slash before the drive letter. Handed to
|
|
8
|
+
// require() or fs it resolves nowhere — four tests threw MODULE_NOT_FOUND at load
|
|
9
|
+
// on the owner's checkout, whose path has a space in it.
|
|
10
|
+
//
|
|
11
|
+
// WHY A FITNESS CHECK AND NOT A TEST. CI checks out to a path with no space, so the
|
|
12
|
+
// broken form passes there and the suite reads green while being unrunnable on a
|
|
13
|
+
// Windows dev machine. CI can never catch the regression on its own; a static scan
|
|
14
|
+
// at a different altitude can.
|
|
15
|
+
//
|
|
16
|
+
// THE FIX IT POINTS AT. `fileURLToPath(new URL('..', import.meta.url))` from
|
|
17
|
+
// node:url (tests/fitness.mjs), or a plain relative require through
|
|
18
|
+
// createRequire(import.meta.url) (tests/provisioning_teardown_intent.mjs).
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const { execFileSync } = require('node:child_process');
|
|
25
|
+
|
|
26
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
27
|
+
const NAME = 'a test builds its repo root with fileURLToPath, never URL.pathname (task 1002889)';
|
|
28
|
+
|
|
29
|
+
// `import.meta.url).pathname` — the URL-path-as-filesystem-path defect, in both its
|
|
30
|
+
// forms: new URL('..', import.meta.url).pathname and new URL(import.meta.url).pathname.
|
|
31
|
+
const PATHNAME_ROOT = /import\.meta\.url\s*\)\s*\.pathname\b/;
|
|
32
|
+
|
|
33
|
+
// Every tracked test file. Only tests use import.meta.url this way; source is CJS.
|
|
34
|
+
function trackedTestFiles() {
|
|
35
|
+
try {
|
|
36
|
+
const out = execFileSync('git', ['ls-files', 'tests/*.mjs', 'tests/*.js', 'tests/**/*.mjs', 'tests/**/*.js'],
|
|
37
|
+
{ cwd: REPO_ROOT, encoding: 'utf8' });
|
|
38
|
+
return [...new Set(out.split('\n').map((s) => s.trim()).filter(Boolean))];
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scanTests(files, read) {
|
|
45
|
+
const violations = [];
|
|
46
|
+
let scanned = 0;
|
|
47
|
+
for (const rel of files) {
|
|
48
|
+
const src = read(rel);
|
|
49
|
+
if (src == null) continue;
|
|
50
|
+
scanned += 1;
|
|
51
|
+
const lines = src.split('\n');
|
|
52
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
53
|
+
if (!PATHNAME_ROOT.test(lines[i])) continue;
|
|
54
|
+
violations.push(
|
|
55
|
+
`${rel}:${i + 1} builds a filesystem path from URL.pathname — a space becomes %20 and a Windows `
|
|
56
|
+
+ 'drive gains a leading slash, so require()/fs throw MODULE_NOT_FOUND at load. Use '
|
|
57
|
+
+ "fileURLToPath(new URL('..', import.meta.url)) from node:url, or a relative require via "
|
|
58
|
+
+ 'createRequire(import.meta.url).',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { violations, scanned };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function checkTestsUseFileUrlToPath({ files, read } = {}) {
|
|
66
|
+
const list = files || trackedTestFiles();
|
|
67
|
+
if (!list.length) {
|
|
68
|
+
return {
|
|
69
|
+
name: NAME, ok: false, hardFail: true,
|
|
70
|
+
violations: ['scan defect — enumerated 0 tracked test files; a broken enumeration, not a clean result.'],
|
|
71
|
+
warnings: [], note: 'static scan of tests/ for a URL.pathname used as a filesystem path.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const reader = read || ((rel) => {
|
|
75
|
+
try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return null; }
|
|
76
|
+
});
|
|
77
|
+
const { violations, scanned } = scanTests(list, reader);
|
|
78
|
+
return {
|
|
79
|
+
name: NAME,
|
|
80
|
+
ok: violations.length === 0,
|
|
81
|
+
hardFail: violations.length > 0,
|
|
82
|
+
violations,
|
|
83
|
+
warnings: [],
|
|
84
|
+
note: `${scanned} test file(s) scanned: none may hand \`import.meta.url).pathname\` to require()/fs — `
|
|
85
|
+
+ 'it keeps %20 and a Windows drive slash, so the file is unrunnable on a checkout with a space '
|
|
86
|
+
+ 'in its path while CI (no space) stays green. fileURLToPath() or a relative require is the form.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main() {
|
|
91
|
+
const r = checkTestsUseFileUrlToPath();
|
|
92
|
+
for (const v of r.violations) console.error(`✗ ${v}`);
|
|
93
|
+
console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
|
|
94
|
+
process.exit(r.hardFail ? 1 : 0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (require.main === module) main();
|
|
98
|
+
|
|
99
|
+
module.exports = { checkTestsUseFileUrlToPath, scanTests, trackedTestFiles, PATHNAME_ROOT };
|
|
@@ -261,6 +261,7 @@ async function maybeSeatFirstAdmin(builder) {
|
|
|
261
261
|
const seated = await seatFirstAdminIfUnclaimed(builder.id, builder.github_login);
|
|
262
262
|
if (seated) {
|
|
263
263
|
log.info(`[auth] first-admin bootstrap: seated ${builder.github_login} as Archon (configured firstAdmin, no prior Archon).`);
|
|
264
|
+
await seedKickoffChecklist(builder.id);
|
|
264
265
|
return { ...builder, rank: 'archon' };
|
|
265
266
|
}
|
|
266
267
|
} catch (err) {
|
|
@@ -269,6 +270,27 @@ async function maybeSeatFirstAdmin(builder) {
|
|
|
269
270
|
return builder;
|
|
270
271
|
}
|
|
271
272
|
|
|
273
|
+
// seedKickoffChecklist — the founder's first board (task 1003239). A hub-provisioned
|
|
274
|
+
// project reaches its first sign-in with an EMPTY tasks table (the scaffold ran init
|
|
275
|
+
// --no-seed; no Archon existed to file anything), so the moment the founder is seated
|
|
276
|
+
// is the first moment the kickoff checklist CAN be filed. Resolved through the
|
|
277
|
+
// `lifecycle.kickoffSeed` port — this is a KERNEL_FILE and may not require a module
|
|
278
|
+
// (ADR 0083 / ADR 0091 §1); the port itself skips a board that already has tasks.
|
|
279
|
+
// BEST-EFFORT: a seed hiccup must never fail the sign-in that just seated the owner.
|
|
280
|
+
// deps.port is a test seam; production resolves the seam registry.
|
|
281
|
+
async function seedKickoffChecklist(builderId, deps = {}) {
|
|
282
|
+
const port = deps.port !== undefined ? deps.port : seams.resolveOptional('lifecycle.kickoffSeed');
|
|
283
|
+
if (!port) return null;
|
|
284
|
+
try {
|
|
285
|
+
const r = await port.seedIfEmpty({ builderId });
|
|
286
|
+
if (r && r.seeded) log.info(`[auth] first-admin bootstrap: filed ${r.seeded} kickoff task(s) under ${r.versionId} — the founder's first board.`);
|
|
287
|
+
return r;
|
|
288
|
+
} catch (err) {
|
|
289
|
+
log.error(`[auth] first-admin bootstrap: kickoff seed failed (non-fatal): ${err.message}`);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
272
294
|
// safeSyncRankRole — give a builder the seeded rank-role matching their rank at
|
|
273
295
|
// REGISTRATION (BV1.R95b, ADR 0151). governance_002's builder→role backfill is
|
|
274
296
|
// run-once, so before this every builder admitted after that migration had no role
|
|
@@ -303,6 +325,7 @@ module.exports = {
|
|
|
303
325
|
firstTimeAdmissionBlocked,
|
|
304
326
|
joinabilityMode,
|
|
305
327
|
maybeSeatFirstAdmin,
|
|
328
|
+
seedKickoffChecklist,
|
|
306
329
|
membershipKind,
|
|
307
330
|
openEnrollmentEnabled,
|
|
308
331
|
reportActivityRollup,
|
package/src/module-api.js
CHANGED
|
@@ -1190,8 +1190,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1190
1190
|
// ADR 0161 §2 is filed as an idea from task 1003044.
|
|
1191
1191
|
// 1.19.375 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1192
1192
|
// landed since 1.19.374 with no explicit bump. run 32909701598. (task 1002620)
|
|
1193
|
+
// 1.19.376 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1194
|
+
// landed since 1.19.375 with no explicit bump. run 32910331206. (task 1002620)
|
|
1195
|
+
// 1.19.377 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1196
|
+
// landed since 1.19.376 with no explicit bump. run 33025011131. (task 1002620)
|
|
1193
1197
|
// ---------------------------------------------------------------------------
|
|
1194
|
-
const CORE_VERSION = '1.19.
|
|
1198
|
+
const CORE_VERSION = '1.19.377'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1195
1199
|
|
|
1196
1200
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1197
1201
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// tests/first_admin_kickoff_seed.mjs — the kernel side of task 1003239: the first-admin
|
|
2
|
+
// bootstrap files the founder's first board through the `lifecycle.kickoffSeed` port,
|
|
3
|
+
// and NOTHING about that can fail the sign-in that just seated the owner.
|
|
4
|
+
//
|
|
5
|
+
// DB-free: the helper is exercised with an injected port (deps.port), the same seam the
|
|
6
|
+
// production call resolves from the registry. Covers the three shapes the port can take
|
|
7
|
+
// at runtime — absent (an instance without the lifecycle module), throwing, seeding.
|
|
8
|
+
//
|
|
9
|
+
// Run: node --test tests/first_admin_kickoff_seed.mjs
|
|
10
|
+
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { seedKickoffChecklist } = require('../src/bongos/auth-admission.js');
|
|
17
|
+
const seams = require('../src/module-seams.js');
|
|
18
|
+
|
|
19
|
+
test('no port registered → a quiet null, never a throw (an instance without lifecycle signs in as before)', async () => {
|
|
20
|
+
assert.equal(seams.hasProvider('lifecycle.kickoffSeed'), false, 'nothing in-process registered the port');
|
|
21
|
+
assert.equal(await seedKickoffChecklist(1), null);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('an absent port passed explicitly is the same quiet null', async () => {
|
|
25
|
+
assert.equal(await seedKickoffChecklist(1, { port: null }), null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('a throwing port is swallowed — best-effort, the seat stands', async () => {
|
|
29
|
+
const port = { seedIfEmpty: async () => { throw new Error('db went away'); } };
|
|
30
|
+
assert.equal(await seedKickoffChecklist(1, { port }), null);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('a seeding port is handed the seated builder id and its result flows back', async () => {
|
|
34
|
+
const calls = [];
|
|
35
|
+
const port = { seedIfEmpty: async (arg) => { calls.push(arg); return { seeded: 3, versionId: 'V1' }; } };
|
|
36
|
+
const r = await seedKickoffChecklist(42, { port });
|
|
37
|
+
assert.deepEqual(calls, [{ builderId: 42 }]);
|
|
38
|
+
assert.deepEqual(r, { seeded: 3, versionId: 'V1' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('a skipping port (board already populated) flows back untouched', async () => {
|
|
42
|
+
const port = { seedIfEmpty: async () => ({ seeded: 0, skipped: 'tasks_exist' }) };
|
|
43
|
+
assert.deepEqual(await seedKickoffChecklist(42, { port }), { seeded: 0, skipped: 'tasks_exist' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('production resolves the port from the seam registry', async () => {
|
|
47
|
+
const calls = [];
|
|
48
|
+
seams.registerProvider('lifecycle.kickoffSeed', { seedIfEmpty: async (arg) => { calls.push(arg); return { seeded: 1, versionId: 'V1' }; } });
|
|
49
|
+
try {
|
|
50
|
+
const r = await seedKickoffChecklist(9);
|
|
51
|
+
assert.deepEqual(calls, [{ builderId: 9 }]);
|
|
52
|
+
assert.equal(r.seeded, 1);
|
|
53
|
+
} finally {
|
|
54
|
+
seams._reset();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// tests/goal_sort_staleness.mjs — find your own work, and see what is rotting
|
|
2
|
+
// (task 1003278, goal 1000072, criterion legibility-sort-and-staleness).
|
|
3
|
+
//
|
|
4
|
+
// Three halves. (1) The shared vocabulary: ONE activity clock and ONE staleness
|
|
5
|
+
// predicate in task-lib.js, consumed by both tiers — pinned by executing them.
|
|
6
|
+
// (2) Attribution: tasks.created_by is stamped from the authenticated request on
|
|
7
|
+
// every human create-vector and NEVER backfilled or body-supplied. (3) THE SCALE
|
|
8
|
+
// CHECK, which the criterion makes the acceptance test ("~1,357 tasks on
|
|
9
|
+
// BONGOS-V1 alone. A view that does not survive that number is not a solution"):
|
|
10
|
+
// the board's filter+sort pipeline is EXECUTED over a synthetic corpus of that
|
|
11
|
+
// size and measured, not asserted about.
|
|
12
|
+
//
|
|
13
|
+
// Run: node tests/goal_sort_staleness.mjs
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { makeRunner } from './helpers.mjs';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
24
|
+
const taskLib = require(path.join(ROOT, 'modules/hall-ui/public/task-lib.js'));
|
|
25
|
+
const WORK_SRC = read('modules/hall-ui/public/work.js');
|
|
26
|
+
const RENDER_SRC = read('modules/hall-ui/public/goals-render.js');
|
|
27
|
+
const GOALS_LIB_SRC = read('modules/hall-ui/public/goals-lib.js');
|
|
28
|
+
const MIGRATION = read('migrations/core_227_task_created_by.sql');
|
|
29
|
+
const DB_TASKS = read('modules/lifecycle/db-tasks.js');
|
|
30
|
+
|
|
31
|
+
const { test, summary } = makeRunner();
|
|
32
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
33
|
+
|
|
34
|
+
// ---- the shared vocabulary --------------------------------------------------
|
|
35
|
+
|
|
36
|
+
await test('one activity clock: updated_at ∨ shipped_at, whichever is later', () => {
|
|
37
|
+
const { taskActivityMs, msOf } = taskLib;
|
|
38
|
+
const t = { updated_at: '2026-08-01T00:00:00Z', shipped_at: '2026-08-20T00:00:00Z' };
|
|
39
|
+
assert.equal(taskActivityMs(t), msOf('2026-08-20T00:00:00Z'),
|
|
40
|
+
'shipped_at folded in — a ship path can stamp it without touching updated_at (task 1002965)');
|
|
41
|
+
assert.equal(taskActivityMs({}), 0, 'no readable timestamp reads as epoch — the stalest possible');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await test('isStale: past the bucket is stale, inside it is not, and undated rows are STALE', () => {
|
|
45
|
+
const { isStale } = taskLib;
|
|
46
|
+
const now = Date.parse('2026-08-25T00:00:00Z');
|
|
47
|
+
const at = (daysAgo) => now - daysAgo * DAY;
|
|
48
|
+
assert.equal(isStale(at(31), '30', now), true);
|
|
49
|
+
assert.equal(isStale(at(29), '30', now), false);
|
|
50
|
+
assert.equal(isStale(at(29), '14', now), true);
|
|
51
|
+
assert.equal(isStale(at(5), '', now), true, "'' = no filter — everything passes");
|
|
52
|
+
assert.equal(isStale(0, '60', now), true,
|
|
53
|
+
'a row nobody can date is exactly what a rot-hunt must not hide');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await test('goals-lib DELEGATES to task-lib — one definition, not a synced pair', () => {
|
|
57
|
+
assert.match(GOALS_LIB_SRC, /const \{ msOf, taskActivityMs \} = window\.OTBTaskLib;/,
|
|
58
|
+
'the private copies were removed; a drift here would mean two meanings of "touched"');
|
|
59
|
+
assert.equal(/Date\.parse/.test(GOALS_LIB_SRC), false, 'no residual private clock');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await test('both tiers offer the SAME staleness buckets', () => {
|
|
63
|
+
assert.match(WORK_SRC, /\{ id: 'f-stale', label: 'Age', options: STALE_CHOICES \}/);
|
|
64
|
+
assert.match(read('modules/hall-ui/public/goals-page.js'), /\{ id: 'f-goal-stale', label: 'Age', options: window\.OTBTaskLib\.STALE_CHOICES \}/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await test("the goals page's stale FILTER and its 'recently active' SORT read one clock", () => {
|
|
68
|
+
const body = RENDER_SRC.slice(RENDER_SRC.indexOf('function filteredGoals'));
|
|
69
|
+
assert.match(body.slice(0, 2400), /isStale\(goalActivityMs\(g\), staleDays\)/,
|
|
70
|
+
'the filter reads goalActivityMs — the same function the active/stale sorts compare on');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ---- attribution ------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
await test('created_by comes from the AUTHENTICATED request, never the body', () => {
|
|
76
|
+
// task-write-routes stamps req.builder.id; the body validator must NOT accept a
|
|
77
|
+
// created_by key — who created a task is a fact about the caller, not an input.
|
|
78
|
+
const routes = read('modules/lifecycle/routes/task-write-routes.js');
|
|
79
|
+
assert.match(routes, /createdBy: req\.builder\.id/);
|
|
80
|
+
assert.equal(/created_by: \{/.test(routes), false, 'not a validatable body field');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await test('the goal-authored and idea-route vectors attribute too', () => {
|
|
84
|
+
assert.match(read('modules/lifecycle/routes/goals.js'), /createdBy: req\.builder\.id/);
|
|
85
|
+
assert.match(read('modules/ideas/inbox.js'), /createdBy: capturedBy \?\? null/,
|
|
86
|
+
'routing an idea attributes the task to the person whose idea it was');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await test('history is never backfilled — a guessed attribution is a confident lie', () => {
|
|
90
|
+
assert.match(MIGRATION, /ADD COLUMN IF NOT EXISTS created_by bigint REFERENCES builders\(id\)/);
|
|
91
|
+
assert.equal(/UPDATE tasks\s+SET created_by/i.test(MIGRATION), false,
|
|
92
|
+
'NULL = created before attribution existed; the goals.created_by convention');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await test('the t.* views are rebuilt WITH the column — the core_190 lesson', () => {
|
|
96
|
+
// A Postgres view pins `SELECT t.*` at CREATE time; without the rebuild the
|
|
97
|
+
// Creator filter would read undefined off the claim feed for every row — the
|
|
98
|
+
// exact silent bug core_190 existed to repair.
|
|
99
|
+
assert.match(MIGRATION, /DROP VIEW IF EXISTS claimable_tasks;/);
|
|
100
|
+
assert.match(MIGRATION, /DROP VIEW IF EXISTS tasks_session_fit;/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await test('createTask appends createdBy at the end of the bind list (the goals convention)', () => {
|
|
104
|
+
assert.match(DB_TASKS, /goal_id, created_by, promoted_at\)/,
|
|
105
|
+
'new columns go on the end so pinned bind orders survive');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---- the mixed option form is supported, not a latent render bug ------------
|
|
109
|
+
|
|
110
|
+
await test('a filter control may mix string and {value,label} options — the kit normalises', () => {
|
|
111
|
+
// Raised in review as "will likely fail to render". It does not: hall-kit.js
|
|
112
|
+
// declares `options?: [{value,label}|string]` and normalises through optVal /
|
|
113
|
+
// optLabel, and the pre-existing f-discipline and f-goal-status controls have
|
|
114
|
+
// always used the mixed form. Pinned here so the question is settled in code.
|
|
115
|
+
const kit = read('modules/hall-ui/public/hall-kit.js');
|
|
116
|
+
assert.match(kit, /function optVal\(o\) \{ return typeof o === 'string' \? o : o\.value; \}/);
|
|
117
|
+
assert.match(kit, /function optLabel\(o\) \{ return typeof o === 'string' \? o : o\.label; \}/);
|
|
118
|
+
assert.match(WORK_SRC, /id: 'f-discipline',[\s\S]{0,200}'engineer', 'artist', 'ideator', 'ui', 'unclassified'/,
|
|
119
|
+
'the mixed form predates this task on the very same board');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ---- the payload, which is where the scale actually bit ---------------------
|
|
123
|
+
|
|
124
|
+
await test('the Goals page no longer downloads the version to count it', () => {
|
|
125
|
+
// MEASURED on the real corpus, 2026-08-25: GET /tasks?version=BONGOS-V1&include=deps
|
|
126
|
+
// returned 5,229,455 bytes for ~1,400 tasks to produce ~40 counter objects.
|
|
127
|
+
// GET /goals/rollups returns one small row per goal instead.
|
|
128
|
+
const page = read('modules/hall-ui/public/goals-page.js');
|
|
129
|
+
const body = page.slice(page.indexOf('async function loadRollups'), page.indexOf('/* ---------------- list view'));
|
|
130
|
+
assert.match(body, /getJSON\(`\$\{API\}\/goals\/rollups\?version=/);
|
|
131
|
+
assert.equal(/tasks\?version=/.test(body), false,
|
|
132
|
+
'not even as a fallback — a fallback that quietly re-downloads 5 MB is the bug wearing a safety belt');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await test('the Backlog read is BOUNDED — it was SELECT * FROM tasks WHERE status=backlog', () => {
|
|
136
|
+
assert.match(WORK_SRC, /const BACKLOG_LIMIT = \d+;/);
|
|
137
|
+
assert.match(WORK_SRC, /tasks\?status=backlog&limit=\$\{BACKLOG_LIMIT\}/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await test('the server offers the axes, so a board can ask a narrow question', () => {
|
|
141
|
+
const routes = read('modules/lifecycle/routes/tasks.js');
|
|
142
|
+
for (const p of ['created_by', 'stale_days', 'limit', 'offset']) {
|
|
143
|
+
assert.ok(routes.includes(p), `GET /tasks must accept ${p}`);
|
|
144
|
+
}
|
|
145
|
+
assert.match(routes, /limit must be a positive integer up to 1000/,
|
|
146
|
+
'an uncapped limit would be the whole-table read this axis exists to stop');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await test("'none' selects the unattributed server-side, not a missing filter", () => {
|
|
150
|
+
const dbTasks = read('modules/lifecycle/db-tasks.js');
|
|
151
|
+
assert.match(dbTasks, /if \(createdBy === 'none'\) \{\s*\n\s*where\.push\('created_by IS NULL'\);/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await test('the SQL rollup buckets MIRROR goals-lib readinessOf exactly', () => {
|
|
155
|
+
// A divergence here shows a person different numbers depending on which
|
|
156
|
+
// surface they opened, which is the opposite of what this goal is for.
|
|
157
|
+
const rollup = read('modules/lifecycle/goal-rollup.js');
|
|
158
|
+
const lib = GOALS_LIB_SRC;
|
|
159
|
+
// done + cut vocabularies
|
|
160
|
+
assert.match(rollup, /const DONE_STATUSES = \['shipped', 'confirmed', 'completed'\]/);
|
|
161
|
+
assert.match(lib, /DONE_STATUSES = new Set\(\['completed', 'confirmed', 'shipped'\]\)/);
|
|
162
|
+
assert.match(rollup, /const CUT_STATUSES = \['abandoned', 'dropped'\]/);
|
|
163
|
+
assert.match(lib, /CUT_STATUSES = new Set\(\['abandoned', 'dropped'\]\)/);
|
|
164
|
+
// blocked = backlog with >=1 unshipped dependency, both sides
|
|
165
|
+
assert.match(rollup, /WHEN s\.status = 'backlog' AND s\.has_unshipped_dep THEN 'blocked'/);
|
|
166
|
+
assert.match(lib, /s === 'backlog' && \(t\.blocked_by \|\| \[\]\)\.some\(\(d\) => !d\.satisfied\)/);
|
|
167
|
+
// cut is excluded from the total on both sides (migration 017's rule)
|
|
168
|
+
assert.match(rollup, /FILTER \(WHERE b\.bucket <> 'cut'\)::int\s+AS total/);
|
|
169
|
+
assert.match(read('modules/hall-ui/public/goals-page.js'), /total: row\.total \|\| 0/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await test('the rollup activity clock is the SAME max(updated_at, shipped_at)', () => {
|
|
173
|
+
const rollup = read('modules/lifecycle/goal-rollup.js');
|
|
174
|
+
assert.match(rollup, /GREATEST\(COALESCE\(t\.updated_at, 'epoch'::timestamptz\),\s*\n?\s*COALESCE\(t\.shipped_at, 'epoch'::timestamptz\)\)/);
|
|
175
|
+
assert.match(read('modules/lifecycle/db-tasks.js'), /GREATEST\(COALESCE\(updated_at, 'epoch'::timestamptz\), COALESCE\(shipped_at, 'epoch'::timestamptz\)\)/,
|
|
176
|
+
"the server's stale_days filter reads the same clock as the rollup and the clients");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ---- the board asks the SERVER the narrow question --------------------------
|
|
180
|
+
|
|
181
|
+
await test('Creator and Age are sent to the server, on BOTH board feeds', () => {
|
|
182
|
+
assert.match(WORK_SRC, /function serverFilterQuery\(\)/);
|
|
183
|
+
assert.match(WORK_SRC, /tasks\/claimable\$\{serverFilterQuery\(\)\}/,
|
|
184
|
+
'the claimable feed narrows at the source, not in the browser');
|
|
185
|
+
assert.match(WORK_SRC, /tasks\?status=backlog&limit=\$\{BACKLOG_LIMIT\}\$\{q\}/,
|
|
186
|
+
'and so does the backlog read');
|
|
187
|
+
const routes = read('modules/lifecycle/routes/tasks.js');
|
|
188
|
+
const claimable = routes.slice(routes.indexOf("router.get('/tasks/claimable'"));
|
|
189
|
+
assert.match(claimable.slice(0, 2500), /createdBy: claimCreatedBy \?\? null/);
|
|
190
|
+
assert.match(claimable.slice(0, 2500), /staleDays: claimStaleDays \?\? null/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await test('a server-side filter change RE-FETCHES; every other filter stays instant', () => {
|
|
194
|
+
// makeFilterBar hands onChange every value, not the changed id (hall-kit.js),
|
|
195
|
+
// so the decision is made by comparing the server-facing query — which is the
|
|
196
|
+
// direct question anyway, and means a control absent from that query can never
|
|
197
|
+
// cost a round-trip.
|
|
198
|
+
assert.match(WORK_SRC, /let lastServerFilterQuery = '';/);
|
|
199
|
+
assert.match(WORK_SRC, /if \(q !== lastServerFilterQuery\) \{[\s\S]{0,200}refreshLive\(\);\s*\n\s*loadBacklog\(\);/);
|
|
200
|
+
const kit = read('modules/hall-ui/public/hall-kit.js');
|
|
201
|
+
assert.match(kit, /if \(onChange\) onChange\(values\(\)\);/,
|
|
202
|
+
'the contract this decision depends on — if it ever hands the changed id instead, revisit');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await test('one staleness rule, four expressions, none of them drifting', () => {
|
|
206
|
+
const clock = /GREATEST\(COALESCE\((?:v\.|t\.)?updated_at, 'epoch'::timestamptz\),\s*\n?\s*COALESCE\((?:v\.|t\.)?shipped_at, 'epoch'::timestamptz\)\)/;
|
|
207
|
+
const dbTasks = read('modules/lifecycle/db-tasks.js');
|
|
208
|
+
assert.match(dbTasks, clock, 'listTasks');
|
|
209
|
+
assert.ok((dbTasks.match(new RegExp(clock.source, 'g')) || []).length >= 2, 'and listClaimableTasks');
|
|
210
|
+
assert.match(read('modules/lifecycle/goal-rollup.js'), clock, 'the goal rollup');
|
|
211
|
+
assert.equal(taskLib.taskActivityMs({ updated_at: '2026-01-01T00:00:00Z', shipped_at: '2026-02-01T00:00:00Z' }),
|
|
212
|
+
Date.parse('2026-02-01T00:00:00Z'), 'and the browser');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ---- privacy -----------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
await test('a rollup never hands a private goal\'s task title to a non-member', () => {
|
|
218
|
+
// Counts on a private goal are not the secret — its prose is (ADR 0112's leak
|
|
219
|
+
// rule). next_ready_title is a real task title, so it is gated on membership
|
|
220
|
+
// or Archon; the counters still come back, so the board renders the ring and
|
|
221
|
+
// the readiness line either way.
|
|
222
|
+
const rollup = read('modules/lifecycle/goal-rollup.js');
|
|
223
|
+
assert.match(rollup, /CASE WHEN bool_or\(g\.visibility IS DISTINCT FROM 'private'\)/);
|
|
224
|
+
assert.match(rollup, /bool_or\(gm\.builder_id IS NOT NULL\)/, 'membership is what unlocks it');
|
|
225
|
+
assert.match(rollup, /THEN MAX\(nr\.title\) END\s+AS next_ready_title/);
|
|
226
|
+
const routes = read('modules/lifecycle/routes/goals.js');
|
|
227
|
+
assert.match(routes, /viewerId: req\.builder\.id/, 'identity from the request, never the query');
|
|
228
|
+
assert.match(routes, /viewerIsArchon: req\.builder\.rank === 'archon'/);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await test('limit and offset are BOUND, not interpolated', () => {
|
|
232
|
+
const dbTasks = read('modules/lifecycle/db-tasks.js');
|
|
233
|
+
assert.match(dbTasks, /tail \+= ` LIMIT \$\$\{params\.length\}`/);
|
|
234
|
+
assert.equal(/LIMIT \$\{Number\(limit\)\}/.test(dbTasks), false,
|
|
235
|
+
'a query builder that is only safe because of who calls it is one careless caller from unsafe');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ---- the scale check: EXECUTED, not asserted --------------------------------
|
|
239
|
+
|
|
240
|
+
await test('the board pipeline survives the real corpus size (~1,400 rows), measured', () => {
|
|
241
|
+
// Reproduce the board's per-render work — filter (creator + staleness + the
|
|
242
|
+
// existing axes) then sort — over a corpus the size of BONGOS-V1's, and hold it
|
|
243
|
+
// to a budget. The budget is generous (well under one frame's headroom on this
|
|
244
|
+
// CI box) because the point is the ORDER of the cost, not a microbenchmark: a
|
|
245
|
+
// rewrite that makes this quadratic or fetch-per-row breaks the assert.
|
|
246
|
+
const { taskActivityMs, isStale } = taskLib;
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const corpus = Array.from({ length: 1400 }, (_, i) => ({
|
|
249
|
+
id: i + 1,
|
|
250
|
+
title: `Task number ${i} of the corpus`,
|
|
251
|
+
version_id: 'BONGOS-V1',
|
|
252
|
+
kind: ['feature', 'bug', 'infra'][i % 3],
|
|
253
|
+
discipline: ['engineer', 'artist', 'ideator'][i % 3],
|
|
254
|
+
created_by: i % 5 === 0 ? null : String(1000000 + (i % 7)),
|
|
255
|
+
goal_id: String(1000000 + (i % 40)),
|
|
256
|
+
updated_at: new Date(now - (i % 90) * DAY).toISOString(),
|
|
257
|
+
shipped_at: null,
|
|
258
|
+
credits_reward: i % 50,
|
|
259
|
+
est_minutes: 30 + (i % 120),
|
|
260
|
+
priority: (i % 5) + 1,
|
|
261
|
+
newcomer_friendly: i % 11 === 0,
|
|
262
|
+
}));
|
|
263
|
+
const t0 = process.hrtime.bigint();
|
|
264
|
+
let visible;
|
|
265
|
+
for (let pass = 0; pass < 10; pass++) {
|
|
266
|
+
visible = corpus
|
|
267
|
+
.filter((t) => String(t.created_by) === '1000003' || t.created_by == null)
|
|
268
|
+
.filter((t) => isStale(taskActivityMs(t), '30', now))
|
|
269
|
+
.sort((a, b) => taskActivityMs(a) - taskActivityMs(b));
|
|
270
|
+
}
|
|
271
|
+
const perPassMs = Number(process.hrtime.bigint() - t0) / 1e6 / 10;
|
|
272
|
+
assert.ok(visible.length > 0, 'sanity: the synthetic filters match something');
|
|
273
|
+
assert.ok(perPassMs < 50, `one filter+sort pass over 1,400 rows took ${perPassMs.toFixed(2)}ms — the view no longer survives the corpus`);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await test('the Tasks board still loads the corpus in a bounded number of requests', () => {
|
|
277
|
+
// The board's data story: claimable + backlog + one /goals read. The new axes
|
|
278
|
+
// must not have added a per-row or per-goal fetch. Count the getJSON call
|
|
279
|
+
// sites that run at boot — the creator labels ride ONE directory read.
|
|
280
|
+
const boots = WORK_SRC.match(/getJSON\(`\$\{API\}\/(tasks\/claimable|tasks\?status=backlog|goals|builders\/directory)`\)/g) || [];
|
|
281
|
+
assert.ok(boots.length <= 5, `bounded boot reads — got ${boots.length}`);
|
|
282
|
+
assert.equal(/getJSON\(`\$\{API\}\/tasks\/\$\{t\.id\}/.test(WORK_SRC.slice(WORK_SRC.indexOf('populateCreatorFilter'))), false,
|
|
283
|
+
'no per-task fetch crept into the new filter');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
summary();
|
package/tests/goal_subtitle.mjs
CHANGED
|
@@ -154,8 +154,13 @@ test("Claude's prose is a CLOSED disclosure, not visible body text", () => {
|
|
|
154
154
|
const el = () => ({ textContent: null, hidden: null, open: null });
|
|
155
155
|
function applyProse(goal) {
|
|
156
156
|
const lib = read('modules', 'hall-ui', 'public', 'goals-lib.js');
|
|
157
|
+
const taskLib = read('modules', 'hall-ui', 'public', 'task-lib.js');
|
|
157
158
|
const els = { sub: el(), desc: el(), claude: el() };
|
|
158
|
-
|
|
159
|
+
// task 1003278: goals-lib takes its activity clock from window.OTBTaskLib (one
|
|
160
|
+
// meaning of "touched" across both tiers), so the sibling loads first — the
|
|
161
|
+
// order goals.html uses. Loading the REAL one, not a stub: a stub that always
|
|
162
|
+
// agrees is exactly what a shared definition must not be tested against.
|
|
163
|
+
const fn = new Function('window', `${taskLib}; ${lib}; return window.OTBGoalsLib.applyGoalProse;`)({ OTB: { escapeHtml: (x) => x } });
|
|
159
164
|
fn(els, goal);
|
|
160
165
|
return els;
|
|
161
166
|
}
|