@bongos/core 1.21.3 → 1.21.5

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.
Files changed (73) hide show
  1. package/.bongos-core.json +128 -68
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +2 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +35 -1
  10. package/docs/api-reference.md +3 -2
  11. package/docs/copy-inventory.md +389 -518
  12. package/docs/copy-registry.json +880 -2143
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/page-inventory.json +17 -25
  15. package/docs/page-readings.json +1008 -1246
  16. package/migrations/core_272_rename_cn_criterion_slugs.sql +40 -0
  17. package/modules/autonomy/governor-docket.js +101 -0
  18. package/modules/autonomy/routes/autonomy.js +5 -0
  19. package/modules/discord/board-broadcast.js +3 -1
  20. package/modules/discord/craft-broadcast.js +107 -0
  21. package/modules/discord/routes/discord.js +10 -0
  22. package/modules/discord/ship-broadcast.js +30 -2
  23. package/modules/economy/reward.js +3 -0
  24. package/modules/government/board.js +20 -5
  25. package/modules/government/docket-vocab.js +82 -0
  26. package/modules/government/docket.js +458 -0
  27. package/modules/government/routes/government.js +34 -0
  28. package/modules/hall-ui/public/genesis-home.js +9 -10
  29. package/modules/ideas/governor-docket.js +93 -0
  30. package/modules/ideas/routes/blockers.js +5 -0
  31. package/modules/lifecycle/criterion-ref.js +11 -0
  32. package/modules/lifecycle/db-tasks.js +7 -0
  33. package/modules/lifecycle/governor-docket.js +192 -0
  34. package/modules/lifecycle/routes/done-when.js +6 -0
  35. package/modules/lifecycle/routes/gate-approvals.js +78 -33
  36. package/modules/lifecycle/routes/lifecycle.js +3 -0
  37. package/modules/onboarding/routes/access-requests.js +68 -27
  38. package/modules/public-landing/public/assets/cosmos.css +6 -50
  39. package/modules/public-landing/public/projects.html +1170 -1659
  40. package/modules/public-landing/public/projects.probes.json +18 -18
  41. package/modules/public-landing/public/projects.states.json +23 -31
  42. package/modules/security/routes/reports.js +39 -0
  43. package/package-lock.json +2 -2
  44. package/package.json +1 -1
  45. package/release-notes.json +20 -0
  46. package/src/bongos/module-rechecks.js +77 -0
  47. package/src/bongos/route-rank-check.js +3 -0
  48. package/src/bongos/routes/core-update.js +41 -0
  49. package/src/bongos/routes/modules.js +17 -0
  50. package/src/module-api.js +1 -1
  51. package/tests/criterion_goal_attach.mjs +12 -4
  52. package/tests/criterion_ref_parse.mjs +41 -0
  53. package/tests/discord_craft_broadcast.mjs +162 -0
  54. package/tests/genesis_home.mjs +1 -1
  55. package/tests/government_routes.mjs +3 -1
  56. package/tests/governor_docket.mjs +552 -0
  57. package/tests/onboard.mjs +10 -4
  58. package/tests/platform_boot.mjs +1 -1
  59. package/tests/projects_hub.mjs +196 -89
  60. package/tests/projects_hub_app_status.mjs +3 -1
  61. package/tests/projects_hub_app_step.mjs +72 -48
  62. package/tests/projects_hub_dns_ready.mjs +10 -2
  63. package/tests/projects_hub_look.mjs +32 -22
  64. package/tests/projects_hub_module_picker.mjs +112 -62
  65. package/tests/projects_hub_pre_uat.mjs +48 -41
  66. package/tests/public_landing_projects.mjs +12 -6
  67. package/tests/wizard_demo.mjs +37 -11
  68. package/tests/wizard_draft_resume.mjs +34 -22
  69. package/tests/wizard_front_door.mjs +173 -103
  70. package/tests/wizard_intent_resume.mjs +28 -25
  71. package/tests/wizard_physics.mjs +94 -76
  72. package/tests/wizard_physics_more.mjs +409 -277
  73. package/tests/wizard_six_screens.mjs +126 -0
@@ -15,12 +15,13 @@
15
15
  // ruled out the task's other option (drop the record at a terminal state).
16
16
  //
17
17
  // The reset fires only when there is a SECOND DRAFT to lose, and the draft's own step is
18
- // what says so. A landed create clears DRAFT_KEY — but show(12) then persists the done
18
+ // what says so. A landed create clears DRAFT_KEY — but show(6) then persists the done
19
19
  // panel's position straight back into it, so a plain "is there a draft?" test is always
