@bongos/core 1.19.910 → 1.19.911

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,337 @@
1
+ // tests/idea_landing_preview.mjs
2
+ //
3
+ // task 1004083 (BV2.R02, goal 1000095) — GRAVITY: the landing verdict read
4
+ // before the write. This drives the REAL `previewLanding` over a fake pool and a
5
+ // fake lifecycle port, so `decideLanding`, `authorizeRoutedLanding` and every
6
+ // branch inside the preview actually run.
7
+ //
8
+ // WHY THIS FILE EXISTS, stated plainly because it was a grader finding: the
9
+ // first cut of this task tested only the hall composer, against a mock of
10
+ // `postInboxPreviewLanding`. That proves the LINE renders what the server says
11
+ // and proves nothing at all about what the server says — the verdict every one
12
+ // of those cases asserted on was written by the test itself. The two halves need
13
+ // two harnesses, and this is the server one.
14
+ //
15
+ // DB-FREE, the tests/capture_goal_routing.mjs way: the collaborators the preview
16
+ // reaches through (`pool`, the `lifecycle` port, `moduleScopeMap`) are all
17
+ // injectable or patchable, and none of them is the thing under test — the matrix
18
+ // is, and the matrix is pure.
19
+ //
20
+ // THE PROPERTY THAT MATTERS MOST IS THE NEGATIVE ONE: previewing must never
21
+ // WRITE. A preview runs on every keystroke of a debounced composer, and the
22
+ // homeless path in the real capture resolves through `ensureMaintenanceGoal`,
23
+ // which is find-or-CREATE. The last case in this file fails if the preview ever
24
+ // reaches for it.
25
+ //
26
+ // Run: node tests/idea_landing_preview.mjs
27
+
28
+ import { strict as assert } from 'node:assert';
29
+ import { createRequire } from 'node:module';
30
+ import { readFileSync } from 'node:fs';
31
+ import { makeRunner } from './helpers.mjs';
32
+
33
+ const require = createRequire(import.meta.url);
34
+ const { test, summary } = makeRunner();
35
+
36
+ process.env.NODE_ENV = 'test';
37
+
38
+ const api = require('../src/module-api');
39
+ const { previewLanding, OUTCOMES } = require('../modules/ideas/landing-preview');
40
+
41
+ // The goal row the preview reads, in the shape its SELECT returns. Defaults are
42
+ // the happy one: a real, open goal on a building version, unprotected scope.
43
+ function goalRow(over = {}) {
44
+ return {
45
+ id: '77', title: 'Idea routing', status: 'open',
46
+ version_id: 'BV2', scope_modules: ['ideas'], version_status: 'building',
47
+ ...over,
48
+ };
49
+ }
50
+
51
+ // A pool that answers one query and COUNTS them, so a case can assert that a
52
+ // branch never reached the database at all.
53
+ function fakePool(rows = []) {
54
+ const calls = [];
55
+ return { calls, query: async (sql, params) => { calls.push({ sql, params }); return { rows }; } };
56
+ }
57
+
58
+ // Patch the two singletons the preview reaches through the doorway, and restore
59
+ // afterwards so cases cannot leak into each other.
60
+ // `version` defaults to a real building version, because "there IS one" is the
61
+ // ordinary world and a case that means the opposite must say so. The first cut
62
+ // of this helper defaulted the lifecycle port to NULL, which made two cases pass
63
+ // for the wrong reason — including the one whose whole subject is the missing
64
+ // version, so it asserted the right answer via the wrong cause.
65
+ async function withApi({ version = 'BV2', protectedScope = false } = {}, fn) {
66
+ const realResolve = api.resolveOptional;
67
+ const realMap = Object.getOwnPropertyDescriptor(api, 'moduleScopeMap');
68
+ const reached = { ensureMaintenanceGoal: 0, currentBuildingVersionId: 0 };
69
+ const lifecycle = { currentBuildingVersionId: async () => { reached.currentBuildingVersionId++; return version; } };
70
+ api.resolveOptional = (name) => (name === 'lifecycle' ? lifecycle : realResolve(name));
71
+ Object.defineProperty(api, 'moduleScopeMap', {
72
+ configurable: true,
73
+ value: { scopeIncludesProtected: () => protectedScope },
74
+ });
75
+ try { return await fn(reached); } finally {
76
+ api.resolveOptional = realResolve;
77
+ if (realMap) Object.defineProperty(api, 'moduleScopeMap', realMap);
78
+ }
79
+ }
80
+
81
+ // A lifecycle port that records which methods were touched. `ensureMaintenanceGoal`
82
+ // THROWS rather than answering: the preview must never call it, and a method that
83
+ // quietly succeeds would let that regression pass as a green test.
84
+ function fakeLifecycle(versionId, reached) {
85
+ return {
86
+ currentBuildingVersionId: async () => { reached.currentBuildingVersionId++; return versionId; },
87
+ ensureMaintenanceGoal: async () => {
88
+ reached.ensureMaintenanceGoal++;
89
+ throw new Error('previewLanding must never create a goal');
90
+ },
91
+ };
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // the two lies the composer's picker tells — the reason this task exists
96
+ // ---------------------------------------------------------------------------
97
+
98
+ await test('a homeless BUG routes, and the preview names whose maintenance goal catches it', async () => {
99
+ await withApi({}, async (reached) => {
100
+ const v = await previewLanding(
101
+ { goalId: null, kind: 'bug', grade: null, filerRank: 'archon', vector: 'api' },
102
+ { pool: fakePool() }
103
+ );
104
+ assert.equal(v.outcome, OUTCOMES.ROUTED_HOME, 'the picker says "homeless"; ADR 0262 says otherwise');
105
+ assert.equal(v.status, 'ready', 'a trusted filer\'s bug is claimable at once');
106
+ assert.equal(v.version_id, 'BV2');
107
+ assert.equal(v.goal, null, 'the goal row does not exist yet, so it is not named');
108
+ });
109
+ });
110
+
111
+ await test('the same bug from an UNTRUSTED filer still lands, but at backlog', async () => {
112
+ await withApi({}, async () => {
113
+ const v = await previewLanding(
114
+ { goalId: null, kind: 'bug', filerRank: 'thetes', vector: 'api' },
115
+ { pool: fakePool() }
116
+ );
117
+ assert.equal(v.outcome, OUTCOMES.ROUTED_HOME);
118
+ assert.equal(v.status, 'backlog', 'the rank split is the matrix\'s, not re-derived here');
119
+ });
120
+ });
121
+
122
+ await test('a homeless NON-bug floats — the homeless door is bug-only', async () => {
123
+ await withApi({}, async () => {
124
+ const v = await previewLanding({ goalId: null, kind: 'feature', filerRank: 'archon', vector: 'api' }, { pool: fakePool() });
125
+ assert.equal(v.outcome, OUTCOMES.FLOATS);
126
+ assert.equal(v.reason, 'no_goal');
127
+ });
128
+ });
129
+
130
+ await test('a homeless bug from an UNRECOGNISED vector floats — the allowlist holds', async () => {
131
+ await withApi({}, async () => {
132
+ const v = await previewLanding({ goalId: null, kind: 'bug', filerRank: 'archon', vector: 'discord-ideas' }, { pool: fakePool() });
133
+ assert.equal(v.outcome, OUTCOMES.FLOATS, '#ideas files with hints only (ADR 0234)');
134
+ });
135
+ });
136
+
137
+ await test('a NAMED goal on a judgment kind floats, and the goal is not claimed to catch it', async () => {
138
+ await withApi({}, async () => {
139
+ const v = await previewLanding(
140
+ { goalId: 77, kind: 'decision', filerRank: 'archon', vector: 'api' },
141
+ { pool: fakePool([goalRow()]) }
142
+ );
143
+ assert.equal(v.outcome, OUTCOMES.FLOATS);
144
+ assert.equal(v.reason, 'judgment_kind', 'the second lie the picker tells');
145
+ assert.equal(v.goal, null);
146
+ });
147
+ });
148
+
149
+ await test('a Full Idea never routes, however confidently a goal is picked', async () => {
150
+ await withApi({}, async () => {
151
+ const v = await previewLanding(
152
+ { goalId: 77, kind: 'feature', grade: 'full', filerRank: 'archon', vector: 'api' },
153
+ { pool: fakePool([goalRow()]) }
154
+ );
155
+ assert.equal(v.outcome, OUTCOMES.FLOATS);
156
+ assert.equal(v.reason, 'full_idea_never_routes');
157
+ });
158
+ });
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // a named goal: the happy paths and the wall
162
+ // ---------------------------------------------------------------------------
163
+
164
+ await test('a named goal is named back, with the status the matrix chose', async () => {
165
+ await withApi({}, async () => {
166
+ const pool = fakePool([goalRow()]);
167
+ const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool });
168
+ assert.equal(v.outcome, OUTCOMES.ROUTED);
169
+ assert.equal(v.status, 'ready');
170
+ assert.deepEqual(v.goal, { id: '77', title: 'Idea routing' });
171
+ assert.equal(pool.calls.length, 1, 'one read, and it is a SELECT');
172
+ assert.match(pool.calls[0].sql, /^\s*SELECT/i);
173
+ });
174
+ });
175
+
176
+ await test('a non-bug with a home soft-lands at backlog', async () => {
177
+ await withApi({}, async () => {
178
+ const v = await previewLanding({ goalId: 77, kind: 'refactor', filerRank: 'archon', vector: 'api' }, { pool: fakePool([goalRow()]) });
179
+ assert.equal(v.outcome, OUTCOMES.ROUTED);
180
+ assert.equal(v.status, 'backlog');
181
+ });
182
+ });
183
+
184
+ await test('PROTECTED scope + a sub-Metic filer is refused, in words, before the filing is spent', async () => {
185
+ await withApi({ protectedScope: true }, async () => {
186
+ const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'thetes', vector: 'api' }, { pool: fakePool([goalRow()]) });
187
+ assert.equal(v.outcome, OUTCOMES.REFUSED);
188
+ assert.equal(v.refusal.code, 'protected_scope');
189
+ assert.match(v.refusal.reason, /protected territory/);
190
+ });
191
+ });
192
+
193
+ await test('PROTECTED scope + a Metic+ filer is NOT refused — the two are layers, not alternatives', async () => {
194
+ await withApi({ protectedScope: true }, async () => {
195
+ const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: fakePool([goalRow()]) });
196
+ assert.equal(v.outcome, OUTCOMES.ROUTED);
197
+ });
198
+ });
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // the degraded states — silence, never a guess
202
+ // ---------------------------------------------------------------------------
203
+
204
+ for (const [label, row] of [
205
+ ['a goal that is gone', undefined],
206
+ ['a goal that is archived', goalRow({ status: 'archived' })],
207
+ ['a goal on a version that is not building', goalRow({ version_status: 'shipped' })],
208
+ ]) {
209
+ await test(`${label} states NOTHING rather than guessing a sentence`, async () => {
210
+ await withApi({}, async () => {
211
+ const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: fakePool(row ? [row] : []) });
212
+ assert.equal(v.outcome, null, 'the real capture will name the refusal; the preview does not invent one');
213
+ });
214
+ });
215
+ }
216
+
217
+ await test('no building version means no promise — the homeless path says nothing', async () => {
218
+ await withApi({ version: null }, async (reached) => {
219
+ const v = await previewLanding({ goalId: null, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: fakePool() });
220
+ assert.equal(v.outcome, null, 'the real capture fails closed here (NO_BUILDING_VERSION)');
221
+ assert.equal(reached.currentBuildingVersionId, 1, 'and it got there by asking, not by the port being absent');
222
+ });
223
+ });
224
+
225
+ await test('a pool that throws degrades to silence — a composer must never red on a keystroke', async () => {
226
+ await withApi({}, async () => {
227
+ const angry = { query: async () => { throw new Error('connection reset'); } };
228
+ const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: angry });
229
+ assert.equal(v.outcome, null);
230
+ assert.equal(v.refusal, null);
231
+ });
232
+ });
233
+
234
+ await test('a null input does not throw — the destructuring-defaults trap decideLanding records', async () => {
235
+ await withApi({}, async () => {
236
+ const v = await previewLanding(null, { pool: fakePool() });
237
+ assert.equal(v.outcome, OUTCOMES.FLOATS, 'nothing named, so nothing catches it');
238
+ });
239
+ });
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // THE NEGATIVE PROPERTY — the one that makes this safe to run per keystroke
243
+ // ---------------------------------------------------------------------------
244
+
245
+ await test('previewing a homeless bug NEVER creates the maintenance goal', async () => {
246
+ const reached = { ensureMaintenanceGoal: 0, currentBuildingVersionId: 0 };
247
+ const realResolve = api.resolveOptional;
248
+ api.resolveOptional = (n) => (n === 'lifecycle' ? fakeLifecycle('BV2', reached) : realResolve(n));
249
+ try {
250
+ const v = await previewLanding({ goalId: null, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: fakePool() });
251
+ assert.equal(v.outcome, OUTCOMES.ROUTED_HOME, 'it still answers');
252
+ assert.equal(reached.currentBuildingVersionId, 1, 'by READING the version');
253
+ assert.equal(reached.ensureMaintenanceGoal, 0,
254
+ 'ensureMaintenanceGoal is find-or-CREATE; a preview that called it would mint a goal per keystroke');
255
+ } finally { api.resolveOptional = realResolve; }
256
+ });
257
+
258
+ await test('no branch writes — every pool call in this file was a SELECT', async () => {
259
+ await withApi({}, async () => {
260
+ const pool = fakePool([goalRow()]);
261
+ for (const kind of ['bug', 'feature', 'decision', 'criterion-proposal', 'refactor']) {
262
+ await previewLanding({ goalId: 77, kind, filerRank: 'archon', vector: 'api' }, { pool });
263
+ }
264
+ assert.ok(pool.calls.length > 0, 'the goal read did happen');
265
+ for (const c of pool.calls) {
266
+ assert.match(c.sql, /^\s*SELECT/i, `a preview issued a non-SELECT: ${c.sql.slice(0, 40)}`);
267
+ assert.doesNotMatch(c.sql, /FOR SHARE|FOR UPDATE/i, 'a preview holds no lock — it races nothing');
268
+ }
269
+ });
270
+ });
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // THE ROUTE — asserted in SOURCE, the tests/filing_abuse_matrix.mjs idiom
274
+ //
275
+ // Those properties are guarantees about the CALL SITE, not about a return value:
276
+ // "the rank is the authenticated one" cannot be observed from the outside,
277
+ // because a route that read it from the body would answer identically to one
278
+ // that did not — for a caller who sent the true rank. So they are asserted where
279
+ // they live, exactly as F3 and F18 of the filing abuse matrix already do for
280
+ // POST /inbox. This file is the ideas module's second reader of that source.
281
+ // ---------------------------------------------------------------------------
282
+
283
+ const routeSrc = readFileSync(new URL('../modules/ideas/routes/inbox.js', import.meta.url), 'utf8');
284
+ const previewBlock = routeSrc.slice(routeSrc.indexOf("router.post('/inbox/preview-landing'"));
285
+
286
+ await test('[route] the preview exists, is builder-gated, and declares its validator', () => {
287
+ assert.ok(previewBlock.length > 0, 'POST /inbox/preview-landing is declared');
288
+ assert.match(previewBlock.slice(0, 400), /api\.requireBuilder/, 'no anonymous caller');
289
+ assert.match(previewBlock.slice(0, 900), /validateOrRespond\(req, res, \{/, 'the input SHAPE is validated');
290
+ });
291
+
292
+ await test('[route] rank and vector come from AUTH and a literal — never from the body', () => {
293
+ const head = previewBlock.slice(0, 1600);
294
+ assert.match(head, /filerRank:\s*req\.builder\?\.rank/,
295
+ 'a filer must not be able to preview a landing their rank does not grant');
296
+ assert.match(head, /vector:\s*'api'/, 'the vector is the door, hardcoded');
297
+ assert.ok(!/filerRank:\s*body\.|vector:\s*body\.|vector:\s*req\.body/.test(head),
298
+ 'neither may be read from the request body');
299
+ });
300
+
301
+ await test('[route] the verdict is passed through, not re-shaped into a second vocabulary', () => {
302
+ assert.match(previewBlock.slice(0, 1600), /const verdict = await previewLanding\(/);
303
+ assert.match(previewBlock.slice(0, 1600), /res\.json\(verdict\)/,
304
+ 'a route that rebuilt the object by hand would be a second place the shape can drift');
305
+ });
306
+
307
+ await test('[route] it is declared BEFORE the :id routes that would otherwise shadow it', () => {
308
+ // The file header calls this ordering load-bearing. A literal path declared
309
+ // after `/inbox/:id` binds `:id = 'preview-landing'`, parseId 400s, and the
310
+ // whole feature disappears with no error naming it.
311
+ const mine = routeSrc.indexOf("'/inbox/preview-landing'");
312
+ const byId = routeSrc.indexOf("'/inbox/:id(\\\\d+)'");
313
+ assert.ok(mine > 0, 'the route is there');
314
+ assert.ok(byId === -1 || mine < byId, 'and it is declared above the id-bearing routes');
315
+ });
316
+
317
+ await test('[route] the preview is not wired into the CAPTURE path — it is a read beside it', () => {
318
+ // The whole claim of "no new routing" is that the matrix has ONE caller that
319
+ // can write. previewLanding must never appear inside POST /inbox.
320
+ const capture = routeSrc.slice(routeSrc.indexOf("router.post('/inbox',"), routeSrc.indexOf("router.get('/inbox',"));
321
+ assert.ok(!/previewLanding/.test(capture), 'the capture does not consult the preview');
322
+ });
323
+
324
+ // Probes run, and these are the MEASURED counts:
325
+ // make the homeless branch call ensureMaintenanceGoal -> 4 red (incl. the negative property)
326
+ // let authorizeRoutedLanding's refusal fall through -> 1 red (protected-scope)
327
+ // drop the grade check from decideLanding's early exit -> 1 red (full-idea)
328
+ // fabricate the goal instead of reading it -> 1 red (goal-is-gone)
329
+ //
330
+ // Writing this file is what found two defects the composer suite could not:
331
+ // `previewLanding(null)` THREW (a default in a parameter list fires on undefined
332
+ // and not on null — the trap routing.js's own comment records, reproduced one
333
+ // file away from it), and this harness's first `withApi` defaulted the lifecycle
334
+ // port to null, which made two cases pass for the wrong reason — including the
335
+ // one whose entire subject is the missing version.
336
+
337
+ summary('idea_landing_preview');
@@ -60,6 +60,11 @@ const GOALS = [
60
60
  function boot({
61
61
  search = '', sparks = QUEUE, count = null, idea = SPARK, ideaOk = true, store = {},
62
62
  versions = VERSIONS, goals = GOALS, suggestions = [],
63
+ // GRAVITY (task 1004083 / spec R02). The landing verdict the composer draws
64
+ // its one line from. `null` means the read FAILED — distinct from a verdict of
65
+ // `{ outcome: null }`, which is the server saying "nothing to state yet"; the
66
+ // line must go quiet for both, and there is a case below for each.
67
+ landing = undefined,
63
68
  } = {}) {
64
69
  const els = new Map();
65
70
  const makeEl = () => ({
@@ -108,6 +113,21 @@ function boot({
108
113
  return { status: 201, ok: true, data: { idea: { id: '900', grade: 'full', completeness_score: 80 } } };
109
114
  },
110
115
  postInboxIdDevelop: async (a) => { calls.push(['develop', a]); return { status: 200, ok: true, data: { idea: { ...idea, grade: 'full', completeness_score: 80, ideator_credit: 5 } } }; },
116
+ // task 1004083 (spec R02) — the landing preview. The DEFAULT mirrors the
117
+ // real matrix for the fixtures this file already files with: a goal named
118
+ // ⇒ it lands there, no goal ⇒ it floats. A test that cares about a
119
+ // particular verdict passes `landing` and overrides the lot.
120
+ postInboxPreviewLanding: async (a) => {
121
+ calls.push(['preview', a]);
122
+ if (landing === null) return { status: 500, ok: false, data: null };
123
+ if (landing !== undefined) return { status: 200, ok: true, data: landing };
124
+ const gid = a.body && a.body.goal_id;
125
+ if (gid != null) {
126
+ const g = goals.find((x) => String(x.id) === String(gid));
127
+ return { status: 200, ok: true, data: { outcome: 'routed', reason: 'bug_trusted_filer', status: 'ready', goal: { id: String(gid), title: g ? g.title : `goal ${gid}` }, version_id: 'BV1', refusal: null } };
128
+ }
129
+ return { status: 200, ok: true, data: { outcome: 'floats', reason: 'no_goal', status: null, goal: null, version_id: null, refusal: null } };
130
+ },
111
131
  },
112
132
  // task 1003069 (R08) — the quick-filing goal picker's reads: the building
113
133
  // versions (to scope the suggester) and the open goal list (the picker's
@@ -515,5 +535,168 @@ await test('storage that refuses never takes the composer down, and says it is n
515
535
  // the element. Without the markup pin this file would have certified a page
516
536
  // that renders nothing.
517
537
 
538
+ // ---------------------------------------------------------------------------
539
+ // GRAVITY — which goal will catch this thought, said before it is released
540
+ // (task 1004083 / spec R02)
541
+ //
542
+ // THE DEFECT THESE PIN is not a missing control: #fq-goal is a visible select.
543
+ // It is that the select's own labels are UNTRUE in two directions — "No goal
544
+ // (homeless)" for a bug (ADR 0262 routes it to the maintenance goal) and a named
545
+ // goal for a judgment kind (routing.js NEVER_ROUTE_KINDS). So the cases that
546
+ // matter most here are the two where the LINE AND THE PICKER DISAGREE.
547
+ //
548
+ // The same stub caveat the receipts block above records applies in full: the DOM
549
+ // fake manufactures an element for any id, so the markup pin is load-bearing and
550
+ // every other case below would stay green without it.
551
+ // ---------------------------------------------------------------------------
552
+
553
+ await test('ideas.html actually declares the gravity line (the stub would fake it otherwise)', async () => {
554
+ const html = fs.readFileSync(path.join(HALL, 'ideas.html'), 'utf8');
555
+ assert.match(html, /id="fq-gravity"/, 'the gravity line is in the real markup');
556
+ assert.match(html, /id="fq-gravity"[^>]*hidden/, 'and it starts hidden — nothing true to say yet');
557
+ assert.match(html, /id="fq-gravity"[^>]*aria-live="polite"/, 'and it is announced, not just painted');
558
+ });
559
+
560
+ await test('picking a goal says which goal will catch it, and that it is claimable', async () => {
561
+ const r = boot();
562
+ await settle(); await settle();
563
+ r.el('fq-title').value = 'the widget is broken';
564
+ r.el('fq-goal').value = '77';
565
+ r.el('fq-goal')._handlers.change();
566
+ await settle(); await settle();
567
+ assert.equal(r.el('fq-gravity').hidden, false, 'the line is shown');
568
+ assert.match(r.el('fq-gravity').textContent, /Goal Seventy Seven/, 'and it names the goal');
569
+ assert.match(r.el('fq-gravity').textContent, /anyone can claim/, 'and says the task is claimable');
570
+ assert.equal(r.el('fq-gravity').dataset.tone, 'lands');
571
+ });
572
+
573
+ await test('a goal that only lands at backlog says a nod is still needed', async () => {
574
+ const r = boot({ landing: { outcome: 'routed', reason: 'bug_awaiting_nod', status: 'backlog', goal: { id: '77', title: 'Goal Seventy Seven' }, version_id: 'BV1', refusal: null } });
575
+ await settle(); await settle();
576
+ r.el('fq-title').value = 'the widget is broken';
577
+ r.el('fq-title')._handlers.input();
578
+ await new Promise((res) => setTimeout(res, 320));
579
+ await settle();
580
+ assert.match(r.el('fq-gravity').textContent, /waiting for a nod/, 'the two routed outcomes are not conflated');
581
+ assert.equal(r.el('fq-gravity').dataset.tone, 'lands', 'it still lands — the tone is about having a home, not about being claimable');
582
+ });
583
+
584
+ await test('a homeless BUG is told it lands anyway — the picker says "homeless" and that is false', async () => {
585
+ // ADR 0262: decideLanding routes a goal-less kind='bug' from the api vector
586
+ // into the version's maintenance goal. The select still reads "No goal
587
+ // (homeless)". This line is the correction, and it is the whole reason the
588
+ // verdict is read from the server instead of inferred from the select's value.
589
+ const r = boot({ landing: { outcome: 'routed_home', reason: 'bug_homeless_trusted_filer', status: 'ready', goal: null, version_id: 'BV1', refusal: null } });
590
+ await settle(); await settle();
591
+ r.el('fq-title').value = 'the widget is broken';
592
+ r.el('fq-title')._handlers.input();
593
+ await new Promise((res) => setTimeout(res, 320));
594
+ await settle();
595
+ const said = r.el('fq-gravity').textContent;
596
+ assert.match(said, /A bug always lands/, 'it contradicts the picker rather than echoing it');
597
+ assert.match(said, /BV1/, 'and names whose maintenance goal catches it');
598
+ assert.equal(r.el('fq-gravity').dataset.tone, 'lands');
599
+ });
600
+
601
+ await test('a named goal that the matrix DECLINES says so instead of implying the goal caught it', async () => {
602
+ // The second lie: a criterion-proposal or a decision never routes, however
603
+ // confidently the select shows a goal beside it.
604
+ const r = boot({ landing: { outcome: 'floats', reason: 'judgment_kind', status: null, goal: { id: '77', title: 'Goal Seventy Seven' }, version_id: 'BV1', refusal: null } });
605
+ await settle(); await settle();
606
+ r.el('fq-title').value = 'should we split the hall';
607
+ r.el('fq-title')._handlers.input();
608
+ await new Promise((res) => setTimeout(res, 320));
609
+ await settle();
610
+ const said = r.el('fq-gravity').textContent;
611
+ assert.match(said, /will not catch this/, 'the goal is named as NOT catching it');
612
+ assert.match(said, /inbox/, 'and the real destination is stated');
613
+ assert.equal(r.el('fq-gravity').dataset.tone, 'floats');
614
+ });
615
+
616
+ await test('letting it float is stated as an outcome, not left as an empty default', async () => {
617
+ const r = boot();
618
+ await settle(); await settle();
619
+ r.el('fq-title').value = 'a loose thought';
620
+ r.el('fq-title')._handlers.input();
621
+ await new Promise((res) => setTimeout(res, 320));
622
+ await settle();
623
+ assert.equal(r.el('fq-gravity').hidden, false);
624
+ assert.match(r.el('fq-gravity').textContent, /floats in the inbox/);
625
+ assert.equal(r.el('fq-gravity').dataset.tone, 'floats', 'floating is neutral — it is a legal choice, not a warning');
626
+ });
627
+
628
+ await test('a refusal is shown in the filer\'s own words before they spend the filing', async () => {
629
+ const r = boot({ landing: { outcome: 'refused', reason: 'soft_landing', status: null, goal: { id: '77', title: 'Goal Seventy Seven' }, version_id: 'BV1', refusal: { code: 'protected_scope', reason: 'This goal reaches protected territory, so a filer below Metic cannot route work into it.' } } });
630
+ await settle(); await settle();
631
+ r.el('fq-title').value = 'touch the deploy core';
632
+ r.el('fq-title')._handlers.input();
633
+ await new Promise((res) => setTimeout(res, 320));
634
+ await settle();
635
+ assert.match(r.el('fq-gravity').textContent, /protected territory/, 'the server\'s reason is passed through');
636
+ assert.equal(r.el('fq-gravity').dataset.tone, 'refused');
637
+ });
638
+
639
+ await test('an empty title states nothing, and a failed read states nothing — never a guess', async () => {
640
+ const quiet = boot();
641
+ await settle(); await settle();
642
+ quiet.el('fq-title').value = ' ';
643
+ quiet.el('fq-title')._handlers.input();
644
+ await new Promise((res) => setTimeout(res, 320));
645
+ await settle();
646
+ assert.equal(quiet.el('fq-gravity').hidden, true, 'nothing to file, nothing to say');
647
+
648
+ const down = boot({ landing: null });
649
+ await settle(); await settle();
650
+ down.el('fq-title').value = 'the widget is broken';
651
+ down.el('fq-title')._handlers.input();
652
+ await new Promise((res) => setTimeout(res, 320));
653
+ await settle();
654
+ assert.equal(down.el('fq-gravity').hidden, true, 'a preview outage degrades to silence, not to a wrong sentence');
655
+ assert.equal(down.el('fq-err').textContent, '', 'and never to an error under a half-typed thought');
656
+ });
657
+
658
+ await test('filing clears the line — gravity describes a draft, and the draft is gone', async () => {
659
+ const r = boot();
660
+ await settle(); await settle();
661
+ r.el('fq-title').value = 'the widget is broken';
662
+ r.el('fq-goal').value = '77';
663
+ r.el('fq-goal')._handlers.change();
664
+ await settle(); await settle();
665
+ // The TEXT is what proves it was shown, not `hidden`. The DOM stub starts an
666
+ // element at hidden:false, so asserting only on that passes when the line was
667
+ // never rendered at all — the same class of vacuous pass the markup pin above
668
+ // exists for, caught by reverting the goal-change redraw and watching this
669
+ // case stay green.
670
+ assert.match(r.el('fq-gravity').textContent, /Goal Seventy Seven/, 'shown while composing');
671
+ assert.equal(r.el('fq-gravity').hidden, false);
672
+ await r.submitQuick();
673
+ await settle();
674
+ assert.equal(r.el('fq-gravity').hidden, true, 'and gone once it is filed');
675
+ assert.equal(r.el('fq-gravity').textContent, '', 'and the sentence with it');
676
+ assert.equal(r.el('fq-receipts').hidden, false, 'the RECEIPT is what survives instead (task 1004082)');
677
+ });
678
+
679
+ // Every guard above was reverted and each redded its named case (RUN, and these
680
+ // are the measured counts, not the predicted ones):
681
+ // drop renderGravity from the goal-change handler -> 2 red (named-goal + filing-clears)
682
+ // infer from the select instead of the verdict -> 4 red (both lies, + backlog + refusal)
683
+ // drop the seq guard and paint every answer -> 0 red <-- see below
684
+ // rename the id in ideas.html -> 1 red (the markup pin ONLY)
685
+ //
686
+ // THE FIRST LINE COST A TEST FIX, and it is the reason to run these rather than
687
+ // reason about them. That probe first redded only ONE case: "filing clears the
688
+ // line" opened by asserting `hidden === false`, and the DOM stub starts every
689
+ // element at `hidden: false` — so the assertion passed while the line was never
690
+ // rendered at all. It asserts on the TEXT now. A stub's defaults can agree with
691
+ // a broken page, which is the same lesson the markup pin below teaches twice.
692
+ //
693
+ // THE THIRD LINE IS AN HONEST GAP, recorded rather than papered over. The
694
+ // out-of-order guard in refreshGravity is not covered: the fake resolves
695
+ // synchronously, so two previews cannot interleave here and a test that removed
696
+ // the guard would still pass. Pinning it needs a fake that delays per call,
697
+ // which is a different harness from this one. Filed as task 1004108 rather than
698
+ // claimed as tested — a green test over an unreachable branch would be a false
699
+ // claim of coverage, which is the failure this file's own note above warns about.
700
+
518
701
  console.log(`\nidea_spark_hall: ${passed} passed, ${failed} failed`);
519
702
  if (failed > 0) process.exit(1);