@bongos/core 1.21.83 → 1.21.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +50 -40
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0369-ship-mode-decides-who-starts-a-ship-not-how-it-lands.md +1 -0
- package/docs/api/openapi.json +7 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +32 -28
- package/docs/copy-registry.json +70 -34
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +121 -113
- package/modules/lifecycle/page-tweak-hold.js +12 -0
- package/modules/lifecycle/routes/tasks.js +4 -3
- package/modules/lifecycle/ship-card.js +18 -1
- package/modules/provisioning/birth.js +28 -1
- package/modules/provisioning/routes/provisioning.js +3 -1
- package/modules/public-landing/public/projects.html +46 -5
- package/modules/public-landing/public/projects.states.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/ship-finish.js +4 -2
- package/scripts/gds/ship-flow.js +4 -2
- package/scripts/gds/ship-grade-steps.js +7 -2
- package/scripts/gds/ship-land.js +2 -2
- package/src/bongos/project-settings.js +16 -2
- package/src/branding.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/held_tweak_reporting.mjs +164 -0
- package/tests/page_tweak_hold.mjs +1 -2
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_app_step.mjs +1 -1
- package/tests/provision_settings_apply.mjs +4 -1
- package/tests/provisioning_settings_env.mjs +3 -1
- package/tests/wizard_physics_more.mjs +5 -5
- package/tests/wizard_ship_mode.mjs +187 -0
- package/tests/wizard_six_screens.mjs +3 -2
|
@@ -78,6 +78,7 @@ ${NEUTRAL_PREFIX}_LOGO_URL=
|
|
|
78
78
|
${NEUTRAL_PREFIX}_FOUNDING_BIRTH=
|
|
79
79
|
${NEUTRAL_PREFIX}_DEMO_PEOPLE=
|
|
80
80
|
${NEUTRAL_PREFIX}_DEMO_HOURS=
|
|
81
|
+
${NEUTRAL_PREFIX}_SHIP_MODE=
|
|
81
82
|
${NEUTRAL_PREFIX}_HUB_UPGRADE_URL=
|
|
82
83
|
`;
|
|
83
84
|
|
|
@@ -148,6 +149,8 @@ t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at stan
|
|
|
148
149
|
[`${NEUTRAL_PREFIX}_LOOK_ACCENT`]: '', [`${NEUTRAL_PREFIX}_LOGO_URL`]: '',
|
|
149
150
|
// the birth marker and a demo's box (task 1004506), empty on an un-stamped row
|
|
150
151
|
[`${NEUTRAL_PREFIX}_FOUNDING_BIRTH`]: '', [`${NEUTRAL_PREFIX}_DEMO_PEOPLE`]: '', [`${NEUTRAL_PREFIX}_DEMO_HOURS`]: '',
|
|
152
|
+
// the wizard's ship mode (task 1004629), empty when none was picked
|
|
153
|
+
[`${NEUTRAL_PREFIX}_SHIP_MODE`]: '',
|
|
151
154
|
// where the hall's Update button goes (task 1004608), empty when no hub origin resolves
|
|
152
155
|
[`${NEUTRAL_PREFIX}_HUB_UPGRADE_URL`]: '',
|
|
153
156
|
});
|
|
@@ -250,7 +253,7 @@ await ta('dry-run PLANS the patch: reads nothing real, writes NOTHING (no synthe
|
|
|
250
253
|
assert.equal(r.ok, true);
|
|
251
254
|
assert.equal(r.healthy, null, 'nothing ran, nothing observed');
|
|
252
255
|
assert.equal(writes.length, 0, 'a dry-run must not plan a two-line overwrite of web.env');
|
|
253
|
-
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_SPEND_PAYER, .*_LOOK, .*_NIGHT_TINT, .*_ARTIST_GATE_SINCE, .*_LOOK_ACCENT, .*_LOGO_URL, .*_FOUNDING_BIRTH, .*_DEMO_PEOPLE, .*_DEMO_HOURS, .*_HUB_UPGRADE_URL in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
256
|
+
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_SPEND_PAYER, .*_LOOK, .*_NIGHT_TINT, .*_ARTIST_GATE_SINCE, .*_LOOK_ACCENT, .*_LOGO_URL, .*_FOUNDING_BIRTH, .*_DEMO_PEOPLE, .*_DEMO_HOURS, .*_SHIP_MODE, .*_HUB_UPGRADE_URL in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
254
257
|
assert.ok(cmds.some((c) => /restart spike/.test(c)), 'the restart is still in the plan');
|
|
255
258
|
assert.ok(logs.some((l) => /dry-run — nothing ran/.test(l)), 'the closing line does not claim a restart happened');
|
|
256
259
|
});
|
|
@@ -127,6 +127,8 @@ t('settingsEnvVars is the single source both legs derive from — same keys, sam
|
|
|
127
127
|
// The birth marker and a demo's box (task 1004506): empty on a row the create
|
|
128
128
|
// route never stamped, which is every row made before it shipped (spec D8).
|
|
129
129
|
CLOUDBONGOS_FOUNDING_BIRTH: '', CLOUDBONGOS_DEMO_PEOPLE: '', CLOUDBONGOS_DEMO_HOURS: '',
|
|
130
|
+
// The wizard's ship mode (task 1004629): empty when none was picked.
|
|
131
|
+
CLOUDBONGOS_SHIP_MODE: '',
|
|
130
132
|
});
|
|
131
133
|
// varsOf splits on '=', so an empty value reads as '' on both sides — the point
|
|
132
134
|
// of the comparison is that the standup lines carry exactly the same key set.
|
|
@@ -135,7 +137,7 @@ t('settingsEnvVars is the single source both legs derive from — same keys, sam
|
|
|
135
137
|
X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply', X_VISIBILITY: 'public', X_JOIN_GRANT: 'full',
|
|
136
138
|
X_ARTIST_GATE: 'strict', X_ARTIST_GATE_SINCE: '', X_SPEND_PAYER: 'none',
|
|
137
139
|
X_LOOK: 'own', X_NIGHT_TINT: 'on', X_LOOK_ACCENT: '', X_LOGO_URL: '',
|
|
138
|
-
X_FOUNDING_BIRTH: '', X_DEMO_PEOPLE: '', X_DEMO_HOURS: '',
|
|
140
|
+
X_FOUNDING_BIRTH: '', X_DEMO_PEOPLE: '', X_DEMO_HOURS: '', X_SHIP_MODE: '',
|
|
139
141
|
}, 'defaults, explicitly');
|
|
140
142
|
});
|
|
141
143
|
|
|
@@ -377,13 +377,13 @@ test('rewards are hidden for a solo founder: the real table hides the row, and t
|
|
|
377
377
|
assert.equal(w.run("rowApplies('rewards')"), false, 'the REAL softLimitsFor hides reward_intents for one-and-one');
|
|
378
378
|
assert.equal(w.row('rewards').hidden, true, 'and the row goes with it');
|
|
379
379
|
assert.ok(!w.run('unconfirmedRows()').includes('rewards'), 'nothing to confirm for a question never asked');
|
|
380
|
-
assert.equal(w.run('unconfirmedRows()').length,
|
|
380
|
+
assert.equal(w.run('unconfirmedRows()').length, 7, 'seven rows apply');
|
|
381
381
|
assert.equal(w.run('createDetail()').reward_intents, undefined);
|
|
382
382
|
w.change('team');
|
|
383
383
|
w.q('[data-phys-solo="many"]')[0].click();
|
|
384
384
|
await w.settle();
|
|
385
385
|
assert.equal(w.row('rewards').hidden, false, 'a team brings the question back, once the table is asked again');
|
|
386
|
-
assert.equal(w.run('unconfirmedRows()').length,
|
|
386
|
+
assert.equal(w.run('unconfirmedRows()').length, 8);
|
|
387
387
|
});
|
|
388
388
|
|
|
389
389
|
test('rewards: a greyed option is disabled and leaves the list, and each reason is said once', async () => {
|
|
@@ -482,9 +482,9 @@ test('one "Looks right, continue" confirms every row that applies, each through
|
|
|
482
482
|
assert.equal(w.go().disabled, false, 'Continue is never locked: it is the confirmation');
|
|
483
483
|
assert.equal(w.go().textContent, 'Looks right, continue');
|
|
484
484
|
assert.equal(w.row('code').querySelector('[data-row-ok]'), null, 'no row carries a Looks right of its own');
|
|
485
|
-
assert.equal(w.run('unconfirmedRows()').length,
|
|
485
|
+
assert.equal(w.run('unconfirmedRows()').length, 8, 'nothing is confirmed before the founder says so');
|
|
486
486
|
assert.equal(w.run('validateStep(4)'), true, 'one Continue confirms them all');
|
|
487
|
-
for (const key of ['code', 'address', 'app', 'extras', 'team', 'rewards', 'run']) assert.equal(w.state.confirmed[key], true, key);
|
|
487
|
+
for (const key of ['code', 'address', 'app', 'extras', 'team', 'rewards', 'run', 'ship']) assert.equal(w.state.confirmed[key], true, key);
|
|
488
488
|
assert.equal(w.state.modules && plain(w.state.modules).join(), 'discord', 'confirming the extras is the answer the create sends');
|
|
489
489
|
w.change('team');
|
|
490
490
|
assert.equal(w.state.confirmed.team, undefined, 'Edit takes a row back');
|
|
@@ -629,7 +629,7 @@ test('no path reaches Look without the founder confirming the address: a prefill
|
|
|
629
629
|
assert.equal(w.state.confirmed.address, undefined, `${label}: the prefills leave the address unconfirmed`);
|
|
630
630
|
assert.equal(w.run("rowFixed('address')"), false, `${label}: the address is never a fixed row`);
|
|
631
631
|
assert.equal(w.row('address').querySelector('[data-row-chg]').hidden, false, `${label}: it can always be changed`);
|
|
632
|
-
for (const key of ['code', 'app', 'extras', 'team', 'rewards', 'run']) if (w.run(`rowApplies('${key}')`)) w.ok(key);
|
|
632
|
+
for (const key of ['code', 'app', 'extras', 'team', 'rewards', 'run', 'ship']) if (w.run(`rowApplies('${key}')`)) w.ok(key);
|
|
633
633
|
assert.deepEqual(plain(w.run('unconfirmedRows()')), ['address'], `${label}: every other row confirmed, the address still asked`);
|
|
634
634
|
// the address is on the page the founder confirms (task 1004550): only their own
|
|
635
635
|
// "Looks right, continue" — or Done on the row — confirms it, and its check still runs
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// tests/wizard_ship_mode.mjs — the project-creation wizard asks how finished work ships,
|
|
2
|
+
// and the new project is born with that answer (task 1004629, ADR 0369).
|
|
3
|
+
//
|
|
4
|
+
// Pinned here, end to end:
|
|
5
|
+
// 1. the wizard's review has a Shipping row: Auto first and checked, one plain sentence
|
|
6
|
+
// each, "you can change it later", and the create request carries ship_mode;
|
|
7
|
+
// 2. the create route accepts only auto|manual and stamps it, write-once, on the row it
|
|
8
|
+
// just made (modules/provisioning/birth.js);
|
|
9
|
+
// 3. the stamp rides the new instance's web.env as <PREFIX>_SHIP_MODE, which
|
|
10
|
+
// src/branding.js maps onto the server-only founding.shipMode;
|
|
11
|
+
// 4. the instance's resolveShipMode reads it as a CHOSEN mode while no project_settings
|
|
12
|
+
// row exists, and a row (the settings page, the CLI) always wins.
|
|
13
|
+
//
|
|
14
|
+
// Run: node --test tests/wizard_ship_mode.mjs
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { test, after } from 'node:test';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import vm from 'node:vm';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
27
|
+
|
|
28
|
+
// Destructured from literal relative paths, so the dead-code scan (knip) can trace them.
|
|
29
|
+
const { SHIP_MODE_KEY, stampShipMode, BIRTH_COMPANION_ENV } = require('../modules/provisioning/birth.js');
|
|
30
|
+
const { resolveShipMode } = require('../src/bongos/project-settings.js');
|
|
31
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
32
|
+
const poolMod = require('../src/bongos/pool.js');
|
|
33
|
+
|
|
34
|
+
function slice(start, end, what) {
|
|
35
|
+
const a = HUB.indexOf(start), b = HUB.indexOf(end, a);
|
|
36
|
+
assert.ok(a > 0 && b > a, `slice for ${what} not found — a rename moved it`);
|
|
37
|
+
return HUB.slice(a, b);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── 1. the wizard ──────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
test('the review asks how finished work ships: Auto (recommended) first and checked, Manual second', () => {
|
|
43
|
+
const row = slice('<div class="setRow" data-row="ship">', '<div class="hint" id="h-review">', 'the ship row');
|
|
44
|
+
const auto = row.indexOf('value="auto"'), manual = row.indexOf('value="manual"');
|
|
45
|
+
assert.ok(auto > 0 && manual > auto, 'Auto comes first');
|
|
46
|
+
assert.match(row, /<input type="radio" name="shipMode" value="auto" checked>/, 'and is the pick until the founder says otherwise');
|
|
47
|
+
assert.match(row, /<b>Auto \(recommended\)<\/b>/);
|
|
48
|
+
assert.match(row, /<b>Manual<\/b>/);
|
|
49
|
+
assert.match(row, /ships finished work on its own/, 'one plain sentence for Auto');
|
|
50
|
+
assert.match(row, /waits for you to approve it/, 'and one for Manual');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('the row says what was picked and that it can be changed later; an older draft reads Auto', () => {
|
|
54
|
+
const fns = slice(' /* the ship row (task 1004629)', ' var ROWS = [', 'the ship choice');
|
|
55
|
+
const ctx = { state: {} };
|
|
56
|
+
vm.runInNewContext(`${fns}; this.choice = shipModeChoice;`, ctx);
|
|
57
|
+
assert.equal(ctx.choice(), 'auto', 'a draft with no shipMode is Auto');
|
|
58
|
+
ctx.state.shipMode = 'manual';
|
|
59
|
+
assert.equal(ctx.choice(), 'manual');
|
|
60
|
+
ctx.state.shipMode = 'yolo';
|
|
61
|
+
assert.equal(ctx.choice(), 'auto', 'anything but manual is Auto');
|
|
62
|
+
const say = slice(" if (key === 'ship') {", " return { v: '', s: '' };", "the ship row's words");
|
|
63
|
+
assert.match(say, /v: 'Auto \(recommended\)'/);
|
|
64
|
+
assert.match(say, /v: 'Manual'/);
|
|
65
|
+
assert.equal((say.match(/You can change this later in your project’s settings\./g) || []).length, 2, 'both say it can be changed later');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('the draft remembers the pick, and Create sends it as ship_mode', () => {
|
|
69
|
+
assert.match(HUB, /appPlatform: 'later', shipMode: 'auto', physics: null/, 'the initial state starts on Auto');
|
|
70
|
+
assert.equal((HUB.match(/shipMode: 'auto'/g) || []).length, 2, 'and so does the reset (the two literals stay equal)');
|
|
71
|
+
assert.match(HUB, /if \(type === 'change' && ev\.target\.name === 'shipMode'\) \{ state\.shipMode = ev\.target\.value; saveDraft\(\); \}/);
|
|
72
|
+
assert.match(HUB, /ship_mode: shipModeChoice\(\)\n {10}\}\)/, 'the create request carries it');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── 2. the stamp ───────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
test('stampShipMode writes a valid answer once, and writes nothing for none or junk', async () => {
|
|
78
|
+
const seen = [];
|
|
79
|
+
const db = { query: async (sql, params) => { seen.push({ sql, params }); return { rows: [] }; } };
|
|
80
|
+
assert.equal(await stampShipMode(db, 41, 'manual'), true);
|
|
81
|
+
assert.equal(seen.length, 1);
|
|
82
|
+
assert.match(seen[0].sql, /UPDATE provisioning_instances/);
|
|
83
|
+
assert.match(seen[0].sql, /WHERE id = \$1 AND NOT \(COALESCE\(settings, '\{\}'::jsonb\) \? \$2::text\)/, 'write-once');
|
|
84
|
+
assert.deepEqual(seen[0].params, [41, SHIP_MODE_KEY, 'manual']);
|
|
85
|
+
assert.equal(await stampShipMode(db, 41, undefined), false, 'no answer: the project asks at its first ship instead');
|
|
86
|
+
assert.equal(await stampShipMode(db, 41, 'yolo'), false);
|
|
87
|
+
assert.equal(seen.length, 1, 'nothing written for either');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('the stamp is not a setting: no client patch can write it and it never projects as one', () => {
|
|
91
|
+
assert.ok(!(SHIP_MODE_KEY in provisioning.SETTINGS_VOCAB));
|
|
92
|
+
assert.ok(!(SHIP_MODE_KEY in provisioning.PLANET_VOCAB));
|
|
93
|
+
assert.ok(!(SHIP_MODE_KEY in provisioning.effectiveSettings({ settings: { [SHIP_MODE_KEY]: 'manual' } })));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── 3. the transport ───────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
test('the stamped mode rides web.env as SHIP_MODE; an unstamped or mangled row sends empty', () => {
|
|
99
|
+
assert.equal(typeof BIRTH_COMPANION_ENV.SHIP_MODE, 'function');
|
|
100
|
+
assert.equal(provisioning.settingsEnvVars({ settings: { ship_mode: 'manual' } }, 'X').X_SHIP_MODE, 'manual');
|
|
101
|
+
assert.equal(provisioning.settingsEnvVars({ settings: { ship_mode: 'auto' } }, 'X').X_SHIP_MODE, 'auto');
|
|
102
|
+
assert.equal(provisioning.settingsEnvVars({ settings: {} }, 'X').X_SHIP_MODE, '');
|
|
103
|
+
assert.equal(provisioning.settingsEnvVars({ settings: { ship_mode: 'sometimes' } }, 'X').X_SHIP_MODE, '');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('the instance reads it into founding.shipMode, and never publishes it to the client', () => {
|
|
107
|
+
const read = (env) => JSON.parse(execFileSync(process.execPath, ['-e',
|
|
108
|
+
"const b=require('./src/branding.js');const r=(b.branding||b)();console.log(JSON.stringify({m:(r.founding||{}).shipMode||null,c:'founding' in b.clientBranding(r)}))"],
|
|
109
|
+
{ cwd: ROOT, encoding: 'utf8', env: { ...process.env, ...env } }));
|
|
110
|
+
assert.deepEqual(read({ CLOUDBONGOS_SHIP_MODE: 'manual' }), { m: 'manual', c: false });
|
|
111
|
+
assert.deepEqual(read({ CLOUDBONGOS_SHIP_MODE: '' }), { m: null, c: false }, 'the empty line every other instance carries is nothing');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── 4. the instance's own resolver ─────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
async function withRow(value, fn) {
|
|
117
|
+
const pool = poolMod.pool;
|
|
118
|
+
const saved = pool.query;
|
|
119
|
+
pool.query = async () => ({ rows: value == null ? [] : [{ key: 'ship.mode', value, updated_at: null, updated_by: 1 }] });
|
|
120
|
+
try { return await fn(); } finally { pool.query = saved; }
|
|
121
|
+
}
|
|
122
|
+
const packWith = (shipMode) => () => ({ founding: { shipMode } });
|
|
123
|
+
|
|
124
|
+
test('a wizard pick counts as chosen while no setting row exists', async () => {
|
|
125
|
+
const r = await withRow(null, () => resolveShipMode({ branding: packWith('manual') }));
|
|
126
|
+
assert.deepEqual(r, { mode: 'manual', source: 'wizard', chosen: true, invalid: null });
|
|
127
|
+
const auto = await withRow(null, () => resolveShipMode({ branding: packWith('auto') }));
|
|
128
|
+
assert.equal(auto.chosen, true, 'picking Auto in the wizard is a choice, so the first ship does not ask again');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('the settings page (a row) always wins over the wizard; junk or no pick is the default, unchosen', async () => {
|
|
132
|
+
const r = await withRow('auto', () => resolveShipMode({ branding: packWith('manual') }));
|
|
133
|
+
assert.equal(r.mode, 'auto');
|
|
134
|
+
assert.equal(r.source, 'project_settings');
|
|
135
|
+
assert.deepEqual(await withRow(null, () => resolveShipMode({ branding: packWith('') })), { mode: 'auto', source: 'default', chosen: false, invalid: null });
|
|
136
|
+
assert.deepEqual(await withRow(null, () => resolveShipMode({ branding: packWith('maybe') })), { mode: 'auto', source: 'default', chosen: false, invalid: null });
|
|
137
|
+
assert.equal((await withRow(null, () => resolveShipMode({ branding: () => { throw new Error('no pack'); } }))).source, 'default', 'a pack that cannot be read is no pick');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── 2b. the create route ───────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
const api = require('../src/module-api.js');
|
|
143
|
+
const birth = require('../modules/provisioning/birth.js');
|
|
144
|
+
provisioning.getInstanceBySlug = async () => null;
|
|
145
|
+
provisioning.getInstanceByDomain = async () => null;
|
|
146
|
+
birth.stampBirth = async () => {};
|
|
147
|
+
provisioning.createInstance = async (_db, args) => ({
|
|
148
|
+
instance: { id: 41, owner_builder_id: 7, slug: args.slug, status: 'requested', hosting_shape: 'byo-host', detail: {} }, created: true,
|
|
149
|
+
});
|
|
150
|
+
provisioning.enqueueIntent = async (_db, _id, action) => ({ intent: { id: 9, action, state: 'pending' }, created: true });
|
|
151
|
+
provisioning.getOpenIntent = async () => null;
|
|
152
|
+
provisioning.recordEvent = async () => {};
|
|
153
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
154
|
+
const stamped = [];
|
|
155
|
+
api.pool.query = async (sql, params) => { if (/jsonb_build_object/.test(sql) && params[1] === SHIP_MODE_KEY) stamped.push(params[2]); return { rows: [] }; };
|
|
156
|
+
|
|
157
|
+
const express = require('express');
|
|
158
|
+
const app = express();
|
|
159
|
+
app.use(express.json());
|
|
160
|
+
app.use((_req, res, next) => {
|
|
161
|
+
res.fail = (code, opts) => {
|
|
162
|
+
const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
|
|
163
|
+
res.status(status).json({ error: { code, message: opts && opts.message } });
|
|
164
|
+
};
|
|
165
|
+
next();
|
|
166
|
+
});
|
|
167
|
+
app.use('/api/bongos', require('../modules/provisioning/routes/provisioning.js')());
|
|
168
|
+
const server = app.listen(0);
|
|
169
|
+
after(() => server.close());
|
|
170
|
+
async function create(body) {
|
|
171
|
+
await new Promise((r) => (server.listening ? r() : server.once('listening', r)));
|
|
172
|
+
const res = await fetch(`http://127.0.0.1:${server.address().port}/api/bongos/provisioning/instances`, {
|
|
173
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
174
|
+
});
|
|
175
|
+
return { status: res.status, body: await res.json() };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
test('the create route stamps the pick on the new row, and refuses anything but auto|manual', async () => {
|
|
179
|
+
stamped.length = 0;
|
|
180
|
+
const r = await create({ slug: 'ships-by-hand', hosting_shape: 'byo-host', ship_mode: 'manual' });
|
|
181
|
+
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
182
|
+
assert.deepEqual(stamped, ['manual']);
|
|
183
|
+
stamped.length = 0;
|
|
184
|
+
assert.equal((await create({ slug: 'no-pick', hosting_shape: 'byo-host' })).status, 201, 'the answer is optional (an API caller)');
|
|
185
|
+
assert.deepEqual(stamped, [], 'and none is stamped when none is given');
|
|
186
|
+
assert.equal((await create({ slug: 'bad-pick', hosting_shape: 'byo-host', ship_mode: 'sometimes' })).status, 400);
|
|
187
|
+
});
|
|
@@ -81,10 +81,10 @@ test('planet words survive only as the light touch: the fork and the alive momen
|
|
|
81
81
|
assert.match(SCRIPT, /if \(inst\.last_verified_up\) \{\n\s*\/\*[^*]*\*\/\n\s*\$\('done-h'\)\.textContent =/, 'said only once the project has answered (Live is a verified claim)');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
test('the review cannot be skipped:
|
|
84
|
+
test('the review cannot be skipped: eight rows, one Edit each, and one Continue that confirms them all (task 1004550)', () => {
|
|
85
85
|
const p = panel('panel-review');
|
|
86
86
|
const rows = [...p.matchAll(/<div class="setRow" data-row="([a-z]+)">/g)].map((m) => m[1]);
|
|
87
|
-
assert.deepEqual(rows, ['code', 'address', 'app', 'extras', 'team', 'rewards', 'run']);
|
|
87
|
+
assert.deepEqual(rows, ['code', 'address', 'app', 'extras', 'team', 'rewards', 'run', 'ship']);
|
|
88
88
|
for (const key of rows) {
|
|
89
89
|
const r = (p.match(new RegExp(`<div class="setRow" data-row="${key}">[\\s\\S]*?<span class="acts">[^\\n]*\\n`)) || [''])[0];
|
|
90
90
|
assert.match(r, /<span class="acts"><button type="button" class="rowChg" data-row-chg aria-expanded="false">Edit<\/button><\/span>/, `${key}: one Edit, nothing else`);
|
|
@@ -110,6 +110,7 @@ test('nothing the old steps could do is lost: their controls are moved whole int
|
|
|
110
110
|
has('extras', ['modQ', 'modCards', 'modWhy', 'modCoreNote']);
|
|
111
111
|
has('team', ['physTeam', 'physStartN', 'physExpected', 'teamCards']);
|
|
112
112
|
has('run', ['physSuite']);
|
|
113
|
+
has('ship', ['shipOpts']);
|
|
113
114
|
for (const id of ['physCountry', 'physRegion', 'physAges', 'physIntent', 'physForms', 'physHaveEntity', 'checksStop', 'physStopMsg', 'physFromAccount']) {
|
|
114
115
|
assert.match(panel('panel-checks'), new RegExp(`id="${id}"`), `the quick checks host #${id}`);
|
|
115
116
|
}
|