20
20
  // yes and would skip the reset forever, leaving the finished project's name sitting in
21
21
  // the fresh form. (That is exactly what the first cut of this change shipped into a
22
22
  // render: "still watching Mercury" above a name field still reading "Mercury".) Only
23
- // steps 1-11 are wizard steps (1-10 before task 1004421's Look step, 1-8 before task 1004417's front door took 1 and 2);
23
+ // steps 1-5 are wizard steps since task 1004504's six blended screens (1-11 before them, 1-10
24
+ // before task 1004421's Look step, 1-8 before task 1004417's front door took 1 and 2);
24
25
  // anything else is the done panel's own bookkeeping.
25
26
  //
26
27
  // Harness: there is no jsdom in this repo, so this follows tests/wizard_draft_resume.mjs
@@ -58,8 +59,8 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
58
59
  assert.ok(offerSlice, 'paintResumeOffer() was not found in projects.html');
59
60
  assert.match(enterSlice, /if \(focus && !resumeIntent\) \{/, 'the intent branch is part of the slice');
60
61
  assert.match(enterSlice, /startProgress\(created\.id\);/, 'so is the resume branch it guards');
61
- assert.match(enterSlice, /draft\.step >= 1 && draft\.step <= 12/,
62
- 'the STORED draft\'s step decides, not state.step — state.step is 13 for the whole life of a finished standup');
62
+ assert.match(enterSlice, /draft\.step >= 1 && draft\.step <= 5 \?/,
63
+ 'the STORED draft\'s step decides, not state.step — state.step is 6 for the whole life of a finished standup');
63
64
  assert.match(SCRIPT, /if \(created && created\.id\) \{ enterCreated\(created, !!focus\); return; \}/,
64
65
  'enter() still hands the created record straight to it');
65
66
  });
@@ -73,7 +74,7 @@ function runEnter({ focus, resumeIntent = false, draft = null, record }) {
73
74
  const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0 };
74
75
  // state.step is the DONE step for the whole life of a finished standup — the value
75
76
  // a draft-blind reading would have trusted
76
- const state = { step: draft ? draft.step : 13, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
77
+ const state = { step: draft ? draft.step : 6, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
77
78
  const sandbox = {
78
79
  record, focus, resumeIntent, state,
79
80
  DRAFT_KEY: 'draft',
@@ -99,7 +100,7 @@ const REC = { id: 'inst-1', name: 'Acme', slug: 'acme', domain: 'acme.example',
99
100
 
100
101
  test('a reload / Back / identity settle (focus false) still resumes the done panel, watch and all', () => {
101
102
  const { calls, state } = runEnter({ focus: false, record: REC });
102
- assert.deepEqual(calls.show, [[13, false]], 'the done panel, and no focus steal on a non-deliberate arrival');
103
+ assert.deepEqual(calls.show, [[6, false]], 'the done panel, and no focus steal on a non-deliberate arrival');
103
104
  assert.equal(calls.resets, 0, 'nothing is reset — this is the same standup continuing');
104
105
  assert.equal(calls.done, 1, 'the checklist is re-rendered');
105
106
  assert.deepEqual(calls.progress, ['inst-1'], 'the progress watch is restarted from the record');
@@ -114,7 +115,7 @@ test('a deliberate "New project" click after a finished standup opens a FRESH st
114
115
  assert.equal(calls.progress.length, 0, 'and its watch is not restarted');
115
116
  assert.equal(calls.resets, 1, 'with no second draft to lose, the wizard forgets the finished project');
116
117
  assert.deepEqual(calls.show, [[1, false]], 'step 1, and step 1 does not take the heading focus');
117
- assert.equal(calls.focused, 1, 'its one control, the terms tick, does — the deliberate-nav rule enter() already applies');
118
+ assert.equal(calls.focused, 1, 'its first control, the fork\'s terms tick, does — the deliberate-nav rule enter() already applies');
118
119
  assert.equal(state.name, '', 'the old name is gone from the form');
119
120
  });
120
121
 
@@ -127,7 +128,7 @@ test('…and the standup stays one click away, named, on the resume link', () =>
127
128
 
128
129
  test('the resume link gets the done panel back — the record was never cleared', () => {
129
130
  const { calls, sandbox } = runEnter({ focus: true, resumeIntent: true, record: REC });
130
- assert.deepEqual(calls.show, [[13, true]], 'a deliberate resume moves focus to the done panel');
131
+ assert.deepEqual(calls.show, [[6, true]], 'a deliberate resume moves focus to the done panel');
131
132
  assert.deepEqual(calls.progress, ['inst-1'], 'the watch comes back with it');
132
133
  assert.equal(sandbox.resumeIntent, false, 'the flag is ONE-SHOT — the next plain nav click must mean "new" again');
133
134
  assert.deepEqual(calls.offer, [null], 'and the offer comes down behind it');
@@ -137,29 +138,30 @@ test('a second draft already under way is NOT wiped by navigating back to the wi
137
138
  // The regression this guards: with the reset unconditional, typing a name, stepping
138
139
  // away to "Your projects" and clicking "New project" again would blank the draft —
139
140
  // trading the reported bug for a worse one.
140
- const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 5 } });
141
+ const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 3 } });
141
142
  assert.equal(calls.resets, 0, 'a live draft is never reset');
142
- assert.deepEqual(calls.show, [[5, true]], 'the second draft resumes where it was left, heading focused');
143
- assert.equal(state.step, 5);
143
+ assert.deepEqual(calls.show, [[3, true]], 'the second draft resumes where it was left, heading focused');
144
+ assert.equal(state.step, 3);
144
145
  assert.deepEqual(calls.offer, ['Acme'], 'the offer stays up beside it');
145
146
  });
146
147
 
147
148
  test('the done panel\'s own saved step is NOT a second draft — it must not block the reset', () => {
148
- // Caught in a render, not in a unit test: show(12) calls saveDraft(), so DRAFT_KEY is
149
+ // Caught in a render, not in a unit test: show(6) calls saveDraft(), so DRAFT_KEY is
149
150
  // non-empty again the moment a create lands. Reading "is there a draft?" left the
150
151
  // finished project's name in the fresh form, under a line offering to resume it.
151
- const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 13, name: 'Mercury' } });
152
+ const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 6, name: 'Mercury' } });
152
153
  assert.equal(calls.resets, 1, 'the done step is bookkeeping, not a project being typed');
