@bongos/core 1.19.585 → 1.19.587

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.
@@ -186,7 +186,8 @@ test('no FIFTH vector has appeared', () => {
186
186
  // which case delete its opt-in and this line, do not extend the list.
187
187
  const roster = new Set([...OPT_INS.map(([f]) => f), 'modules/lifecycle/db-tasks.js']);
188
188
  const suspects = [
189
- 'modules/ideas/inbox.js', 'modules/lifecycle/routes/goals.js',
189
+ 'modules/ideas/inbox.js', 'modules/ideas/homeless-home.js',
190
+ 'modules/lifecycle/routes/goals.js',
190
191
  'modules/lifecycle/routes/task-write-routes.js',
191
192
  ];
192
193
  for (const f of suspects) {
@@ -0,0 +1,137 @@
1
+ // tests/wizard_preselect_why.mjs — the module picker always says WHY the preselect is on
2
+ // (task 1003684).
3
+ //
4
+ // The bug as filed said the starter-bundle "why" sentence was being dropped for
5
+ // Game + small-team. It was not: `adjustments` is a DELTA, and a rule that fires without
6
+ // changing anything claims nothing (ADR 0243). The game bundle is already
7
+ // ['dev-box','discord'] and the small-team rule ADDS dev-box, so the rule fires, moves
8
+ // nothing, and correctly reports []. Verified against live core 1.19.580:
9
+ // byTeamShape['small-team'] => {"bundle":["dev-box","discord"],"adjustments":[]}
10
+ //
11
+ // The real defect is what the owner then saw: two extras pre-switched-on with no reason
12
+ // beside them — precisely the state the paint site's own comment calls out, "a preselect an
13
+ // owner cannot see the reason for is one they have to audit, which is the thing ADR 0237
14
+ // says is worse than no preselect at all". Each bundle carries a curated `summary` authored
15
+ // for this, served on every row of GET /provisioning/starter-bundles, and never rendered
16
+ // anywhere in the wizard. It is now the floor beneath the adjustment sentences.
17
+ //
18
+ // Harness: the vm-slice idiom (tests/project_door_ui.mjs), since the wizard's script is
19
+ // inline in the page and there is no jsdom here.
20
+ //
21
+ // Run: node --test --test-reporter=tap tests/wizard_preselect_why.mjs
22
+
23
+ import assert from 'node:assert/strict';
24
+ import { test } from 'node:test';
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import vm from 'node:vm';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8');
32
+
33
+ const SLICE_RE = /function bundleRow\(\)[\s\S]*?function preselectReasons\(\)[\s\S]*?\n {4}\}/;
34
+ const slice = (HUB.match(SLICE_RE) || [null])[0];
35
+
36
+ test('the preselect-reason slice is still findable', () => {
37
+ assert.ok(slice, 'bundleRow/tailoredRow/preselectReasons were not found in projects.html');
38
+ assert.match(slice, /function tailoredRow\(\)/, 'the slice spans the tailoring lookup too');
39
+ });
40
+
41
+ // Faithful to the real payload shape (confirmed against live): every bundle row carries
42
+ // label/summary/bundle, and byTeamShape holds the resolved answer per team shape.
43
+ const BUNDLES = {
44
+ bundles: [
45
+ {
46
+ type: 'game',
47
+ label: 'Game starter',
48
+ summary: 'Somewhere for players to gather, and a place to walk a change through before anyone plays it.',
49
+ bundle: ['dev-box', 'discord'],
50
+ byTeamShape: {
51
+ // the live case: the rule fires, moves nothing, reports nothing
52
+ 'small-team': { bundle: ['dev-box', 'discord'], adjustments: [] },
53
+ solo: {
54
+ bundle: ['discord'],
55
+ adjustments: [{ reason: 'You are building this alone, so the shared cloud environment is off — your own machine is the setup.' }],
56
+ },
57
+ },
58
+ },
59
+ {
60
+ type: 'research',
61
+ label: 'Research starter',
62
+ summary: 'Extra agents to run the reading and the experiments the question needs.',
63
+ bundle: ['agents'],
64
+ byTeamShape: {
65
+ 'small-team': {
66
+ bundle: ['agents', 'dev-box'],
67
+ adjustments: [{ reason: 'A small team gets one shared environment to build in, so nobody is debugging their own laptop.' }],
68
+ },
69
+ },
70
+ },
71
+ { type: 'not-sure', label: 'Not sure', summary: '', bundle: [], byTeamShape: {} },
72
+ ],
73
+ };
74
+
75
+ function why({ projType, teamShape, modules = null, bundles = BUNDLES }) {
76
+ const sandbox = { starterBundles: bundles, state: { projType, teamShape, modules } };
77
+ sandbox.globalThis = sandbox;
78
+ vm.createContext(sandbox);
79
+ vm.runInContext(slice + '\nvar __out = preselectReasons();', sandbox);
80
+ // Copy out of the vm realm: its Array has a different prototype, and assert/strict's
81
+ // deepEqual compares prototypes — so a correct result would otherwise fail as
82
+ // "same structure but not reference-equal".
83
+ return Array.from(sandbox.__out);
84
+ }
85
+
86
+ test('game + small-team: no delta, so the type\'s own summary is the reason', () => {
87
+ // This is the exact combination from the live walk that surfaced the bug.
88
+ const out = why({ projType: 'game', teamShape: 'small-team' });
89
+ assert.deepEqual(out, ['Somewhere for players to gather, and a place to walk a change through before anyone plays it.']);
90
+ });
91
+
92
+ test('a REAL adjustment still speaks for itself — the summary does not displace it', () => {
93
+ const out = why({ projType: 'research', teamShape: 'small-team' });
94
+ assert.equal(out.length, 1);
95
+ assert.match(out[0], /shared environment/);
96
+ assert.doesNotMatch(out[0], /reading and the experiments/, 'the rule\'s sentence wins, not the summary');
97
+ });
98
+
99
+ test('a removal adjustment is reported the same way', () => {
100
+ const out = why({ projType: 'game', teamShape: 'solo' });
101
+ assert.equal(out.length, 1);
102
+ assert.match(out[0], /building this alone/);
103
+ });
104
+
105
+ test('no team shape answered: falls through to the bundle, still explained', () => {
106
+ // tailoredRow() returns the unadjusted bundle with adjustments: [] when no shape is set,
107
+ // which before this change also produced an unexplained preselect.
108
+ const out = why({ projType: 'game', teamShape: null });
109
+ assert.deepEqual(out, ['Somewhere for players to gather, and a place to walk a change through before anyone plays it.']);
110
+ });
111
+
112
+ test('once the owner has answered for themselves, the panel says nothing', () => {
113
+ // The pre-existing rule, and the one a careless fallback would have broken: handing back
114
+ // a reason for a toggle they just flipped reads as the panel arguing with them.
115
+ assert.deepEqual(why({ projType: 'game', teamShape: 'small-team', modules: [] }), []);
116
+ assert.deepEqual(why({ projType: 'game', teamShape: 'small-team', modules: ['agents'] }), []);
117
+ });
118
+
119
+ test('an unknown type, or a bundle with no summary, says nothing rather than crashing', () => {
120
+ assert.deepEqual(why({ projType: 'no-such-type', teamShape: 'small-team' }), []);
121
+ assert.deepEqual(why({ projType: null, teamShape: 'small-team' }), []);
122
+ assert.deepEqual(why({ projType: 'not-sure', teamShape: 'small-team' }), [], 'empty summary is not a sentence');
123
+ });
124
+
125
+ test('before the bundles read lands, nothing is claimed', () => {
126
+ assert.deepEqual(why({ projType: 'game', teamShape: 'small-team', bundles: null }), []);
127
+ });
128
+
129
+ test('every sentence returned is a plain string, so the paint site can escape it directly', () => {
130
+ for (const t of ['game', 'research']) {
131
+ for (const s of ['small-team', 'solo', null]) {
132
+ for (const line of why({ projType: t, teamShape: s })) {
133
+ assert.equal(typeof line, 'string', `${t}/${s} yielded a non-string`);
134
+ }
135
+ }
136
+ }
137
+ });