@bongos/core 1.19.826 → 1.19.828

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.
@@ -0,0 +1,107 @@
1
+ // tests/blockers_hall.mjs — task 1003455 (goal 1000089): "Blockers become
2
+ // visible: the queue page and the attention slice, from one code path."
3
+ //
4
+ // GET /blockers had zero hall consumers before this task. Done-when: the queue
5
+ // renders on a real page from GET /blockers, the attention inbox shows open
6
+ // blockers as line items, BOTH read the same renderer (blockers-lib.js, pinned
7
+ // separately in tests/blockers_lib.mjs), and the page's list is paged through
8
+ // OTBKit rather than hand-rolled. This file pins the WIRING — that both
9
+ // consumers actually call the shared lib and the real API, and that the page
10
+ // uses the kit's pager rather than reimplementing slicing.
11
+ //
12
+ // Source-regex style, matching tests/rot_inbox.mjs's checks on the same file —
13
+ // pure text assertions over the real client source, no DOM/vm harness needed for
14
+ // what this pins (presence + call shape, not runtime behaviour).
15
+ //
16
+ // Run: node tests/blockers_hall.mjs
17
+
18
+ import { strict as assert } from 'node:assert';
19
+ import { test } from 'node:test';
20
+ import { readFileSync } from 'node:fs';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { dirname, join } from 'node:path';
23
+
24
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
25
+ const HALL = join(ROOT, 'modules/hall-ui/public');
26
+ const GOAL_INBOX_SRC = readFileSync(join(HALL, 'goal-inbox.js'), 'utf8');
27
+ const BLOCKERS_JS_SRC = readFileSync(join(HALL, 'blockers.js'), 'utf8');
28
+ const BLOCKERS_HTML_SRC = readFileSync(join(HALL, 'blockers.html'), 'utf8');
29
+ const INDEX_HTML_SRC = readFileSync(join(HALL, 'index.html'), 'utf8');
30
+
31
+ // ---- the attention-inbox slice (goal-inbox.js) -----------------------------
32
+
33
+ test('goal-inbox.js carries a blockers slice div in the SAME needs-attention card', () => {
34
+ assert.match(GOAL_INBOX_SRC, /<div id="attention-blockers"><\/div>/,
35
+ 'a second Home card would violate the placement rule (hall-ui/CLAUDE.md): an action awaiting a human is an inbox line, never a new card');
36
+ });
37
+
38
+ test('goal-inbox.js reads the real GET /blockers route, not a stand-in', () => {
39
+ assert.match(GOAL_INBOX_SRC, /getJSON\(`\$\{API\}\/blockers/);
40
+ });
41
+
42
+ test('goal-inbox.js asks the server for a BOUNDED read and reports the total, not fetch-all-then-slice', () => {
43
+ // The "Home bounded reads" rule this module's own CLAUDE.md names: a capped
44
+ // Home card must ask for exactly what it shows and get a total back, never
45
+ // download the whole feed and .slice() it client-side.
46
+ assert.match(GOAL_INBOX_SRC, /\/blockers\?limit=\$\{MAX_BLOCKER_ROWS\}/);
47
+ assert.match(GOAL_INBOX_SRC, /data\.page\.total/);
48
+ });
49
+
50
+ test('goal-inbox.js renders blocker rows through the SHARED lib, not a re-implementation', () => {
51
+ assert.match(GOAL_INBOX_SRC, /window\.OTBBlockersLib/);
52
+ assert.match(GOAL_INBOX_SRC, /lib\.blockerFacts\(/);
53
+ });
54
+
55
+ test('the blocker row carries .ginbox__item, so the card\'s count-pill and visibility include it', () => {
56
+ assert.match(GOAL_INBOX_SRC, /function blockerRowHtml[\s\S]*?class="ginbox__item"/);
57
+ });
58
+
59
+ test('the mount() boot sequence actually calls loadBlockers()', () => {
60
+ assert.match(GOAL_INBOX_SRC, /loadNodQueue\(\);\s*\n\s*loadBlockers\(\);\s*\n\s*loadRot\(\);/);
61
+ });
62
+
63
+ // ---- the full queue page (blockers.html + blockers.js) ---------------------
64
+
65
+ test('blockers.js reads the real GET /blockers route, not a stand-in', () => {
66
+ assert.match(BLOCKERS_JS_SRC, /API \+ '\/blockers/);
67
+ });
68
+
69
+ test('blockers.js renders rows through the SAME shared lib the attention slice uses', () => {
70
+ assert.match(BLOCKERS_JS_SRC, /window\.OTBBlockersLib/);
71
+ assert.match(BLOCKERS_JS_SRC, /lib\.blockerFacts\(/);
72
+ });
73
+
74
+ test('blockers.js pages through OTBKit — makePager/paginate/wirePager/pagerBarHtml — not by hand', () => {
75
+ assert.match(BLOCKERS_JS_SRC, /kit\.makePager\(/);
76
+ assert.match(BLOCKERS_JS_SRC, /kit\.paginate\(/);
77
+ assert.match(BLOCKERS_JS_SRC, /kit\.wirePager\(/);
78
+ assert.match(BLOCKERS_JS_SRC, /kit\.pagerBarHtml\(/);
79
+ // The negative half: no hand-rolled slice math (start/end arithmetic) sitting
80
+ // beside the kit call, which is what "paged through OTBKit rather than
81
+ // hand-rolled" would otherwise silently tolerate.
82
+ assert.doesNotMatch(BLOCKERS_JS_SRC, /\.slice\(\s*\(?\s*pager\.page/);
83
+ });
84
+
85
+ test('blockers.html loads blockers-lib.js BEFORE blockers.js', () => {
86
+ const libAt = BLOCKERS_HTML_SRC.indexOf('src="/builders/blockers-lib.js"');
87
+ const pageAt = BLOCKERS_HTML_SRC.indexOf('src="/builders/blockers.js"');
88
+ assert.ok(libAt > -1 && pageAt > -1, 'both scripts must be present');
89
+ assert.ok(libAt < pageAt, 'blockers.js calls window.OTBBlockersLib at load time');
90
+ });
91
+
92
+ test('index.html loads blockers-lib.js BEFORE goal-inbox.js', () => {
93
+ // The literal <script src> attribute, not a bare filename mention — a
94
+ // surrounding comment is free to name either file in either order (and does:
95
+ // profile-nudge.js's own comment mentions "goal-inbox.js" ahead of the real
96
+ // goal-inbox.js <script> tag), so only the src strings say what actually loads
97
+ // first.
98
+ const libAt = INDEX_HTML_SRC.indexOf('src="/builders/blockers-lib.js"');
99
+ const inboxAt = INDEX_HTML_SRC.indexOf('src="/builders/goal-inbox.js');
100
+ assert.ok(libAt > -1 && inboxAt > -1, 'both scripts must be present');
101
+ assert.ok(libAt < inboxAt);
102
+ });
103
+
104
+ test('every title link opens the SAME #/blocker/N overlay task-detail.js already renders', () => {
105
+ assert.match(GOAL_INBOX_SRC, /`#\/blocker\/\$\{encodeURIComponent\(id\)\}`/);
106
+ assert.match(BLOCKERS_JS_SRC, /'#\/blocker\/' \+ encodeURIComponent\(id\)/);
107
+ });
@@ -0,0 +1,102 @@
1
+ // tests/blockers_lib.mjs — the shared blocker-row renderer (blockers-lib.js,
2
+ // task 1003455, goal 1000089).
3
+ //
4
+ // Pins that the lib is genuinely pure (no window.OTB, no DOM) so it loads
5
+ // standalone via require() the way goals-lib.js loads standalone in a vm — and
6
+ // that its output is what both callers (goal-inbox.js's attention-inbox slice,
7
+ // blockers.js's full-queue page) actually depend on: a title link into the
8
+ // EXISTING #/blocker/N overlay (task-detail.js), a quoted note, and a `·`-joined
9
+ // facts line naming which tasks a blocker holds up.
10
+ //
11
+ // Run: node tests/blockers_lib.mjs
12
+
13
+ import { strict as assert } from 'node:assert';
14
+ import { test } from 'node:test';
15
+ import { createRequire } from 'node:module';
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const lib = require('../modules/hall-ui/public/blockers-lib.js');
19
+
20
+ test('loads standalone via require() — no window, no DOM (Node-require-safe like hall-kit.js)', () => {
21
+ assert.ok(lib, 'module.exports must be set');
22
+ for (const k of ['esc', 'fmtDate', 'blockingLine', 'blockerFacts']) {
23
+ assert.ok(k in lib, `missing export: ${k}`);
24
+ }
25
+ });
26
+
27
+ test('blockerFacts: title links into the SAME #/blocker/N overlay task-detail.js renders', () => {
28
+ const b = { id: 42, title: 'Need the owner’s AWS card on file', created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [] };
29
+ const { titleHtml } = lib.blockerFacts(b, { blockerHref: (id) => `#/blocker/${id}` });
30
+ assert.match(titleHtml, /href="#\/blocker\/42"/);
31
+ assert.match(titleHtml, /Need the owner/);
32
+ });
33
+
34
+ test('blockerFacts: with no blockerHref, the title renders as plain text, not a dead link', () => {
35
+ const b = { id: 42, title: 'Something stuck', created_at: null, blocking_task_ids: [] };
36
+ const { titleHtml } = lib.blockerFacts(b);
37
+ assert.doesNotMatch(titleHtml, /<a /);
38
+ assert.match(titleHtml, /<span/);
39
+ });
40
+
41
+ test('blockerFacts: the caller-supplied classes land on the right fragments', () => {
42
+ const b = { id: 1, title: 'x', body_md: 'a reason', created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [] };
43
+ const { titleHtml, noteHtml, factsHtml } = lib.blockerFacts(b, {
44
+ titleClass: 'ginbox__goal', noteClass: 'ginbox__note', whenClass: 'ginbox__sub',
45
+ });
46
+ assert.match(titleHtml, /class="ginbox__goal"/);
47
+ assert.match(noteHtml, /class="ginbox__note"/);
48
+ assert.match(factsHtml, /class="ginbox__sub"/);
49
+ });
50
+
51
+ test('blockingLine: no linked tasks reads honestly, not as a false "0 tasks"', () => {
52
+ assert.equal(lib.blockingLine({ blocking_task_ids: [] }, null), 'Blocking nothing yet');
53
+ assert.equal(lib.blockingLine({}, null), 'Blocking nothing yet');
54
+ });
55
+
56
+ test('blockingLine: singular/plural noun agreement, and every id becomes a link when taskHref is given', () => {
57
+ const one = lib.blockingLine({ blocking_task_ids: [5] }, (id) => `/task/${id}`);
58
+ assert.equal(one, 'Blocking 1 task: <a href="/task/5">#5</a>');
59
+ const many = lib.blockingLine({ blocking_task_ids: [5, 6] }, (id) => `/task/${id}`);
60
+ assert.equal(many, 'Blocking 2 tasks: <a href="/task/5">#5</a>, <a href="/task/6">#6</a>');
61
+ });
62
+
63
+ test('blockingLine: with no taskHref the ids still render, as plain text — never a dead link', () => {
64
+ const line = lib.blockingLine({ blocking_task_ids: [5] }, null);
65
+ assert.equal(line, 'Blocking 1 task: #5');
66
+ assert.doesNotMatch(line, /<a /);
67
+ });
68
+
69
+ test('blockerFacts: the facts line is ONE `·`-joined span, not several ungapped siblings', () => {
70
+ // The bug this shape avoids: two adjacent .ginbox__sub spans have no flex gap
71
+ // between them (unlike .ov-row__facts, which is a flex container) and would
72
+ // render Filed-date and Blocking-line running together with no separator.
73
+ const b = { id: 1, title: 'x', created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [7] };
74
+ const { factsHtml } = lib.blockerFacts(b, { taskHref: (id) => `/task/${id}` });
75
+ assert.equal((factsHtml.match(/<span/g) || []).length, 1, 'exactly one wrapping span');
76
+ assert.match(factsHtml, /Filed .* · Blocking 1 task/);
77
+ });
78
+
79
+ test('blockerFacts: body_md becomes a quoted note; an empty one renders nothing', () => {
80
+ const withNote = lib.blockerFacts({ id: 1, title: 'x', body_md: 'reason', blocking_task_ids: [] });
81
+ assert.match(withNote.noteHtml, /<p class="ov-row__sub">“reason”<\/p>/);
82
+ const noNote = lib.blockerFacts({ id: 1, title: 'x', body_md: ' ', blocking_task_ids: [] });
83
+ assert.equal(noNote.noteHtml, '');
84
+ });
85
+
86
+ test('blockerFacts: a very long body_md is clipped, so one enormous blocker cannot blow up the row', () => {
87
+ const long = 'x'.repeat(500);
88
+ const { noteHtml } = lib.blockerFacts({ id: 1, title: 't', body_md: long, blocking_task_ids: [] });
89
+ assert.ok(noteHtml.length < 500, 'clipped well below the source length');
90
+ assert.match(noteHtml, /…”<\/p>$/);
91
+ });
92
+
93
+ test('blockerFacts: HTML in a title/body/task id is escaped, not injected', () => {
94
+ const b = {
95
+ id: 1, title: '<script>alert(1)</script>', body_md: '<img onerror=alert(1)>',
96
+ created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [1],
97
+ };
98
+ const { titleHtml, noteHtml, factsHtml } = lib.blockerFacts(b, { taskHref: (id) => `/task/"><script>${id}` });
99
+ assert.doesNotMatch(titleHtml, /<script>/);
100
+ assert.doesNotMatch(noteHtml, /<img /);
101
+ assert.doesNotMatch(factsHtml, /<script>/);
102
+ });
@@ -255,7 +255,10 @@ test('sidebar groups read by audience, in order', () => {
255
255
  // (2026-09-09) was to move it: a sitting is work the project does, beside Goals
256
256
  // and Tasks, not one of Government's monitoring surfaces. The item is invisible
257
257
  // to anyone without the atom, so most builders never see it here.
258
- assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'board-room', 'ideas', 'copy-desk', 'studio'],
258
+ // task 1003455 (goal 1000089): 'blockers' joins the same group, after Tasks —
259
+ // its subject is the project (what's stuck on the owner), not a person or a
260
+ // craft, and its one data source (GET /blockers) is public like Roadmap's.
261
+ assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'blockers', 'board-room', 'ideas', 'copy-desk', 'studio'],
259
262
  'the unlabeled group is the project itself (task 1002425 order)');
260
263
  assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
261
264
  // People (task 1002577): the /scouting directory graduated into the hall.
@@ -129,6 +129,14 @@ const ORACLE = {
129
129
  '/builders/drachmae': null,
130
130
  '/builders/profile': null,
131
131
 
132
+ // task 1003455: the blocker queue. GET /blockers is deliberately public (the
133
+ // status dashboard is meant to read it too — "anyone landing on /builders or
134
+ // the status dashboard can see what's stuck on the owner", the route's own
135
+ // handler comment), so the page must not be gated tighter than the API it
136
+ // fronts — the same rule that puts /roster and /drachmae here. The nav link
137
+ // is still cosmetically 'signed', same as those two.
138
+ '/builders/blockers': null,
139
+
132
140
  // The bare detail slugs, gated by task 1003629 — the decision idea 1000907
133
141
  // (task 1003448) asked for. They resolve to the same task.html / idea.html
134
142
  // shells their /:id forms do, so they now sit on the same tier: the id-less
@@ -0,0 +1,208 @@
1
+ // tests/join_dark_matter.mjs — the hub's join relay refuses a project the platform's own
2
+ // record says is unpublished, before it relays anything (task 1003578, ADR 0247 D4).
3
+ //
4
+ // TWO HALVES.
5
+ //
6
+ // Part A — the provider: `provisioning.projectPublishIdentity` reads the provisioning row
7
+ // (the one LIVE copy of a control-plane project's description) keyed on BOTH halves of the
8
+ // ADR 0246 projection, and maps slug → name so the hub's gate never learns this schema.
9
+ //
10
+ // Part B — the real POST /my-projects/join with the port scripted. Only a positive
11
+ // "unpublished" verdict refuses. No port, no row (a self-hosted or hand-seeded project),
12
+ // or a failed read is NO verdict, and the request is relayed exactly as before — the
13
+ // project's own door still decides, and the hub grants nothing by relaying.
14
+ //
15
+ // DB-free. Run: node --test tests/join_dark_matter.mjs
16
+
17
+ import assert from 'node:assert/strict';
18
+ import { test, before, after } from 'node:test';
19
+ import http from 'node:http';
20
+ import { createRequire } from 'node:module';
21
+ import { readFileSync } from 'node:fs';
22
+
23
+ // ESM import so the knip ratchet credits the exports this file consumes
24
+ // (a createRequire() import does not credit a CJS export).
25
+ import { projectPublishIdentity, registerProvisioningSeams } from '../modules/provisioning/seams.js';
26
+
27
+ const require = createRequire(import.meta.url);
28
+ const express = require('express');
29
+
30
+ const PORT_NAME = 'provisioning.projectPublishIdentity';
31
+
32
+ // ── the doorway, with the port scripted ──────────────────────────────────────
33
+
34
+ let portMode = 'present'; // 'present' | 'absent' | 'throws'
35
+ let identity = null; // what the port answers
36
+ let portCalls = [];
37
+ let fetches = [];
38
+ let echoWrites = 0;
39
+
40
+ const pool = {
41
+ query: async (text, params = []) => {
42
+ const sql = String(text).replace(/\s+/g, ' ').trim();
43
+ if (/FROM platform_identity_sso_clients WHERE origin = \$1 AND status = 'active'/.test(sql)) {
44
+ return { rows: params[0] === 'https://Target.example'
45
+ ? [{ client_id: 'proj-open', name: 'Target', origin: 'https://Target.example', status: 'active' }]
46
+ : [] };
47
+ }
48
+ if (/^INSERT INTO platform_identity_application_echoes/.test(sql)) { echoWrites++; return { rows: [] }; }
49
+ throw new Error(`fake pool: unsupported statement — ${sql}`);
50
+ },
51
+ };
52
+
53
+ const apiPath = require.resolve('../src/module-api.js');
54
+ const realApi = require(apiPath);
55
+ require.cache[apiPath].exports = {
56
+ ...realApi,
57
+ pool,
58
+ logger: () => ({ error() {}, warn() {}, info() {}, debug() {} }),
59
+ requireBuilder: (req, _res, next) => {
60
+ req.builder = { id: 1, github_id: 4242, github_login: 'octo', display_name: 'Octo' };
61
+ next();
62
+ },
63
+ resolveOptional: (name, fallback = null) => {
64
+ if (name !== PORT_NAME) return realApi.resolveOptional(name, fallback);
65
+ if (portMode === 'absent') return fallback;
66
+ return async (target) => {
67
+ portCalls.push(target);
68
+ if (portMode === 'throws') throw new Error('provisioning is down');
69
+ return identity;
70
+ };
71
+ },
72
+ };
73
+
74
+ const statsPath = require.resolve('../modules/platform-identity/project-stats.js');
75
+ require.cache[statsPath] = {
76
+ id: statsPath, filename: statsPath, loaded: true,
77
+ exports: { getProjectStats: async () => ({}) },
78
+ };
79
+
80
+ const myProjectsRoutes = require('../modules/platform-identity/routes/my-projects.js');
81
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
82
+
83
+ const realFetch = globalThis.fetch;
84
+ globalThis.fetch = async (url) => {
85
+ fetches.push(String(url));
86
+ return { status: 201, json: async () => ({ ok: true }) };
87
+ };
88
+ after(() => { globalThis.fetch = realFetch; });
89
+
90
+ const app = express();
91
+ app.use(express.json());
92
+ app.use(attachFail);
93
+ app.use('/api/bongos', myProjectsRoutes());
94
+
95
+ let server, port;
96
+ before(async () => {
97
+ server = http.createServer(app);
98
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
99
+ port = server.address().port;
100
+ });
101
+ after(async () => { await new Promise((r) => server.close(r)); });
102
+
103
+ function join(origin = 'https://Target.example') {
104
+ return new Promise((resolve, reject) => {
105
+ const payload = Buffer.from(JSON.stringify({ origin }));
106
+ const req = http.request({ host: '127.0.0.1', port, method: 'POST', path: '/api/bongos/my-projects/join',
107
+ headers: { 'Content-Type': 'application/json', 'Content-Length': payload.length } }, (res) => {
108
+ let data = '';
109
+ res.on('data', (c) => { data += c; });
110
+ res.on('end', () => resolve({ status: res.statusCode, body: data ? JSON.parse(data) : null }));
111
+ });
112
+ req.on('error', reject);
113
+ req.end(payload);
114
+ });
115
+ }
116
+
117
+ function reset(mode, answer) {
118
+ portMode = mode; identity = answer; portCalls = []; fetches = []; echoWrites = 0;
119
+ }
120
+
121
+ // ── Part A — the provider ────────────────────────────────────────────────────
122
+
123
+ function recorder(rows = []) {
124
+ const calls = [];
125
+ return { calls, query: async (sql, params) => { calls.push({ sql: String(sql).replace(/\s+/g, ' ').trim(), params }); return { rows }; } };
126
+ }
127
+
128
+ test('projectPublishIdentity asks the provisioning row for BOTH halves of the projection', async () => {
129
+ const db = recorder([{ slug: 'spike', description: 'A harbour of builders' }]);
130
+ const got = await projectPublishIdentity(db, { slug: 'spike', host: 'Spike.Example.com' });
131
+ assert.deepEqual(got, { name: 'spike', description: 'A harbour of builders' },
132
+ 'the slug IS the name on this plane — the mapping stays here, the rule stays in the hub');
133
+ const [{ sql, params }] = db.calls;
134
+ assert.match(sql, /FROM provisioning_instances WHERE slug = \$1 AND lower\(domain\) = \$2/);
135
+ assert.deepEqual(params, ['spike', 'spike.example.com']);
136
+ });
137
+
138
+ test('projectPublishIdentity answers null — no verdict — when no row carries both halves', async () => {
139
+ assert.equal(await projectPublishIdentity(recorder([]), { slug: 'spike', host: 'h.test' }), null);
140
+ });
141
+
142
+ test('projectPublishIdentity never queries for a target that names no project', async () => {
143
+ for (const target of [{}, { slug: '', host: 'h.test' }, { slug: ['spike'], host: 'h.test' }, undefined]) {
144
+ const db = recorder([{ slug: 'spike', description: 'x' }]);
145
+ assert.equal(await projectPublishIdentity(db, target), null, JSON.stringify(target));
146
+ assert.equal(db.calls.length, 0);
147
+ }
148
+ });
149
+
150
+ test('the port is registered once, declared by provisioning, and not required by the hub', () => {
151
+ registerProvisioningSeams();
152
+ registerProvisioningSeams();
153
+ assert.equal(typeof realApi.resolveOptional(PORT_NAME), 'function');
154
+ const read = (rel) => JSON.parse(readFileSync(new URL(rel, import.meta.url), 'utf8'));
155
+ assert.ok(read('../modules/provisioning/module.json').provides.includes(PORT_NAME), 'declared');
156
+ assert.ok(!(read('../modules/platform-identity/module.json').consumes || []).includes(PORT_NAME),
157
+ 'optional — a hub with no provisioning module must still boot and relay');
158
+ });
159
+
160
+ // ── Part B — the relay ───────────────────────────────────────────────────────
161
+
162
+ test('an unpublished project is refused at the hub: not_published, no relay, no echo', async () => {
163
+ reset('present', { name: 'proj-open', description: null });
164
+ const r = await join();
165
+ assert.equal(r.status, 200);
166
+ assert.equal(r.body.status, 'not_published');
167
+ assert.match(r.body.message, /isn’t published yet/);
168
+ assert.deepEqual(r.body.project, { name: 'Target', origin: 'https://Target.example' });
169
+ assert.deepEqual(fetches, [], 'the request never reaches a project with no door');
170
+ assert.equal(echoWrites, 0, 'a refusal is not an application');
171
+ assert.deepEqual(portCalls, [{ slug: 'proj-open', host: 'target.example' }],
172
+ 'asked about the client row the relay would have used: client_id + its origin host');
173
+ });
174
+
175
+ test('a blank description is dark matter too — the gate trims, so the relay does', async () => {
176
+ reset('present', { name: 'proj-open', description: ' ' });
177
+ const r = await join();
178
+ assert.equal(r.body.status, 'not_published');
179
+ assert.deepEqual(fetches, []);
180
+ });
181
+
182
+ test('a published project is relayed exactly as before', async () => {
183
+ reset('present', { name: 'proj-open', description: 'A harbour of builders' });
184
+ const r = await join();
185
+ assert.equal(r.body.status, 'requested');
186
+ assert.equal(fetches.length, 1);
187
+ assert.equal(echoWrites, 1);
188
+ });
189
+
190
+ for (const [label, mode, answer] of [
191
+ ['no provisioning module (no port)', 'absent', null],
192
+ ['no provisioning row (self-hosted or hand-seeded)', 'present', null],
193
+ ['a verdict read that fails', 'throws', null],
194
+ ]) {
195
+ test(`${label} is NO verdict: the request is relayed and the project decides`, async () => {
196
+ reset(mode, answer);
197
+ const r = await join();
198
+ assert.equal(r.body.status, 'requested');
199
+ assert.equal(fetches.length, 1, 'never refused on a guess');
200
+ });
201
+ }
202
+
203
+ test('an unknown project is still a 404 and the port is never asked', async () => {
204
+ reset('present', { name: 'x', description: null });
205
+ const r = await join('https://nowhere.example');
206
+ assert.equal(r.status, 404);
207
+ assert.deepEqual(portCalls, []);
208
+ });
@@ -1950,7 +1950,8 @@ test('the feed carries the owner\'s choice through a seam: platform-identity res
1950
1950
  assert.ok(manifest.provides.includes('provisioning.projectPlanets'), 'the port is declared');
1951
1951
  const pi = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'platform-identity', 'module.json'), 'utf8'));
1952
1952
  assert.ok(!(pi.consumes || []).includes('provisioning.projectPlanets'), 'not a required capability — the hub must boot without provisioning');
1953
- const prov = fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'routes', 'provisioning.js'), 'utf8');
1953
+ // the registrations moved to seams.js at the routes file's size ratchet (task 1003578)
1954
+ const prov = fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'seams.js'), 'utf8');
1954
1955
  assert.match(prov, /api\.registerProvider\('provisioning\.projectPlanets', \(hosts\) => provisioning\.planetChoicesForHosts\(pool, hosts\)\);/, 'registered beside instanceState');
1955
1956
  });
1956
1957