153
154
  assert.deepEqual(calls.show, [[1, false]], 'out-of-range steps land on step 1, the front door');
154
155
  assert.equal(state.name, '', 'and the finished project\'s name does not survive into it');
155
156
  assert.equal(calls.done, 0, 'never back onto the done panel');
156
157
  });
157
158
 
158
- test('a draft on the app step, the Look step or the review is live too — the last three numbered steps (task 1004351)', () => {
159
- // The app step moved the review from 7 to 8, the front door (task 1004417) to 10 and the
160
- // Look step (task 1004421) to 11. A guard left behind would reset a second project
161
- // sitting on its review, silently wiping it.
162
- for (const step of [10, 11, 12]) {
159
+ test('a draft on the checks, the review or the look is live too — the last three screens (tasks 1004351, 1004504)', () => {
160
+ // Every renumbering has moved the review: 7 to 8 for the app step, 10 for the front door
161
+ // (task 1004417), 11 for the Look step (task 1004421), 4 for the six blended screens
162
+ // (task 1004504). A guard left behind would reset a second project sitting on its
163
+ // review, silently wiping it.
164
+ for (const step of [3, 4, 5]) {
163
165
  const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
164
166
  assert.equal(calls.resets, 0, `step ${step} is a live draft`);
165
167
  assert.deepEqual(calls.show, [[step, true]]);
@@ -167,7 +169,7 @@ test('a draft on the app step, the Look step or the review is live too — the l
167
169
  }
168
170
  });
169
171
 
170
- test('a draft on the front door is live too — a founder part-way through it keeps their place (task 1004417)', () => {
172
+ test('a draft on the fork or the first question is live too — a founder part-way through keeps their place (tasks 1004417, 1004504)', () => {
171
173
  for (const step of [1, 2]) {
172
174
  const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
173
175
  assert.equal(calls.resets, 0, `step ${step} is a live draft`);
@@ -175,8 +177,8 @@ test('a draft on the front door is live too — a founder part-way through it ke
175
177
  }
176
178
  });
177
179
 
178
- test('a draft whose step is missing or junk resets too — only 1-12 preserves anything', () => {
179
- for (const draft of [{ step: 0 }, { step: 13 }, { name: 'half-typed' }]) {
180
+ test('a draft whose step is missing or junk resets too — only 1-5 preserves anything', () => {
181
+ for (const draft of [{ step: 0 }, { step: 6 }, { name: 'half-typed' }]) {
180
182
  const { calls } = runEnter({ focus: true, record: REC, draft });
181
183
  assert.equal(calls.resets, 1, `step ${JSON.stringify(draft.step)} is not a wizard step`);
182
184
  assert.deepEqual(calls.show, [[1, false]]);
@@ -201,13 +203,14 @@ test('paintResumeOffer names the project by textContent and hides on null (no li
201
203
  });
202
204
 
203
205
  test('the offer ships as real markup inside step 1, hidden until there is something to resume', () => {
204
- // step 1 is the front door's terms screen since task 1004417 — the offer moved with it
205
- const panel = HUB.slice(HUB.indexOf('id="panel-terms"'), HUB.indexOf('id="panel-fork"'));
206
+ // step 1 is the fork since task 1004504 (the terms are its tick) — the offer lives there
207
+ const panel = HUB.slice(HUB.indexOf('id="panel-fork"'), HUB.indexOf('id="panel-what"'));
208
+ assert.ok(HUB.indexOf('id="panel-fork"') > -1 && HUB.indexOf('id="panel-what"') > HUB.indexOf('id="panel-fork"'), 'step 1 is findable');
206
209
  assert.match(panel, /<p class="resumeOffer" id="wizResume" hidden>still watching <strong id="wizResumeName">your project<\/strong> — <a href="#" id="wizResumeGo">resume it<\/a><\/p>/,
207
210
  'baked into the panel (C7), carrying its own hidden state');
208
211
  assert.ok(panel.indexOf('id="wizResume"') < panel.indexOf('<form data-next="2">'),
209
- 'it sits above the terms form, where a visitor reads it before anything else');
210
- assert.doesNotMatch(HUB.slice(HUB.indexOf('id="panel-name"'), HUB.indexOf('id="panel-type"')), /id="wizResume"/,
212
+ 'it sits above the fork\'s form, where a visitor reads it before anything else');
213
+ assert.equal((HUB.match(/id="wizResume"/g) || []).length, 1,
211
214
  'and only once — a second copy would be a duplicate id');
212
215
  assert.match(SCRIPT, /\$\('wizResumeGo'\)\.addEventListener\('click', function \(ev\) \{\n\s*ev\.preventDefault\(\);\n\s*resumeIntent = true;\n\s*enter\(true\);/,
213
216
  'the link raises the one-shot intent and re-enters, rather than href-navigating');
@@ -1,11 +1,12 @@
1
1
  // tests/wizard_physics.mjs — planet physics, questions 1 to 4, in the create wizard
2
- // (task 1004418 / BV2.PS07, goal 1000121).
2
+ // (task 1004418 / BV2.PS07, goal 1000121; regrouped by task 1004504).
3
3
  //
4
- // Done-when: the four questions and their branches work end to end (state incl. the
5
- // existing-project branch and the stop screen; geography; ages with the guardian
6
- // tiers; entity, intent when new and legal type when it exists), the planet states
7
- // match the canvas, answers save through the create's `detail`, and the options the
8
- // soft-limit table hides or greys show their reason (spec D6).
4
+ // Done-when: the questions and their branches work end to end (geography; ages with
5
+ // the guardian tiers; entity, intent when new and legal type when it exists; the stop
6
+ // screen), answers save through the create's `detail`, and the options the soft-limit
7
+ // table hides or greys show their reason (spec D6). Since task 1004504 geography, ages
8
+ // and entity are the "few quick checks" screen and the project's state is the review's
9
+ // Code row; the forming planet left with the rail (the night room replaced it).
9
10
  //
10
11
  // Three layers, all RUN: the server's pre-create check over a mounted router; the
11
12
  // page's vocabularies against planet-physics.js (a page cannot require a module, so
@@ -93,28 +94,41 @@ test('a builder who loops the check is slowed down, and another builder is not',
93
94
  });
94
95
 
95
96
  // ── the page's vocabularies are the server's ─────────────────────────────────
96
- // questions 5 to 7 joined the same region (task 1004419, tests/wizard_physics_more.mjs)
97
- const SCRIPT = HUB.slice(HUB.indexOf('/* ── planet physics, questions 1 to 7 (tasks 1004418 and 1004419, BV2.PS07 + PS11) ──'),
98
- HUB.indexOf(' /* per-panel validation on Next */'));
97
+ // questions 5 to 7 joined the same region (task 1004419, tests/wizard_physics_more.mjs);
98
+ // the region runs to the row checks the old steps made on Next (task 1004504)
99
+ const START = '/* ── planet physics (tasks 1004418 and 1004419, BV2.PS07 + PS11; regrouped by task 1004504) ──';
100
+ const END = ' /* ── checks the old steps made on Next, now made by their rows ── */';
101
+ assert.ok(HUB.indexOf(START) > -1, 'the physics region opens with its sentinel comment');
102
+ assert.ok(HUB.indexOf(END, HUB.indexOf(START)) > HUB.indexOf(START), 'and closes where the row checks begin');
103
+ const SCRIPT = HUB.slice(HUB.indexOf(START), HUB.indexOf(END, HUB.indexOf(START)));
99
104
 
100
105
  test('the page carries the server\'s countries, regions and entity forms, exactly', () => {
101
106
  assert.ok(SCRIPT.length > 1000, 'the physics controller was found');
107
+ assert.ok(SCRIPT.includes('function checksAdvance()'), 'and it carries the quick checks\' Continue');
102
108
  const box = {};
103
109
  vm.createContext(box);
104
- vm.runInContext(SCRIPT.slice(0, SCRIPT.indexOf(' var PHYS_LAST')).replace(/\bvar /g, 'this.'), box);
110
+ vm.runInContext(SCRIPT.slice(0, SCRIPT.indexOf(' var physLimits = [];')).replace(/\bvar /g, 'this.'), box);
105
111
  const plain = (v) => JSON.parse(JSON.stringify(v)); // values born in the vm
106
112
  assert.deepEqual(plain(box.PHYS_COUNTRIES), [...physics.COUNTRY_CODES]);
107
113
  assert.deepEqual(Object.keys(box.PHYS_REGIONS).sort(), [...physics.REGION_CODES].sort());
108
114
  assert.deepEqual(plain(box.PHYS_ENTITY_FORMS), [...physics.ENTITY_FORMS]);
109
- for (const k of Object.keys(box.PHYS_MATERIAL)) {
115
+ // every entity the page has words for is an entity answer (PHYS_MATERIAL, the plates
116
+ // per entity, left with the forming planet in task 1004504)
117
+ assert.equal(box.PHYS_MATERIAL, undefined, 'the per-entity plates are gone');
118
+ for (const k of Object.keys(box.PHYS_SAY.entity)) {
110
119
  assert.notEqual(physics.canonicalAnswer(physics.PLANET_PHYSICS_QUESTIONS.entity, k), undefined, `${k} is an entity answer`);
111
120
  }
112
121
  });
113
122
 
114
123
  test('every choice the questions offer is an answer the server accepts', () => {
115
- const panel = HUB.slice(HUB.indexOf('id="panel-physics"'), HUB.indexOf('<!-- step 4 · name'));
124
+ // the quick checks, and the rows of the review that host the rest (task 1004504)
125
+ const panel = HUB.slice(HUB.indexOf('<section id="view-new"'), HUB.indexOf('<!-- 5 · a look, then Create'));
126
+ const checks = HUB.slice(HUB.indexOf('id="panel-checks"'), HUB.indexOf('<!-- 4 · check what we set up'));
127
+ assert.deepEqual([...new Set([...checks.matchAll(/data-phys="([a-z_]+)"/g)].map((m) => m[1]))], ['builder_geography', 'minors_on_board', 'entity'],
128
+ 'the quick checks ask where builders will be, about under-18s and who is behind it');
129
+ assert.match(checks, /<select id="physCountry">/, 'and where you are');
116
130
  const seen = [...panel.matchAll(/data-phys="([a-z_]+)" data-val="([^"]+)"/g)];
117
- assert.ok(seen.length >= 20, 'the four questions\' choices');
131
+ assert.ok(seen.length >= 20, 'the questions\' choices');
118
132
  for (const [, key, raw] of seen) {
119
133
  const one = raw === 'true' ? true : raw === 'false' ? false : raw;
120
134
  // a list question's choice is one member of the list it sends (task 1004419)
@@ -128,9 +142,10 @@ test('every choice the questions offer is an answer the server accepts', () => {
128
142
  });
129
143
 
130
144
  test('the age tiers are described, never claimed as enforced (spec D6)', () => {
131
- const ages = HUB.slice(HUB.indexOf('id="physAges"'), HUB.indexOf('<div class="physQ" data-q="4"'));
145
+ const ages = HUB.slice(HUB.indexOf('id="physAges"'), HUB.indexOf('id="physIntent"'));
146
+ assert.ok(ages.length > 100, 'the age tiers sit between the under-18 question and who is behind it');
132
147
  assert.match(ages, /13 to 15<\/dt><dd>Non-monetary tracks only/);
133
- assert.match(ages, /the platform does not check ages for you yet/);
148
+ assert.match(ages, /[Tt]he platform does not check ages for you yet/);
134
149
  assert.doesNotMatch(ages, /apply automatically|enforced/i);
135
150
  });
136
151
 
@@ -157,11 +172,13 @@ function boot({ checkAnswer = { stop: false, soft_limits: { limits: [] } } } = {
157
172
  const $ = (id) => (els[id] = els[id] || new El(id));
158
173
  const shown = [];
159
174
  const fetched = [];
160
- const state = { description: '', teamShape: null, physics: null };
175
+ const state = { description: '', teamShape: null, physics: null, mode: 'greenfield', confirmed: {} };
161
176
  const sandbox = {
162
177
  $, state, API: '/api/bongos', document: { createElement: () => new El('opt') },
163
178
  root: { querySelectorAll: () => [], querySelector: () => null },
164
179
  saveDraft() {}, show(n) { shown.push(n); }, Intl,
180
+ // the review's machinery lives outside this region (tests/wizard_physics_more.mjs runs it)
181
+ paintSetup() {}, unconfirm() {}, setMode(m) { state.mode = m; }, authKnown: true,
165
182
  fetch: async (url, opts) => { fetched.push({ url, body: opts && opts.body && JSON.parse(opts.body) }); return { ok: true, json: async () => (url.includes('physics-check') ? checkAnswer : []) }; },
166
183
  };
167
184
  vm.createContext(sandbox);
@@ -170,51 +187,56 @@ function boot({ checkAnswer = { stop: false, soft_limits: { limits: [] } } } = {
170
187
  }
171
188
  const tick = () => new Promise((r) => setTimeout(r, 0));
172
189
 
173
- test('Continue walks the questions and moves the wizard on only after the last', async () => {
174
- const w = boot();
190
+ test('Continue on the quick checks asks for each answer, then the server, and only then moves on', async () => {
191
+ const w = boot({ checkAnswer: { stop: false, soft_limits: { limits: [{ option: 'equity', effect: 'note', reason: 'r' }] } } });
175
192
  const a = () => w.state.physics.answers;
176
- assert.equal(w.run('physAdvance()'), false, 'the step never says yes itself');
177
- assert.equal(w.state.physics.q, 1, 'from the start screen to question 1');
178
- w.run('physAdvance()');
179
- assert.equal(w.state.physics.q, 1, 'question 1 unanswered: it stays');
180
- assert.match(w.$('h-phys').textContent, /new or existing/);
181
- a().project_state = 'existing';
182
- w.run('physAdvance()');
183
- assert.match(w.$('h-phys').textContent, /what already exists/i, 'an existing planet must say what exists');
184
- a().existing_work = ['code', 'digital-files']; a().main_work = 'code';
185
- w.state.description = 'A co-op farming game.';
186
- w.run('physAdvance()');
187
- await tick(); await tick();
188
- assert.equal(w.state.physics.q, 2, 'the screen passed it, on to geography');
189
- assert.deepEqual(w.fetched.at(-1).body, { description: 'A co-op farming game.', detail: { project_state: 'existing', existing_work: ['code', 'digital-files'], main_work: 'code' } });
190
- w.run('physAdvance()');
191
- assert.equal(w.state.physics.q, 2, 'no country: it stays');
192
- a().home_country = 'US'; a().home_region = 'US-CA'; a().builder_geography = 'us-eu';
193
- w.run('physAdvance()');
194
- assert.equal(w.state.physics.q, 3);
193
+ assert.equal(w.run('checksAdvance()'), false, 'the screen never says yes itself');
194
+ assert.match(w.$('h-phys').textContent, /Pick your country/);
195
+ a().home_country = 'US'; a().home_region = 'US-CA';
196
+ w.run('checksAdvance()');
197
+ assert.match(w.$('h-phys').textContent, /where builders will be/);
198
+ a().builder_geography = 'us-eu';
199
+ w.run('checksAdvance()');
200
+ assert.match(w.$('h-phys').textContent, /under 18/);
195
201
  a().minors_on_board = true;
196
- w.run('physAdvance()');
197
- assert.equal(w.state.physics.q, 4);
198
- w.run('physAdvance()');
199
- assert.deepEqual(w.shown, [], 'no entity: still here');
202
+ w.run('checksAdvance()');
203
+ assert.match(w.$('h-phys').textContent, /who’s behind it/);
204
+ assert.equal(w.fetched.length, 0, 'nothing is asked of the server until every answer is given');
200
205
  a().entity = 'for-profit';
201
- w.run('physAdvance()');
206
+ w.state.description = 'A co-op farming game.';
207
+ w.run('checksAdvance()');
208
+ w.run('checksAdvance()');
202
209
  await tick(); await tick();
203
- // task 1004419: questions 5 to 7 and the summary follow inside the same step
204
- // (walked to the name step in tests/wizard_physics_more.mjs)
205
- assert.equal(w.state.physics.q, 5, 'on to question 5');
206
- assert.deepEqual(w.shown, [], 'and the wizard has not moved on');
210
+ assert.equal(w.fetched.length, 1, 'a second Continue while the first is out asks nothing more');
211
+ assert.equal(w.fetched[0].url, '/api/bongos/provisioning/physics-check');
212
+ // the review's prefills are written on the way: a new project, one person, all managed
213
+ assert.deepEqual(w.fetched[0].body, { description: 'A co-op farming game.', detail: {
214
+ project_state: 'new', home_country: 'US', home_region: 'US-CA', builder_geography: 'us-eu', minors_on_board: true, entity: 'for-profit',
215
+ builders_at_start: 1, builders_expected: 1, framing: 'managed', voice: 'professional' } });
216
+ assert.equal(w.state.teamShape, 'solo', 'one person is a solo team shape');
217
+ assert.equal(w.$('h-phys').textContent, ' ', 'the ask clears');
218
+ assert.deepEqual(w.shown, [4], 'the screen passed it: on to the review');
219
+ assert.equal(JSON.stringify(w.run('physLimits')), JSON.stringify([{ option: 'equity', effect: 'note', reason: 'r' }]), 'and its soft limits shape the review');
207
220
  });
208
221
 
209
- test('a stopped description lands on the stop screen with the server\'s words', async () => {
222
+ test('a stopped description lands on the stop screen with the server\'s words, and Start over clears it all', async () => {
210
223
  const w = boot({ checkAnswer: { stop: true, message: STOP_MESSAGE, soft_limits: { limits: [] } } });
211
- w.run('physAdvance()');
212
- w.state.physics.answers.project_state = 'new';
224
+ w.run('phys()');
225
+ Object.assign(w.state.physics.answers, { home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'intent-solo' });
213
226
  w.state.description = 'ransomware';
214
- w.run('physAdvance()');
227
+ w.run('checksAdvance()');
215
228
  await tick(); await tick();
216
- assert.equal(w.state.physics.q, 'stop');
217
229
  assert.equal(w.$('physStopMsg').textContent, STOP_MESSAGE);
230
+ assert.equal(w.$('checksStop').hidden, false, 'the stop screen shows');
231
+ assert.equal(w.$('checksMain').hidden, true, 'in place of the questions');
232
+ assert.deepEqual(w.shown, [], 'and nothing moves on: nothing exists to undo');
233
+ w.state.confirmed = { code: true };
234
+ w.$('physStartOver').listeners.click();
235
+ assert.equal(w.state.description, '');
236
+ assert.equal(JSON.stringify(w.state.physics.answers), '{}', 'every answer goes');
237
+ assert.equal(JSON.stringify(w.state.confirmed), '{}', 'and every confirmed row');
238
+ assert.deepEqual(w.shown, [2], 'back to "What are you making?"');
239
+ assert.equal(w.$('checksMain').hidden, false, 'with the questions back in place');
218
240
  });
219
241
 
220
242
  test('the create sends the answers as detail: existing work only for an existing planet, the team shape beside them', () => {
@@ -223,49 +245,45 @@ test('the create sends the answers as detail: existing work only for an existing
223
245
  Object.assign(w.state.physics.answers, { project_state: 'new', existing_work: ['code'], main_work: 'code', home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'intent-solo' });
224
246
  w.state.teamShape = 'solo';
225
247
  assert.deepEqual(JSON.parse(JSON.stringify(w.run('createDetail()'))), {
226
- project_state: 'new', home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'intent-solo', voice: 'planet', team_shape: 'solo',
227
- });
248
+ project_state: 'new', home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'intent-solo', voice: 'professional', team_shape: 'solo',
249
+ }, 'one plain voice since task 1004504: an individual is sent the professional voice too');
250
+ w.state.physics.answers.entity = 'for-profit';
251
+ assert.equal(w.run('createDetail()').voice, 'professional', 'and so is an organization');
252
+ w.state.physics.answers.project_state = 'existing';
253
+ assert.deepEqual(JSON.parse(JSON.stringify(w.run('createDetail()'))).existing_work, ['code'], 'an existing planet sends what exists');
254
+ w.state.demo = { people: 3, hours_each: 4 };
255
+ assert.deepEqual(JSON.parse(JSON.stringify(w.run('createDetail()'))), { team_shape: 'solo' }, 'a demo sends only its team shape');
256
+ w.state.demo = null;
228
257
  w.state.physics.answers = {}; w.state.teamShape = null;
229
258
  assert.equal(w.run('createDetail()'), undefined, 'nothing answered: the detail is omitted, as before');
230
259
  });
231
260
 
232
- test('the planet forms from the answers: a seed, a formed planet, the protective ring, the core material', () => {
233
- const w = boot();
234
- w.run('phys()');
235
- const src = () => w.$('physPlanet').getAttribute('src');
236
- w.state.physics.q = 1; w.state.physics.answers.project_state = 'new';
237
- w.run('physPaintStage()');
238
- assert.equal(src(), '/assets/world/r2-chrome-blob-one-cut.webp', 'a seed');
239
- w.state.physics.answers.project_state = 'existing';
240
- w.run('physPaintStage()');
241
- assert.equal(src(), '/assets/world/r2-chrome-blobs-set-cut.webp', 'terrain already formed');
242
- assert.equal(w.$('physGuard').hidden, true);
243
- w.state.physics.q = 3; w.state.physics.answers.minors_on_board = true;
244
- w.run('physPaintStage()');
245
- assert.equal(w.$('physGuard').hidden, false, 'the protective ring');
246
- w.state.physics.q = 4;
247
- for (const [entity, plate] of [['intent-solo', 'r3-frost-blob-cut'], ['for-profit', 'r3-gold-blob-cut'], ['nonprofit', 'r3-obsidian-blob-cut']]) {
248
- w.state.physics.answers.entity = entity;
249
- w.run('physPaintStage()');
250
- assert.equal(src(), `/assets/world/${plate}.webp`, entity);
251
- }
252
- for (const plate of ['r3-frost-blob-cut', 'r3-gold-blob-cut', 'r3-obsidian-blob-cut', 'r2-chrome-blob-one-cut', 'r2-chrome-blobs-set-cut']) {
253
- assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', `${plate}.webp`)), `${plate} is a world plate`);
261
+ test('the forming planet is gone: the night room stands behind the wizard instead (task 1004504)', () => {
262
+ for (const gone of ['physPaintStage', 'PHYS_MATERIAL', 'PHYS_CORE_WORD', 'PHYS_ORBIT', 'physAdvance', 'physNextQ', 'physPrevQ', 'physSummaryRows']) {
263
+ assert.ok(!HUB.includes(gone), `${gone} is gone`);
254
264
  }
265
+ assert.doesNotMatch(HUB, /id="(physPlanet|physGuard|physStage|panel-physics|physSummary)"/, 'and so is its markup');
266
+ assert.match(HUB, /<div class="wizRoom" id="wizRoom" data-screen="fork" aria-hidden="true">/, 'the room is decoration only');
267
+ const seed = HUB.match(/<span class="rmSeed"><img src="\/assets\/world\/([a-z0-9-]+)\.webp" alt=""><\/span>/);
268
+ assert.ok(seed, 'the room\'s seed is a world plate');
269
+ assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', `${seed[1]}.webp`)), `${seed[1]} exists`);
255
270
  });
256
271
 
257
272
  test('a limit the table puts on an option shows its reason beside it, and a grey one is disabled', () => {
258
273
  const w = boot();
259
274
  const why = new El('why');
260
275
  const btn = new El('btn');
276
+ const fieldset = new El('physExisting');
261
277
  w.sandbox.root.querySelector = (sel) => (sel.includes('data-why="existing_work"') ? why : null);
262
- w.sandbox.root.querySelectorAll = (sel) => (sel.includes('data-phys="entity"][data-val="intent-solo"') ? [btn] : []);
278
+ w.sandbox.root.querySelectorAll = (sel) => (sel.includes('data-phys="entity"][data-val="intent-solo"') ? [btn]
279
+ : sel.includes('[data-limit="existing_work"]') ? [fieldset] : []);
263
280
  w.run(`physLimits = [
264
281
  { option: 'existing_work', effect: 'hide', reason: 'Your planet is new, so there is nothing built yet to describe.' },
265
282
  { option: 'entity:intent-solo', effect: 'grey', reason: 'Because.' }
266
283
  ]; physApplyLimits();`);
267
284
  assert.equal(why.hidden, false);
268
285
  assert.match(why.textContent, /nothing built yet/);
286
+ assert.equal(fieldset.hidden, true, 'a question the table hides is hidden in place');
269
287
  assert.equal(btn.disabled, true);
270
288
  assert.equal(btn.title, 'Because.');
271
289
  });