@bongos/core 1.19.951 → 1.19.953
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 +67 -62
- package/docs/copy-inventory.md +32 -24
- package/docs/copy-registry.json +100 -28
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/deploy.js +97 -5
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.css +16 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +12 -3
- package/modules/hall-ui/public/sky.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +6 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/provisioning/core-upgrade.js +35 -3
- package/modules/provisioning/migrations/provisioning_022_last_core_move_index.sql +25 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/adopt-preflight.js +1 -1
- package/scripts/gds/device-flow-help.js +1 -1
- package/scripts/gds/init.js +5 -3
- package/scripts/gds/login.js +1 -1
- package/scripts/gds/setup.js +1 -1
- package/src/module-api.js +1 -1
- package/tests/adopt_preflight.mjs +1 -1
- package/tests/bongos_login.mjs +1 -1
- package/tests/core_update_banner.mjs +30 -2
- package/tests/core_upgrade_door.mjs +50 -0
- package/tests/deploy_page_projects.mjs +131 -7
- package/tests/discord_approvals.mjs +1 -1
- package/tests/module_api.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// • CI collisions — the repo's workflows vs core's expected unit/secret-scan/
|
|
13
13
|
// gate-review checks.
|
|
14
14
|
// • IGNORED incoming files — a pre-existing .gitignore rule that would swallow what
|
|
15
|
-
// adopt is about to commit (task
|
|
15
|
+
// adopt is about to commit (task 1003881). This is the check that was missing when
|
|
16
16
|
// Cloud Bongos was adopted onto a repo whose .gitignore already carried `.claude/`:
|
|
17
17
|
// the skills, hooks and settings.json were materialized onto disk and then dropped
|
|
18
18
|
// by `git add`, so the adopt commit landed 8 files with no methodology surface at
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// scripts/gds/device-flow-help.js — the one explanation of a Device-Flow refusal, shared
|
|
4
|
-
// by `bongos setup` and `bongos login` (task
|
|
4
|
+
// by `bongos setup` and `bongos login` (task 1003881).
|
|
5
5
|
//
|
|
6
6
|
// WHY IT EXISTS AT ALL. GitHub creates an App from a manifest with Device Flow OFF and
|
|
7
7
|
// exposes no API to switch it on — only a checkbox in the App's settings. So on every
|
package/scripts/gds/init.js
CHANGED
|
@@ -497,7 +497,8 @@ async function interview({ adopt = false } = {}) {
|
|
|
497
497
|
// ---------------------------------------------------------------------------
|
|
498
498
|
|
|
499
499
|
// PURE (exported for tests) — the root START-HERE guide (task 1002404, goal
|
|
500
|
-
// 1000052
|
|
500
|
+
// 1000052; task 1003881 made it LAYER onto an existing README instead of
|
|
501
|
+
// skipping one — see layerReadme below). A freshly-CLONED instance must tell a human what to run FIRST: the
|
|
501
502
|
// `bongos` CLI ships INSIDE the not-yet-installed @bongos/core package, so
|
|
502
503
|
// `npm install` has to come before any `bongos` command (including `bongos dev`).
|
|
503
504
|
// `bongos init` runs npm install during scaffolding, but a later `git clone` of
|
|
@@ -637,8 +638,9 @@ function writeConfigs(spec, { dir, force, dryRun, coreDep, log = console.log })
|
|
|
637
638
|
log(` ✓ wrote ${path.relative(dir, modulesPath)}`);
|
|
638
639
|
log(` ✓ wrote ${path.relative(dir, hierarchyPath)}`);
|
|
639
640
|
log(` ✓ wrote ${path.relative(dir, pkgPath)} (pins ${CORE_PKG} → ${pinNote})`);
|
|
640
|
-
// README START-HERE (task 1002404): mint it when
|
|
641
|
-
// section. Never a blind overwrite — an owner's own
|
|
641
|
+
// README START-HERE (task 1002404, layering added by task 1003881): mint it when
|
|
642
|
+
// absent, else APPEND the marked section. Never a blind overwrite — an owner's own
|
|
643
|
+
// README content always survives, which is why the guide stopped being skipped.
|
|
642
644
|
layerReadme(branding, { dir, dryRun: false, log });
|
|
643
645
|
return { brandingPath, modulesPath, hierarchyPath, pkgPath, readmePath, wrote: true };
|
|
644
646
|
}
|
package/scripts/gds/login.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
const readline = require('node:readline');
|
|
24
24
|
const { spawnSync } = require('node:child_process');
|
|
25
25
|
const { saveSession, loadStoredSession, listStoredSessions } = require('./cli-lib');
|
|
26
|
-
// The device-flow refusal detector + its plain-language cure (task
|
|
26
|
+
// The device-flow refusal detector + its plain-language cure (task 1003881). A leaf
|
|
27
27
|
// module, NOT setup.js: setup requires this file for runDeviceFlow, so requiring it
|
|
28
28
|
// back would close a require cycle. Both callers depend on the leaf instead.
|
|
29
29
|
const { isDeviceFlowDisabled, deviceFlowFallbackLines } = require('./device-flow-help');
|
package/scripts/gds/setup.js
CHANGED
|
@@ -274,7 +274,7 @@ async function promptOnboardingConsent() {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
// ── W1: the manifest-born App has Device Flow OFF (task 1003051) ─────────────
|
|
277
|
-
// The detector + the guidance moved to ./device-flow-help (task
|
|
277
|
+
// The detector + the guidance moved to ./device-flow-help (task 1003881) so
|
|
278
278
|
// `bongos login` can use the same words without closing a require cycle back
|
|
279
279
|
// through this file (setup requires login for runDeviceFlow). Re-exported here
|
|
280
280
|
// because both are part of setup.js's tested public surface.
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.953'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -101,7 +101,7 @@ t('preflightAdopt: a clean repo → no blocking; every category reports', () =>
|
|
|
101
101
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
// ---- ignored-incoming: the check that was missing (task
|
|
104
|
+
// ---- ignored-incoming: the check that was missing (task 1003881) ----------
|
|
105
105
|
//
|
|
106
106
|
// The failure it exists to stop: adopt onto a repo whose .gitignore already carries
|
|
107
107
|
// `.claude/`. The skills/hooks/settings are written to disk, `git add` drops them, the
|
package/tests/bongos_login.mjs
CHANGED
|
@@ -45,7 +45,7 @@ test('pollDecision maps the /access-requests/status body to a loop action', () =
|
|
|
45
45
|
assert.equal(pollDecision({}), 'wait');
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
// ---- the Device-Flow dead end (task
|
|
48
|
+
// ---- the Device-Flow dead end (task 1003881) -------------------------------
|
|
49
49
|
//
|
|
50
50
|
// Device Flow is OFF on every GitHub App created from a manifest and GitHub offers no
|
|
51
51
|
// API to switch it on, so terminal sign-in is broken from birth on a fresh instance —
|
|
@@ -202,7 +202,7 @@ test('a real request through the assembled router returns the verdict, not a 500
|
|
|
202
202
|
|
|
203
203
|
// ── the banner ────────────────────────────────────────────────────────────────
|
|
204
204
|
|
|
205
|
-
function boot({ permissions, update, status = 200, dismissed = null, provisioning = true } = {}) {
|
|
205
|
+
function boot({ permissions, update, status = 200, dismissed = null, provisioning = true, pathname = '/settings' } = {}) {
|
|
206
206
|
const els = new Map();
|
|
207
207
|
const inserted = [];
|
|
208
208
|
const stored = {};
|
|
@@ -227,7 +227,7 @@ function boot({ permissions, update, status = 200, dismissed = null, provisionin
|
|
|
227
227
|
addEventListener() {}, dispatchEvent() {},
|
|
228
228
|
get cookie() { return ''; }, set cookie(_v) {},
|
|
229
229
|
};
|
|
230
|
-
const location = { hostname: 'h', origin: 'https://h', pathname
|
|
230
|
+
const location = { hostname: 'h', origin: 'https://h', pathname, hash: '', assign() {}, replace() {} };
|
|
231
231
|
const windowObj = {
|
|
232
232
|
__BRANDING__: {}, __MODULES__: { enabled: { provisioning } },
|
|
233
233
|
BongosClient: {
|
|
@@ -334,3 +334,31 @@ test('the banner renders from the projection /me ACTUALLY sends, not one written
|
|
|
334
334
|
'gates on is outside NAV_PERMISSION_ATOMS and the feature is dead for everyone (task 1004118)',
|
|
335
335
|
);
|
|
336
336
|
});
|
|
337
|
+
|
|
338
|
+
// ── the banner must not offer the page you are on (task 1004145) ──────────────
|
|
339
|
+
// The owner pressed this on /deploy and reported "I clicked update, but I didn't see
|
|
340
|
+
// anything happen". Nothing had: it is an <a href="/deploy"> styled as a button and
|
|
341
|
+
// labelled with the verb, so on /deploy it navigates to the current page and does
|
|
342
|
+
// nothing at all. No request was ever made — verified against the live box, which had
|
|
343
|
+
// no core-upgrade POST in three hours and no queued intent from that press.
|
|
344
|
+
|
|
345
|
+
test('on /deploy the banner points at the control instead of linking to /deploy', async () => {
|
|
346
|
+
const b = boot({ pathname: '/deploy', permissions: { 'core.pin.move': true }, update: BEHIND });
|
|
347
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
348
|
+
const html = b.banner().innerHTML;
|
|
349
|
+
assert.doesNotMatch(html, /href="\/deploy"/, 'no link to the page already open');
|
|
350
|
+
assert.match(html, /control below/i, 'it says where the real control is');
|
|
351
|
+
assert.match(html, /1\.19\.871/, 'and still announces the release');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('everywhere else the banner still offers the link', async () => {
|
|
355
|
+
const b = boot({ pathname: '/settings', permissions: { 'core.pin.move': true }, update: BEHIND });
|
|
356
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
357
|
+
assert.match(b.banner().innerHTML, /href="\/deploy"/, 'the action is real from any other page');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('a trailing slash is still /deploy', async () => {
|
|
361
|
+
const b = boot({ pathname: '/deploy/', permissions: { 'core.pin.move': true }, update: BEHIND });
|
|
362
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
363
|
+
assert.doesNotMatch(b.banner().innerHTML, /href="\/deploy"/);
|
|
364
|
+
});
|
|
@@ -697,5 +697,55 @@ await t('every strict body schema in this module declares each field its handler
|
|
|
697
697
|
'under strict, a field the schema does not name is a 400 BEFORE the handler runs, so reading it is unreachable code and a broken endpoint');
|
|
698
698
|
});
|
|
699
699
|
|
|
700
|
+
|
|
701
|
+
console.log('\nthe last move, whatever became of it (task 1004145):');
|
|
702
|
+
|
|
703
|
+
const SNAP = { served_version: '1.19.900', available: ['1.19.900', '1.19.949'], recommended: '1.19.949', observed_at: new Date().toISOString() };
|
|
704
|
+
|
|
705
|
+
await t('a FAILED move is visible — the payload is no longer identical to before it', () => {
|
|
706
|
+
// open_intent is `state IN ('pending','running')` by the index's own definition, so an
|
|
707
|
+
// errored intent is not open. Before last_move, the payload after a rolled-back
|
|
708
|
+
// upgrade matched the payload before it exactly, and the owner's only signal was a
|
|
709
|
+
// number that never changed — indistinguishable from never having pressed.
|
|
710
|
+
const failed = { action: 'core-upgrade', state: 'error', target_version: '1.19.949', resolved_at: new Date().toISOString(), last_error: 'working tree not clean — commit/stash first, or pass --force.' };
|
|
711
|
+
const p = CU.previewFromSnapshot(ACTIVE, SNAP, null, failed);
|
|
712
|
+
assert.equal(p.open_intent, null, 'it is not an OPEN intent');
|
|
713
|
+
assert.equal(p.last_move.state, 'error');
|
|
714
|
+
assert.equal(p.last_move.target_version, '1.19.949');
|
|
715
|
+
assert.match(p.last_move.error, /working tree not clean/, 'and the reason travels');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
await t('a SUCCESSFUL move is attributable, not merely finished', () => {
|
|
719
|
+
const done = { action: 'core-upgrade', state: 'done', target_version: '1.19.949', resolved_at: new Date().toISOString(), last_error: null };
|
|
720
|
+
const p = CU.previewFromSnapshot({ ...ACTIVE }, { ...SNAP, served_version: '1.19.949', recommended: null }, null, done);
|
|
721
|
+
assert.equal(p.last_move.state, 'done');
|
|
722
|
+
assert.equal(p.last_move.target_version, p.served_version, 'the move names the version the box now serves');
|
|
723
|
+
assert.equal(p.last_move.error, null);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
await t('a long refusal is truncated — it reaches a DOM', () => {
|
|
727
|
+
// The dirty-tree refusal carries a whole `git status`; test4's ran to dozens of lines.
|
|
728
|
+
const huge = { action: 'core-upgrade', state: 'error', target_version: '1.19.949', resolved_at: new Date().toISOString(), last_error: 'x'.repeat(5000) };
|
|
729
|
+
const p = CU.previewFromSnapshot(ACTIVE, SNAP, null, huge);
|
|
730
|
+
assert.ok(p.last_move.error.length <= 600, `bounded — got ${p.last_move.error.length}`);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
await t('no move ever made is null, not an invented success', () => {
|
|
734
|
+
const p = CU.previewFromSnapshot(ACTIVE, SNAP, null, null);
|
|
735
|
+
assert.equal(p.last_move, null);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
await t('lastCoreMove reads MOVES only — a preview refusal is not a failed deploy', async () => {
|
|
739
|
+
// Folding core-upgrade-preflight in here would let a dry run's refusal render as
|
|
740
|
+
// "the move did not land". The preview reports itself through `preflight`.
|
|
741
|
+
const seen = [];
|
|
742
|
+
const pool = { query: async (sql, params) => { seen.push({ sql, params }); return { rows: [] }; } };
|
|
743
|
+
await CU.lastCoreMove(pool, 41);
|
|
744
|
+
assert.match(seen[0].sql, /action = 'core-upgrade'/, 'scoped to the move action');
|
|
745
|
+
assert.doesNotMatch(seen[0].sql, /preflight/, 'and never widened to the preview');
|
|
746
|
+
assert.doesNotMatch(seen[0].sql, /state IN/, 'and not filtered to open states — the point is the terminal ones');
|
|
747
|
+
assert.deepEqual(seen[0].params, [41]);
|
|
748
|
+
});
|
|
749
|
+
|
|
700
750
|
console.log(`\ncore_upgrade_door: ${passed} passed, ${failed} failed`);
|
|
701
751
|
if (failed) process.exit(1);
|
|
@@ -32,10 +32,15 @@ const FLEET = [
|
|
|
32
32
|
{ id: '35', slug: 'unreadable-one', status: 'active', hosting_shape: 'standalone' },
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
-
function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200 } = {}) {
|
|
35
|
+
function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200, instanceOver = null, moveTries = 4, postStatus = 200 } = {}) {
|
|
36
36
|
const els = new Map();
|
|
37
37
|
const posts = [];
|
|
38
38
|
const gets = [];
|
|
39
|
+
// How many times each box has been re-read. A move POLLS, so a test of the move has to
|
|
40
|
+
// be able to change what the server says between reads — otherwise the only reachable
|
|
41
|
+
// assertion is "it asked", never "it noticed".
|
|
42
|
+
const reads = new Map();
|
|
43
|
+
const toasts = [];
|
|
39
44
|
const makeEl = () => ({
|
|
40
45
|
_on: {}, _html: '', hidden: false, textContent: '', disabled: false,
|
|
41
46
|
addEventListener(ev, fn) { this._on[ev] = fn; },
|
|
@@ -48,7 +53,13 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
|
|
|
48
53
|
const request = async (method, url, opts) => {
|
|
49
54
|
const p = url.replace('/api/bongos', '');
|
|
50
55
|
if (method === 'GET') gets.push(p);
|
|
51
|
-
if (method === 'POST') {
|
|
56
|
+
if (method === 'POST') {
|
|
57
|
+
posts.push({ path: p, body: opts && opts.body });
|
|
58
|
+
if (postStatus !== 200) {
|
|
59
|
+
return { ok: false, status: postStatus, data: { error: { code: 'intent_conflict', message: 'A core-upgrade run is currently running for this project.' } } };
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, status: 200, data: { message: 'Queued.' } };
|
|
62
|
+
}
|
|
52
63
|
if (p === '/provisioning/control-plane') {
|
|
53
64
|
if (controlPlaneStatus !== 200) {
|
|
54
65
|
return { ok: false, status: controlPlaneStatus, data: { error: { code: 'control_plane_not_enrolled', message: 'This platform has no deploy door yet. Run enroll-control-plane.js once.' } } };
|
|
@@ -66,13 +77,16 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
|
|
|
66
77
|
if (!inst || inst.slug === 'unreadable-one') {
|
|
67
78
|
return { ok: false, status: 500, data: { error: { code: 'core_upgrade_preview_failed', message: 'internal error' } } };
|
|
68
79
|
}
|
|
69
|
-
|
|
80
|
+
const n = (reads.get(inst.id) || 0) + 1;
|
|
81
|
+
reads.set(inst.id, n);
|
|
82
|
+
const over = instanceOver ? instanceOver(inst, n) : {};
|
|
83
|
+
return { ok: true, status: 200, data: { preview: preview(inst.id, inst.slug, { hosting_shape: inst.hosting_shape, ...over }) } };
|
|
70
84
|
}
|
|
71
85
|
return { ok: false, status: 404, data: { error: { code: 'not_found' } } };
|
|
72
86
|
};
|
|
73
87
|
const windowObj = {
|
|
74
88
|
BongosClient: { createClient: () => ({ request }) },
|
|
75
|
-
OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])), toast() {} },
|
|
89
|
+
OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])), toast(m) { toasts.push(String(m)); } },
|
|
76
90
|
location: { assign() {} },
|
|
77
91
|
};
|
|
78
92
|
// The page's poll interval, driven rather than slept through. Injecting the REAL
|
|
@@ -81,13 +95,18 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
|
|
|
81
95
|
// flaky against a torn-down sandbox.
|
|
82
96
|
windowObj.__deployPollMs = 0;
|
|
83
97
|
windowObj.__deployPollTries = 3;
|
|
98
|
+
windowObj.__deployMoveTries = moveTries;
|
|
84
99
|
const sandbox = { window: windowObj, document: documentObj, console, setTimeout, clearTimeout, queueMicrotask, Date, Set, Map, Promise };
|
|
85
100
|
sandbox.globalThis = sandbox;
|
|
86
101
|
vm.createContext(sandbox);
|
|
87
102
|
vm.runInContext(SRC, sandbox, { filename: 'deploy.js' });
|
|
88
103
|
documentObj._ready();
|
|
89
104
|
const settle = async () => { for (let i = 0; i < 40; i++) await new Promise((r) => setTimeout(r, 0)); };
|
|
90
|
-
|
|
105
|
+
// A SHORT advance, for observing a state that exists only while the poll is running.
|
|
106
|
+
// `settle` drains the whole loop (POLL_MS is 0 here), so an assertion about the
|
|
107
|
+
// in-flight row has to look before the loop finishes, not after it.
|
|
108
|
+
const tick = async (n = 3) => { for (let i = 0; i < n; i++) await new Promise((r) => setTimeout(r, 0)); };
|
|
109
|
+
return { els, posts, gets, reads, toasts, settle, tick, panel: () => els.get('dp-panel').innerHTML, sub: () => els.get('dp-sub').textContent };
|
|
91
110
|
}
|
|
92
111
|
|
|
93
112
|
test('the platform AND every live hosted project get a row', async () => {
|
|
@@ -189,8 +208,12 @@ test('a move re-reads ONLY the box it moved, not every project again', async ()
|
|
|
189
208
|
b.els.get('dp-confirm-32')._on.click();
|
|
190
209
|
await b.settle();
|
|
191
210
|
const previewReads = b.gets.filter((g) => g.endsWith('/core-upgrade'));
|
|
192
|
-
|
|
193
|
-
|
|
211
|
+
// Since task 1004145 a move POLLS, so the count is no longer one — and the count was
|
|
212
|
+
// only ever a proxy for the thing that matters. Asserted directly instead: every
|
|
213
|
+
// re-read is the moved box, and no other box is touched.
|
|
214
|
+
assert.ok(previewReads.length > 0, 'the moved box is re-read');
|
|
215
|
+
assert.deepEqual([...new Set(previewReads)], ['/provisioning/instances/32/core-upgrade'],
|
|
216
|
+
'and it is the ONLY box re-read — no fan-out across the fleet');
|
|
194
217
|
assert.ok(!b.gets.includes('/provisioning/fleet'), 'the fleet is not re-listed');
|
|
195
218
|
});
|
|
196
219
|
|
|
@@ -290,3 +313,104 @@ test('a verdict already in hand is NOT re-requested', async () => {
|
|
|
290
313
|
assert.equal(b.posts.filter((x) => x.body && x.body.dry_run).length, 0, 'no check requested');
|
|
291
314
|
assert.match(b.panel(), /nothing blocks this move/i, 'the held verdict is shown');
|
|
292
315
|
});
|
|
316
|
+
|
|
317
|
+
// ── the press has to show something (task 1004145) ────────────────────────────
|
|
318
|
+
// The owner pressed Upgrade and reported "I didn't see anything happen". Two reasons:
|
|
319
|
+
// the banner's Upgrade is a link to /deploy (fixed in shell.js), and move() refreshed
|
|
320
|
+
// once — before the runner's tick — then fell silent for the whole upgrade.
|
|
321
|
+
|
|
322
|
+
test('confirming a move says so IMMEDIATELY, naming the version it is taking', async () => {
|
|
323
|
+
const b = boot();
|
|
324
|
+
await b.settle();
|
|
325
|
+
b.els.get('dp-preview-32')._on.click();
|
|
326
|
+
b.els.get('dp-confirm-32')._on.click();
|
|
327
|
+
await b.tick();
|
|
328
|
+
const html = b.panel();
|
|
329
|
+
assert.match(html, /Moving to core/i, 'the row reports the move, rather than snapping back');
|
|
330
|
+
assert.match(html, /1\.19\.871/, 'and names the target');
|
|
331
|
+
assert.match(html, /restarts as part of this/i, 'and warns what it costs');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('the in-flight state survives the poll — it does not close under the reader', async () => {
|
|
335
|
+
// refreshOne() returns a row to step one, which is right after a move and wrong
|
|
336
|
+
// DURING one; without keepStep the poll wipes its own message on the first interval.
|
|
337
|
+
// Observed after a poll has actually run, but before the bound is exhausted.
|
|
338
|
+
// A long bound so the loop cannot exhaust mid-assertion: the state under test exists
|
|
339
|
+
// only WHILE it polls.
|
|
340
|
+
const b = boot({ moveTries: 500, instanceOver: () => ({ open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }) });
|
|
341
|
+
await b.settle();
|
|
342
|
+
b.els.get('dp-preview-32')._on.click();
|
|
343
|
+
b.els.get('dp-confirm-32')._on.click();
|
|
344
|
+
await b.tick(8);
|
|
345
|
+
assert.ok(b.reads.get('32') >= 1, 'at least one poll has landed');
|
|
346
|
+
assert.match(b.panel(), /Moving to core/i, 'still reporting after a refresh');
|
|
347
|
+
assert.doesNotMatch(b.panel(), /id="dp-preview-32"/, 'and has not reverted to step one');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('when the box lands on the new core, it CONFIRMS it', async () => {
|
|
351
|
+
// The whole ask: "confirmation when it updates". A changed number is not a
|
|
352
|
+
// confirmation — nothing tied it to the press.
|
|
353
|
+
const b = boot({
|
|
354
|
+
instanceOver: (inst, n) => (n >= 2
|
|
355
|
+
? { served_version: '1.19.871', behind: false, open_intent: null, last_move: { target_version: '1.19.871', state: 'done', at: new Date().toISOString(), error: null } }
|
|
356
|
+
: { open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }),
|
|
357
|
+
});
|
|
358
|
+
await b.settle();
|
|
359
|
+
b.els.get('dp-preview-32')._on.click();
|
|
360
|
+
b.els.get('dp-confirm-32')._on.click();
|
|
361
|
+
await b.settle();
|
|
362
|
+
const html = b.panel();
|
|
363
|
+
assert.match(html, /Now running core 1\.19\.871/i, 'the move is confirmed, not merely finished');
|
|
364
|
+
assert.doesNotMatch(html, /Moving to core/i, 'and the in-flight state is gone');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test('a FAILED move is reported, with the reason, instead of looking untouched', async () => {
|
|
368
|
+
// An errored intent is not an OPEN intent, so before last_move existed the payload
|
|
369
|
+
// after a rolled-back upgrade was identical to the payload before it.
|
|
370
|
+
const b = boot({
|
|
371
|
+
instanceOver: (inst, n) => (n >= 2
|
|
372
|
+
? { open_intent: null, last_move: { target_version: '1.19.871', state: 'error', at: new Date().toISOString(), error: 'working tree not clean — commit/stash first, or pass --force.' } }
|
|
373
|
+
: { open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }),
|
|
374
|
+
});
|
|
375
|
+
await b.settle();
|
|
376
|
+
b.els.get('dp-preview-32')._on.click();
|
|
377
|
+
b.els.get('dp-confirm-32')._on.click();
|
|
378
|
+
await b.settle();
|
|
379
|
+
const html = b.panel();
|
|
380
|
+
assert.match(html, /did not land/i, 'the failure is stated');
|
|
381
|
+
assert.match(html, /working tree not clean/, 'and carries the runner’s own reason');
|
|
382
|
+
assert.match(html, /put back on 1\.19\.800/i, 'and says the project is still running');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('a poll that runs out says it is still working, not that it failed', async () => {
|
|
386
|
+
// MOVE_TRIES is a bound on this page's patience, not on the runner's work.
|
|
387
|
+
const b = boot({ instanceOver: () => ({ open_intent: { action: 'core-upgrade', state: 'running', target_version: '1.19.871' } }) });
|
|
388
|
+
await b.settle();
|
|
389
|
+
b.els.get('dp-preview-32')._on.click();
|
|
390
|
+
b.els.get('dp-confirm-32')._on.click();
|
|
391
|
+
for (let i = 0; i < 6; i++) await b.settle();
|
|
392
|
+
assert.ok(b.toasts.some((t) => /Still working on core 1\.19\.871/i.test(t)),
|
|
393
|
+
`it says where things stand — got ${JSON.stringify(b.toasts)}`);
|
|
394
|
+
assert.doesNotMatch(b.panel(), /did not land/i, 'and never claims a failure it did not see');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('a queue that is REFUSED leaves no in-flight state to wait on', async () => {
|
|
398
|
+
// THIS TEST USED TO PASS WITHOUT CLICKING ANYTHING. Its setup read
|
|
399
|
+
// `b.els.get('dp-confirm-32')._on = el._on` — a self-assignment no-op — and set
|
|
400
|
+
// `b.postFails`, a property the harness never read. So it asserted "no in-flight
|
|
401
|
+
// state" on a row where no move had been attempted, which is true of every row on the
|
|
402
|
+
// page. The grader caught it. It now refuses the POST for real and presses the button.
|
|
403
|
+
const b = boot({ postStatus: 409 });
|
|
404
|
+
await b.settle();
|
|
405
|
+
b.els.get('dp-preview-32')._on.click();
|
|
406
|
+
b.els.get('dp-confirm-32')._on.click();
|
|
407
|
+
await b.settle();
|
|
408
|
+
// Counted on the MOVE specifically: arming the row also POSTs a dry_run pre-flight,
|
|
409
|
+
// so a bare posts.length would have been satisfied by the preview alone — the same
|
|
410
|
+
// class of mistake this test is being fixed for.
|
|
411
|
+
const moves = b.posts.filter((x) => !(x.body && x.body.dry_run));
|
|
412
|
+
assert.equal(moves.length, 1, `the press really happened — posts: ${JSON.stringify(b.posts)}`);
|
|
413
|
+
assert.doesNotMatch(b.panel(), /Moving to core/i, 'nothing was queued, so nothing is watched');
|
|
414
|
+
assert.ok(b.toasts.some((t) => /currently running/i.test(t)), 'and the refusal is reported');
|
|
415
|
+
assert.match(b.panel(), /id="dp-confirm-32"/, 'the button comes back so it can be retried');
|
|
416
|
+
});
|
|
@@ -170,7 +170,7 @@ await t('a DB error returns {error}, never throws', async () => {
|
|
|
170
170
|
assert.equal(r.outcome, 'error');
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
-
// ---- the outbound half must not outlive the inbound half (task
|
|
173
|
+
// ---- the outbound half must not outlive the inbound half (task 1003881) ----
|
|
174
174
|
//
|
|
175
175
|
// The 👍 this line invites is applied ONLY by discord-approvals.js, which lives in the
|
|
176
176
|
// `discord` module. onboarding (which posts) is default-ON; discord is default-OFF; and
|
package/tests/module_api.mjs
CHANGED
|
@@ -48,7 +48,7 @@ const PUBLISHED_SURFACE = [
|
|
|
48
48
|
'artistGate', // added by task 1003575 (ADR 0241): the artist gate — off | advisory | strict, plus the pure verdict helpers over it. Two modules ask it two different questions ("file this review?" and "does an open one hold the deploy?") and both must resolve the knob through this one reader, never branding().project
|
|
49
49
|
'readApplicantProfileFromHub', // added by task 1002972 (privacy spec D7): the reviewer queue's LIVE per-render read of one applicant's hub profile view. NARROW by design — the underlying call is authenticated with this instance's hub client secret, and loadIdpConfig stays OFF the doorway so no module ever holds the credential that speaks for the whole project
|
|
50
50
|
'enabledDisciplines', // BV1.R81: instance offered-disciplines (1.9.0; onboarding restock backstop)
|
|
51
|
-
'isModuleEnabled', // task
|
|
51
|
+
'isModuleEnabled', // task 1003881: is ANOTHER module present? onboarding's Discord approve/decline control is only actionable when the `discord` module's reaction handler is loaded, so the posting half has to be able to ask
|
|
52
52
|
'registerProvider', 'hasProvider', 'resolve', 'resolveOptional', 'listPorts', 'verifyPortsSatisfied',
|
|
53
53
|
'on', 'emit', 'emitAsync', 'listEvents', 'seamSnapshot',
|
|
54
54
|
'contribute', 'contributions', 'collectContributions', 'listContributionPoints',
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = '02de48ca2ae07995';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|