@bongos/core 1.19.877 → 1.19.879
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 +54 -84
- package/docs/copy-inventory.md +195 -283
- package/docs/copy-registry.json +254 -1141
- package/docs/file-map.md +2 -2
- package/docs/module-api-changelog.md +5 -1
- package/docs/recipes/autobongos-windows-host.md +26 -1
- package/modules/autonomy/digest.js +344 -0
- package/modules/hall-ui/module.json +1 -1
- package/modules/hall-ui/public/builders.js +1 -13
- package/modules/hall-ui/public/dom-utils.js +4 -4
- package/modules/hall-ui/public/hall-render.js +2 -18
- package/modules/hall-ui/public/index.html +0 -3
- package/modules/hall-ui/public/panel.css +8 -9
- package/modules/hall-ui/public/settings.css +0 -5
- package/modules/hall-ui/public/settings.html +0 -29
- package/modules/hall-ui/public/settings.js +1 -26
- package/modules/hall-ui/public/shell.js +0 -1
- package/modules/hall-ui/public/style.css +0 -44
- package/modules/hall-ui/public/watch.html +0 -7
- package/modules/hall-ui/public/watch.js +1 -37
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-digest.js +123 -0
- package/scripts/gds/autobongos-run.js +7 -0
- package/scripts/gds/autobongos-service.ps1 +7 -1
- package/scripts/gds/provision-core-upgrade.js +23 -3
- package/src/bongos/serve-internal.js +11 -11
- package/src/module-api.js +1 -1
- package/tests/autonomy_digest.mjs +302 -0
- package/tests/core_upgrade_runner.mjs +43 -1
- package/tests/government_page_gates.mjs +1 -1
- package/tests/hall_landing_boot.mjs +1 -1
- package/tests/hall_nav.mjs +1 -1
- package/tests/hall_oversight_world.mjs +5 -5
- package/tests/hall_page_gate_map.mjs +11 -2
- package/tests/hall_settings_world.mjs +21 -18
- package/modules/hall-ui/public/box-panel.js +0 -456
- package/modules/hall-ui/public/harbor.css +0 -9
- package/modules/hall-ui/public/harbor.html +0 -69
- package/modules/hall-ui/public/harbor.js +0 -367
- package/modules/hall-ui/public/harbor.states.json +0 -77
- package/modules/hall-ui/public/pair.css +0 -23
- package/modules/hall-ui/public/pair.html +0 -58
- package/modules/hall-ui/public/pair.js +0 -132
- package/modules/hall-ui/public/pair.states.json +0 -14
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// tests/autonomy_digest.mjs — the night, readable afterwards (task 1003906).
|
|
2
|
+
//
|
|
3
|
+
// The done-when is "a night produces one digest answering all five questions
|
|
4
|
+
// without anyone opening a log". So the spine of this file is one test that
|
|
5
|
+
// feeds a realistic night through and asserts all five answers are present and
|
|
6
|
+
// correct — because five separately-passing sections can still add up to a page
|
|
7
|
+
// that does not answer the question.
|
|
8
|
+
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const { buildDigest, renderDigest, windowsFromGauge, humanDuration } = require('../modules/autonomy/digest.js');
|
|
15
|
+
const cli = require('../scripts/gds/autobongos-digest.js');
|
|
16
|
+
|
|
17
|
+
const T0 = Date.parse('2026-09-19T02:00:00.000Z');
|
|
18
|
+
const at = (m) => new Date(T0 + m * 60000).toISOString();
|
|
19
|
+
const NOW_S = Math.floor((T0 + 8 * 3600e3) / 1000);
|
|
20
|
+
|
|
21
|
+
const night = () => [
|
|
22
|
+
{ at: at(0), event: 'boot_reconcile', ok: true, leftover: 0 },
|
|
23
|
+
{ at: at(1), event: 'worked', task_id: 111, title: 'A shipped thing', goal_id: 1000119,
|
|
24
|
+
duration_s: 2820, cost_usd: 3.41, verified: true, ledger_shape: 'shipped',
|
|
25
|
+
undetermined_decisions: ['Cached for 60s rather than 5m'] },
|
|
26
|
+
{ at: at(50), event: 'worked', task_id: 222, title: 'A stranded thing', goal_id: 1000119,
|
|
27
|
+
duration_s: 5400, cost_usd: 6.8, verified: false, ledger_shape: 'unlanded',
|
|
28
|
+
reason: 'the grade passed and the land never finished', blocker_id: '1000161',
|
|
29
|
+
undetermined_decisions: ['Sorted inactive last', 'Kept the id tie-break', 'Chose ascending'] },
|
|
30
|
+
{ at: at(120), event: 'fenced', code: 'grader_unavailable', reason: 'the grader is bypassed' },
|
|
31
|
+
{ at: at(121), event: 'fenced', code: 'grader_unavailable', reason: 'the grader is bypassed' },
|
|
32
|
+
{ at: at(122), event: 'cadence', mode: 'probing', class: 'transient', consecutive_failures: 3, wait_s: 60 },
|
|
33
|
+
{ at: at(200), event: 'nothing_claimable', goals: [1000119],
|
|
34
|
+
skipped: [{ id: 333, goal_id: 1000119, reasons: ['needs_migration'] }] },
|
|
35
|
+
{ at: at(201), event: 'hold', reason: 'seven-day pace: 78% used' },
|
|
36
|
+
];
|
|
37
|
+
const gauge = {
|
|
38
|
+
five_hour: { used_percentage: 41, resets_at: NOW_S + 3600 },
|
|
39
|
+
seven_day: { used_percentage: 78, resets_at: NOW_S + 240000 },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ── the done-when, in one test ─────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
test('one night produces one page answering all five questions', () => {
|
|
45
|
+
const text = renderDigest(buildDigest(night(), { gauge, nowEpochS: NOW_S }));
|
|
46
|
+
|
|
47
|
+
// 1. what shipped — and ONLY what the ledger agreed shipped.
|
|
48
|
+
assert.match(text, /SHIPPED — 1 task/);
|
|
49
|
+
assert.match(text, /111\s+A shipped thing/);
|
|
50
|
+
assert.doesNotMatch(text.split('ON HOLD')[0], /222/, 'a task that ran but did not ship is not "shipped"');
|
|
51
|
+
|
|
52
|
+
// 2. what is on hold, and why.
|
|
53
|
+
assert.match(text, /ON HOLD/);
|
|
54
|
+
assert.match(text, /222\s+ran but did not ship — unlanded/);
|
|
55
|
+
assert.match(text, /blocker 1000161/);
|
|
56
|
+
assert.match(text, /fence: grader_unavailable ×2/);
|
|
57
|
+
assert.match(text, /every one halted/);
|
|
58
|
+
assert.match(text, /capacity: held 1 time/);
|
|
59
|
+
|
|
60
|
+
// 3. what each worker decided that its spec did not determine.
|
|
61
|
+
assert.match(text, /DECIDED FOR YOU — 4 decisions across 2 tasks/);
|
|
62
|
+
assert.match(text, /Cached for 60s rather than 5m/);
|
|
63
|
+
assert.match(text, /sentence the task description should have contained/);
|
|
64
|
+
assert.match(text, /Thinnest spec: 222/);
|
|
65
|
+
|
|
66
|
+
// 4. what it cost.
|
|
67
|
+
assert.match(text, /COST/);
|
|
68
|
+
assert.match(text, /\$10\.21 across 2 worker runs/);
|
|
69
|
+
|
|
70
|
+
// 5. where both limit windows stand.
|
|
71
|
+
assert.match(text, /Five-hour window\s+41% used, resets in 60m/);
|
|
72
|
+
assert.match(text, /Seven-day window\s+78% used/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('every section prints even when the night was empty', () => {
|
|
76
|
+
// A digest whose quiet sections vanish teaches its reader that absence means
|
|
77
|
+
// "fine" — and then the morning a section is missing because the DATA is
|
|
78
|
+
// missing reads exactly the same.
|
|
79
|
+
const text = renderDigest(buildDigest([], { gauge, nowEpochS: NOW_S }));
|
|
80
|
+
for (const heading of ['SHIPPED', 'ON HOLD', 'DECIDED FOR YOU', 'COST', 'LIMITS', 'RUNNER']) {
|
|
81
|
+
assert.ok(text.includes(heading), `${heading} must print on an empty night`);
|
|
82
|
+
}
|
|
83
|
+
assert.match(text, /nothing shipped/);
|
|
84
|
+
assert.match(text, /nothing is waiting on you/);
|
|
85
|
+
assert.match(text, /no worker had to fill a gap/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── the answers, individually ──────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
test('shipped is the LEDGER’s answer, never the worker’s', () => {
|
|
91
|
+
const d = buildDigest([
|
|
92
|
+
{ at: at(0), event: 'worked', task_id: 1, verified: true, ledger_shape: 'shipped' },
|
|
93
|
+
{ at: at(1), event: 'worked', task_id: 2, verified: false, ledger_shape: 'still_held' },
|
|
94
|
+
{ at: at(2), event: 'worked', task_id: 3, outcome: 'claims_shipped', ledger_shape: 'shipped_no_artifact', verified: false },
|
|
95
|
+
], { nowEpochS: NOW_S });
|
|
96
|
+
assert.deepEqual(d.shipped.map((s) => s.taskId), ['1']);
|
|
97
|
+
assert.deepEqual(d.onHold.ranButDidNotShip.map((r) => r.shape), ['still_held', 'shipped_no_artifact']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('an unworkable queue is reported as a queue problem, not as breakage', () => {
|
|
101
|
+
const d = buildDigest([{ at: at(0), event: 'nothing_claimable', skipped: [{ id: 9, reasons: ['needs_migration'] }] }], { nowEpochS: NOW_S });
|
|
102
|
+
assert.equal(d.onHold.unworkable.length, 1);
|
|
103
|
+
const text = renderDigest(d);
|
|
104
|
+
assert.match(text, /this is not a runner failure/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('the decisions list groups by task and names the thinnest spec', () => {
|
|
108
|
+
// The SHAPE is the signal: one task with six decisions is a thin spec, six
|
|
109
|
+
// tasks with one each is a normal night.
|
|
110
|
+
const d = buildDigest([
|
|
111
|
+
{ at: at(0), event: 'worked', task_id: 1, verified: true, undetermined_decisions: ['a'] },
|
|
112
|
+
{ at: at(1), event: 'worked', task_id: 2, verified: true, undetermined_decisions: ['b', 'c', 'd'] },
|
|
113
|
+
{ at: at(2), event: 'worked', task_id: 3, verified: true, undetermined_decisions: [] },
|
|
114
|
+
], { nowEpochS: NOW_S });
|
|
115
|
+
assert.equal(d.decisionCount, 4);
|
|
116
|
+
assert.equal(d.decisions.length, 2, 'a task with no decisions is not listed');
|
|
117
|
+
assert.equal(d.thinnestSpec.taskId, '2');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('a decision from a task that did NOT ship is still surfaced', () => {
|
|
121
|
+
// These are the ones that matter most: the judgement was made, the work is not
|
|
122
|
+
// in, and the call is sitting in a branch nobody has read.
|
|
123
|
+
const text = renderDigest(buildDigest([
|
|
124
|
+
{ at: at(0), event: 'worked', task_id: 7, title: 'T', verified: false, ledger_shape: 'unlanded', undetermined_decisions: ['picked a default'] },
|
|
125
|
+
], { nowEpochS: NOW_S }));
|
|
126
|
+
assert.match(text, /picked a default/);
|
|
127
|
+
assert.match(text, /\[did not ship\]/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('an unreported cost is excluded and counted, never coerced to zero', () => {
|
|
131
|
+
const d = buildDigest([
|
|
132
|
+
{ at: at(0), event: 'worked', task_id: 1, verified: true, cost_usd: 2.5 },
|
|
133
|
+
{ at: at(1), event: 'worked', task_id: 2, verified: true, cost_usd: null },
|
|
134
|
+
{ at: at(2), event: 'worked', task_id: 3, verified: true },
|
|
135
|
+
], { nowEpochS: NOW_S });
|
|
136
|
+
assert.equal(d.cost.totalUsd, 2.5, 'a worker whose cost was not reported must not make the night look cheaper');
|
|
137
|
+
assert.equal(d.cost.reported, 1);
|
|
138
|
+
assert.equal(d.cost.unreported, 2);
|
|
139
|
+
assert.match(renderDigest(d), /2 did not report a cost/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── the limit windows ──────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
test('a window whose reset has PASSED is reported as stale, not as headroom', () => {
|
|
145
|
+
// "41% used, resets in 0s" invites a decision about headroom that no longer
|
|
146
|
+
// exists: the percentage was true of a window that has since rolled.
|
|
147
|
+
const [five] = windowsFromGauge({ five_hour: { used_percentage: 41, resets_at: NOW_S - 60 } }, NOW_S);
|
|
148
|
+
assert.equal(five.known, false);
|
|
149
|
+
assert.equal(five.stale, true);
|
|
150
|
+
assert.match(five.detail, /has since reset/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('a window the gauge never reported says so rather than reading as zero', () => {
|
|
154
|
+
const [five, seven] = windowsFromGauge({ seven_day: { used_percentage: 12, resets_at: NOW_S + 999 } }, NOW_S);
|
|
155
|
+
assert.equal(five.known, false);
|
|
156
|
+
assert.equal(five.detail, 'not reported');
|
|
157
|
+
assert.equal(seven.known, true);
|
|
158
|
+
for (const g of [null, undefined, {}, 'nope']) {
|
|
159
|
+
assert.deepEqual(windowsFromGauge(g, NOW_S).map((w) => w.known), [false, false], JSON.stringify(g));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── the runner footer ──────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
test('a night that ended probing says so — "nothing shipped" has two causes', () => {
|
|
166
|
+
const text = renderDigest(buildDigest([
|
|
167
|
+
{ at: at(0), event: 'cadence', mode: 'full', class: 'quiet' },
|
|
168
|
+
{ at: at(1), event: 'cadence', mode: 'probing', class: 'transient' },
|
|
169
|
+
], { nowEpochS: NOW_S }));
|
|
170
|
+
assert.match(text, /Ended the night PROBING/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('an event this digest does not know about is counted, never dropped', () => {
|
|
174
|
+
// A digest that silently ignores what it does not understand is how a new
|
|
175
|
+
// failure mode stays invisible for a week.
|
|
176
|
+
const d = buildDigest([{ at: at(0), event: 'something_new_entirely' }], { nowEpochS: NOW_S });
|
|
177
|
+
assert.equal(d.runner.unrecognised, 1);
|
|
178
|
+
assert.equal(d.runner.byEvent.something_new_entirely, 1);
|
|
179
|
+
assert.match(renderDigest(d), /a kind this digest does not know about/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('malformed rows never take the night down', () => {
|
|
183
|
+
const d = buildDigest([null, undefined, 42, 'nope', {}, { event: 'worked', verified: true }], { nowEpochS: NOW_S });
|
|
184
|
+
assert.equal(d.shipped.length, 1);
|
|
185
|
+
assert.doesNotThrow(() => renderDigest(d));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ── the CLI half ───────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
test('--since accepts a span and defaults to local midnight', () => {
|
|
191
|
+
const now = new Date('2026-09-19T14:30:00');
|
|
192
|
+
assert.equal(cli.sinceMs('24h', now), now.getTime() - 86400e3);
|
|
193
|
+
assert.equal(cli.sinceMs('90m', now), now.getTime() - 5400e3);
|
|
194
|
+
assert.equal(cli.sinceMs('3d', now), now.getTime() - 3 * 86400e3);
|
|
195
|
+
// Local midnight, because "last night" is a local idea — a UTC cut would split
|
|
196
|
+
// an owner's evening in half depending on where they live.
|
|
197
|
+
const midnight = new Date(now); midnight.setHours(0, 0, 0, 0);
|
|
198
|
+
assert.equal(cli.sinceMs(null, now), midnight.getTime());
|
|
199
|
+
assert.equal(cli.sinceMs('soon', now), null, 'an unparseable span must refuse, not guess');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('a torn last line costs one row, not the night', async () => {
|
|
203
|
+
// A JSONL written by a process that can be killed mid-write has a torn last
|
|
204
|
+
// line more often than never.
|
|
205
|
+
const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path');
|
|
206
|
+
const f = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'ab-digest-')), 'runs.jsonl');
|
|
207
|
+
fs.writeFileSync(f, `${JSON.stringify({ at: at(0), event: 'worked', task_id: 1, verified: true })}\n{"at":"x","event":"wor`);
|
|
208
|
+
const { rows, skipped, missing } = cli.readRows(f);
|
|
209
|
+
assert.equal(missing, false);
|
|
210
|
+
assert.equal(rows.length, 1);
|
|
211
|
+
assert.equal(skipped, 1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('a missing log is reported, not mistaken for a quiet night', async () => {
|
|
215
|
+
const { rows, missing } = cli.readRows('/definitely/not/here/runs.jsonl');
|
|
216
|
+
assert.deepEqual(rows, []);
|
|
217
|
+
assert.equal(missing, true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('humanDuration reads like a person wrote it', () => {
|
|
221
|
+
assert.equal(humanDuration(45), '45s');
|
|
222
|
+
assert.equal(humanDuration(2820), '47m');
|
|
223
|
+
assert.equal(humanDuration(10080), '2.8h');
|
|
224
|
+
assert.equal(humanDuration(null), null);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ── the grader's three findings (task 1003906) ─────────────────────────────────
|
|
228
|
+
|
|
229
|
+
test('the "did not ship" reason is the LEDGER’s, not the worker’s', () => {
|
|
230
|
+
// THE INVERSION THIS GOAL EXISTS TO UNDO. `row.reason` is verdict.reason — the
|
|
231
|
+
// WORKER's account of itself — and quoting it here means the digest reports the
|
|
232
|
+
// thing being checked rather than the check. A worker that stopped dead needing
|
|
233
|
+
// a decision reports success; the ledger says what actually happened.
|
|
234
|
+
const d = buildDigest([{
|
|
235
|
+
at: at(0), event: 'worked', task_id: 9, title: 'T', verified: false, ledger_shape: 'unlanded',
|
|
236
|
+
reason: 'I finished the work and shipped it',
|
|
237
|
+
ledger_reason: 'the grade passed and the land never finished',
|
|
238
|
+
ledger_remedy: '/merge-mode is the documented manual fallback',
|
|
239
|
+
}], { nowEpochS: NOW_S });
|
|
240
|
+
|
|
241
|
+
assert.equal(d.onHold.ranButDidNotShip[0].reason, 'the grade passed and the land never finished');
|
|
242
|
+
assert.equal(d.onHold.ranButDidNotShip[0].workerSaid, 'I finished the work and shipped it');
|
|
243
|
+
|
|
244
|
+
const text = renderDigest(d);
|
|
245
|
+
assert.match(text, /the grade passed and the land never finished/);
|
|
246
|
+
assert.match(text, /next: \/merge-mode/);
|
|
247
|
+
// The disagreement is the single most useful line on the page.
|
|
248
|
+
assert.match(text, /the worker said: I finished the work and shipped it/);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('the worker’s account is printed only when it disagrees', () => {
|
|
252
|
+
// A worker that agreed with the ledger tells the reader nothing twice.
|
|
253
|
+
const same = 'the grade parked this';
|
|
254
|
+
const text = renderDigest(buildDigest([{
|
|
255
|
+
at: at(0), event: 'worked', task_id: 9, verified: false, ledger_shape: 'ungraded', reason: same, ledger_reason: same,
|
|
256
|
+
}], { nowEpochS: NOW_S }));
|
|
257
|
+
assert.equal((text.match(/the grade parked this/g) || []).length, 1);
|
|
258
|
+
assert.doesNotMatch(text, /the worker said:/);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('a row from before ledger_reason existed still shows something', () => {
|
|
262
|
+
const d = buildDigest([{ at: at(0), event: 'worked', task_id: 9, verified: false, ledger_shape: 'unlanded', reason: 'older row' }], { nowEpochS: NOW_S });
|
|
263
|
+
assert.equal(d.onHold.ranButDidNotShip[0].reason, 'older row');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('the digest calls a gauge stale on the SAME rule the runner holds on', () => {
|
|
267
|
+
// A digest that applied a laxer test would print live headroom about a window
|
|
268
|
+
// the runner had already decided it could not vouch for — two surfaces
|
|
269
|
+
// disagreeing about one file, with the reader believing the wrong one.
|
|
270
|
+
const gaugeMod = require('../modules/autonomy/gauge.js');
|
|
271
|
+
const tooOld = gaugeMod.DEFAULT_STALE_AFTER_S + 60;
|
|
272
|
+
|
|
273
|
+
const [fresh] = windowsFromGauge(
|
|
274
|
+
{ five_hour: { used_percentage: 41, resets_at: NOW_S + 7200, observed_at: NOW_S - 60 } }, NOW_S);
|
|
275
|
+
assert.equal(fresh.known, true, 'a recent reading is usable');
|
|
276
|
+
|
|
277
|
+
const [stale] = windowsFromGauge(
|
|
278
|
+
{ five_hour: { used_percentage: 41, resets_at: NOW_S + 7200, observed_at: NOW_S - tooOld } }, NOW_S);
|
|
279
|
+
assert.equal(stale.known, false);
|
|
280
|
+
assert.equal(stale.stale, true);
|
|
281
|
+
assert.match(stale.detail, /older than the runner trusts/);
|
|
282
|
+
assert.doesNotMatch(stale.detail, /resets in/, 'never offer headroom from a reading the runner rejected');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('a window with no observed_at falls back to the file’s written_at', () => {
|
|
286
|
+
// One window can go unreported while the other is carried forward — the exact
|
|
287
|
+
// shape gauge.js reads.
|
|
288
|
+
const gaugeMod = require('../modules/autonomy/gauge.js');
|
|
289
|
+
const old = NOW_S - gaugeMod.DEFAULT_STALE_AFTER_S - 60;
|
|
290
|
+
const [w] = windowsFromGauge({ written_at: old, five_hour: { used_percentage: 41, resets_at: NOW_S + 7200 } }, NOW_S);
|
|
291
|
+
assert.equal(w.stale, true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('the period bounds survive a log too long to spread as arguments', () => {
|
|
295
|
+
// `Math.min(...times)` throws past the engine's argument cap, and a reporting
|
|
296
|
+
// CLI that dies on a big log fails exactly when the log is worth reading.
|
|
297
|
+
const many = Array.from({ length: 200000 }, (_, i) => ({ at: at(i % 1000), event: 'cadence', mode: 'full' }));
|
|
298
|
+
let d;
|
|
299
|
+
assert.doesNotThrow(() => { d = buildDigest(many, { nowEpochS: NOW_S }); });
|
|
300
|
+
assert.equal(d.period.rows, 200000);
|
|
301
|
+
assert.equal(d.period.fromMs, T0);
|
|
302
|
+
});
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
import { strict as assert } from 'node:assert';
|
|
30
30
|
import { createRequire } from 'node:module';
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
31
33
|
|
|
32
34
|
const require = createRequire(import.meta.url);
|
|
33
35
|
const R = require('../scripts/gds/provision-core-upgrade.js');
|
|
@@ -46,13 +48,19 @@ const t = async (name, fn) => {
|
|
|
46
48
|
// instant. `apply:true` because the interesting behaviour is the applied path.
|
|
47
49
|
function makeDeps({ shape = 'control-plane', served = '1.19.760', available = ['1.19.758', '1.19.760', '1.19.762'], installed = '1.19.760', queries = [] } = {}) {
|
|
48
50
|
const ran = [];
|
|
49
|
-
|
|
51
|
+
// task 1004053: the CWD is half of what makes a relative CLI path resolve, and the
|
|
52
|
+
// bug that shipped was invisible to every assertion that read only the command
|
|
53
|
+
// string. Captured alongside, so a test can check the pair.
|
|
54
|
+
const ranWith = [];
|
|
55
|
+
const exec = (cmd, opts) => {
|
|
50
56
|
ran.push(cmd);
|
|
57
|
+
ranWith.push({ cmd, cwd: opts && opts.cwd });
|
|
51
58
|
if (/\/version/.test(cmd)) return { ok: true, stdout: served == null ? '' : JSON.stringify({ coreVersion: served }) };
|
|
52
59
|
return { ok: true, stdout: '' };
|
|
53
60
|
};
|
|
54
61
|
return {
|
|
55
62
|
ran,
|
|
63
|
+
ranWith,
|
|
56
64
|
queries,
|
|
57
65
|
db: { query: async (sql, params) => { queries.push({ sql, params }); return { rows: [] }; } },
|
|
58
66
|
provisioning: { recordEvent: async () => {}, clearErrorNote: async () => {} },
|
|
@@ -245,6 +253,40 @@ await t('drives upgrade.js with the registry, the health gate, the version read-
|
|
|
245
253
|
assert.ok(cmd.includes('--commit-pin'));
|
|
246
254
|
});
|
|
247
255
|
|
|
256
|
+
await t('the upgrade CLI is run somewhere it ACTUALLY EXISTS — the bug that broke every standalone tenant (task 1004053)', async () => {
|
|
257
|
+
// The whole failure was a path that resolved nowhere:
|
|
258
|
+
// Error: Cannot find module '/srv/<base>/<slug>/scripts/gds/upgrade.js'
|
|
259
|
+
// No assertion about FLAGS could have caught it, so this one resolves the composed
|
|
260
|
+
// cwd + relative CLI against the real filesystem, for every shape the door admits.
|
|
261
|
+
for (const shape of ['control-plane', 'standalone', 'co-tenant']) {
|
|
262
|
+
const d = makeDeps({ served: '1.19.762' });
|
|
263
|
+
await R.coreUpgradeInstance(inst({ hosting_shape: shape, slug: 'hermeslines-marketing', port: 3011 }), d, intent());
|
|
264
|
+
const call = d.ranWith.find((c) => /upgrade\.js/.test(c.cmd));
|
|
265
|
+
assert.ok(call, `${shape}: it ran the upgrade CLI`);
|
|
266
|
+
const rel = (call.cmd.match(/node (\S*scripts\/gds\/upgrade\.js)/) || [])[1];
|
|
267
|
+
assert.ok(rel, `${shape}: the command names the CLI`);
|
|
268
|
+
assert.ok(fs.existsSync(path.join(call.cwd, rel)),
|
|
269
|
+
`${shape}: ${path.join(call.cwd, rel)} must exist — this is the assertion the original bug would have failed`);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
await t('a standalone tenant is NAMED with --instance, because the CLI no longer runs inside it', async () => {
|
|
274
|
+
const d = makeDeps({ served: '1.19.762' });
|
|
275
|
+
await R.coreUpgradeInstance(inst({ hosting_shape: 'standalone', slug: 'hermeslines-marketing', port: 3011 }), d, intent());
|
|
276
|
+
const cmd = d.ran.find((c) => /upgrade\.js/.test(c));
|
|
277
|
+
assert.match(cmd, /--instance \S*hermeslines-marketing/, 'the target is named explicitly');
|
|
278
|
+
assert.ok(cmd.includes('--service hermeslines-marketing'), 'and its own unit is still the one restarted');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await t('the control-plane row carries NO --instance — its target IS the checkout the CLI runs in', async () => {
|
|
282
|
+
// Naming it would be harmless but wrong, and the shapes that already worked must not
|
|
283
|
+
// change behaviour on the way to fixing the one that never did.
|
|
284
|
+
const d = makeDeps({ served: '1.19.762' });
|
|
285
|
+
await R.coreUpgradeInstance(inst(), d, intent());
|
|
286
|
+
const cmd = d.ran.find((c) => /upgrade\.js/.test(c));
|
|
287
|
+
assert.ok(!cmd.includes('--instance'), 'unchanged for the platform itself');
|
|
288
|
+
});
|
|
289
|
+
|
|
248
290
|
await t('the health URL and the version URL are LOOPBACK — the apex is behind basic_auth and a CDN (ADR 0293 D6)', async () => {
|
|
249
291
|
const d = makeDeps();
|
|
250
292
|
await R.coreUpgradeInstance(inst(), d, intent());
|
|
@@ -64,7 +64,7 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
|
64
64
|
// unaffected — it held right up to the deletion.
|
|
65
65
|
//
|
|
66
66
|
// ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
|
|
67
|
-
// the monitoring pages (/watch, /
|
|
67
|
+
// the monitoring pages (/watch, /gate) now open to the working rank. That gate
|
|
68
68
|
// was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
|
|
69
69
|
// were first written against — and was renamed once the floor moved; the audience here,
|
|
70
70
|
// not the name, is the contract.
|
|
@@ -91,7 +91,7 @@ async function boot({ meStatus }) {
|
|
|
91
91
|
wirePager: () => {},
|
|
92
92
|
},
|
|
93
93
|
OTBHallRender: () => ({
|
|
94
|
-
SETUP_CONFIG_KEY: 'k', analyticsState: {}, lbState: {},
|
|
94
|
+
SETUP_CONFIG_KEY: 'k', analyticsState: {}, lbState: {},
|
|
95
95
|
shipState: {}, shipsPager: { page: 1 }, welcomeDismissalKey: () => 'k',
|
|
96
96
|
mountHallWidgets: spy('mountHallWidgets'),
|
|
97
97
|
renderLanding: spy('renderLanding'),
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -287,7 +287,7 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
287
287
|
// 'deploy' (task 1003159, ADR 0293) is the SECOND archon item, beside 'government':
|
|
288
288
|
// its gate is the `core.pin.move` atom, archon-floor and system:true. It sits with
|
|
289
289
|
// the steering surfaces because that is what it is — the owner's third release gate.
|
|
290
|
-
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', '
|
|
290
|
+
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'gate', 'sessions', 'project-settings', 'deploy', 'government']);
|
|
291
291
|
});
|
|
292
292
|
|
|
293
293
|
test('Economy lives in Docs and points at the reward-schedule page', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// tests/hall_oversight_world.mjs — the oversight family in the chrome world
|
|
2
|
-
// (task 1003469, goal 1000081): government, watch
|
|
2
|
+
// (task 1003469, goal 1000081): government, watch and gate share one
|
|
3
3
|
// archetype sheet (oversight.css) the way the records share record.css and the
|
|
4
4
|
// reading rooms room.css. These are static pins over the shipped source: the
|
|
5
5
|
// family sheet is linked and spends only the fifteen, every page renders the
|
|
@@ -17,7 +17,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
17
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
18
|
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
19
19
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
|
|
20
|
-
const PAGES = ['government', 'watch', '
|
|
20
|
+
const PAGES = ['government', 'watch', 'gate'];
|
|
21
21
|
|
|
22
22
|
test('every oversight page links the family sheet, before its own', () => {
|
|
23
23
|
for (const p of PAGES) {
|
|
@@ -76,8 +76,8 @@ test('Government renders the rank description the create form collects', () => {
|
|
|
76
76
|
assert.match(read('government.html'), /id="gov-create-desc"/, 'the form still collects it');
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
test('
|
|
80
|
-
for (const p of ['
|
|
79
|
+
test('Gate carries no coloured band: state is a pill', () => {
|
|
80
|
+
for (const p of ['gate']) {
|
|
81
81
|
assert.ok(!/__roof|__seal/.test(read(`${p}.js`)), `${p}.js emits no roof or seal band`);
|
|
82
82
|
assert.ok(!/__roof|__seal/.test(read(`${p}.css`)), `${p}.css has no band rule`);
|
|
83
83
|
assert.match(read(`${p}.js`), /fact-pill fact-pill--/, `${p}.js paints state as a .fact-pill`);
|
|
@@ -91,7 +91,7 @@ test('the shared confirm modal sits on --z-modal, closed by one hairline and no
|
|
|
91
91
|
const modal = css.slice(css.indexOf('.ov-modal {'), css.indexOf('.ov-modal .scroll__h'));
|
|
92
92
|
assert.ok(!/box-shadow/.test(modal), 'no shadow under the dialog');
|
|
93
93
|
assert.match(modal, /border: var\(--line\) solid var\(--rule\)/, 'one rule hairline');
|
|
94
|
-
for (const p of ['
|
|
94
|
+
for (const p of ['gate']) {
|
|
95
95
|
assert.match(read(`${p}.js`), /className = 'ov-modal-bg'/, `${p}.js opens the family modal`);
|
|
96
96
|
assert.ok(!/-modal-bg/.test(read(`${p}.css`)), `${p}.css no longer styles a modal of its own`);
|
|
97
97
|
}
|
|
@@ -44,7 +44,6 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'ut
|
|
|
44
44
|
const ORACLE = {
|
|
45
45
|
// Any signed-in builder — no rank floor.
|
|
46
46
|
'/builders/work': 'requireBuilderPage',
|
|
47
|
-
'/builders/pair': 'requireBuilderPage',
|
|
48
47
|
'/builders/settings': 'requireBuilderPage',
|
|
49
48
|
'/builders/atlas': 'requireBuilderPage',
|
|
50
49
|
'/builders/modules': 'requireBuilderPage',
|
|
@@ -74,7 +73,6 @@ const ORACLE = {
|
|
|
74
73
|
|
|
75
74
|
// Metic+ operator surfaces (the Governance nav group).
|
|
76
75
|
'/builders/watch': 'requireGovernmentPage',
|
|
77
|
-
'/builders/harbor': 'requireGovernmentPage',
|
|
78
76
|
'/builders/gate': 'requireGovernmentPage',
|
|
79
77
|
'/builders/sessions': 'requireGovernmentPage',
|
|
80
78
|
// The five fundamentals of the project (task 1003280, goal 1000072). Same tier as
|
|
@@ -155,6 +153,14 @@ const ORACLE = {
|
|
|
155
153
|
// catch. GONE_PAGES below keeps the row honest if the file ever returns.
|
|
156
154
|
'/builders/my-projects': null,
|
|
157
155
|
|
|
156
|
+
// Gone with the dev box (task 1003890, goal 1000091). Harbor was the Archon
|
|
157
|
+
// view of the box fleet and pair the device-pairing page; the fleet was turned
|
|
158
|
+
// off at the control plane by task 1003886, so both pages drove an API that no
|
|
159
|
+
// longer answers. Their gates left serve-internal.js with them — a gate in
|
|
160
|
+
// front of nothing is the dead-guard shape the check below exists to catch.
|
|
161
|
+
'/builders/harbor': null,
|
|
162
|
+
'/builders/pair': null,
|
|
163
|
+
|
|
158
164
|
// Gone too, and for a reason worth keeping in front of the next reader: this
|
|
159
165
|
// was a bespoke refusal PAGE ("not yet — ship three tasks to graduate"), the
|
|
160
166
|
// deniedTo of the thetes+ gate. Task 1002613 replaced that whole idea with a
|
|
@@ -278,6 +284,9 @@ const GONE_PAGES = {
|
|
|
278
284
|
'/builders/my-projects': 'task 1003308 — the hub owns "my projects"',
|
|
279
285
|
// Task 1003450 — the shell's ?denied= banner replaced the bespoke refusal page.
|
|
280
286
|
'/builders/not-ready': 'task 1003450 — the shell renders the refusal from ?denied=',
|
|
287
|
+
// Task 1003890 — the dev-box deprecation removed the fleet these two drove.
|
|
288
|
+
'/builders/harbor': 'task 1003890 — the dev box is gone, so its Archon view is too',
|
|
289
|
+
'/builders/pair': 'task 1003890 — the dev box is gone, so its pairing page is too',
|
|
281
290
|
};
|
|
282
291
|
|
|
283
292
|
test('a row that is null because the page left the hall serves no page', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// tests/hall_settings_world.mjs — the PANEL family in the chrome world
|
|
2
|
-
// (task 1003308, goal 1000081): settings
|
|
2
|
+
// (task 1003308, goal 1000081): settings and profile. Pair left with the dev
|
|
3
|
+
// box (task 1003890).
|
|
3
4
|
//
|
|
4
5
|
// tests/hall_tokens.mjs pins the TIER these pages spend; this pins how they
|
|
5
6
|
// spend it. Everything here is a fact that is silent in a browser: the page
|
|
@@ -9,9 +10,9 @@
|
|
|
9
10
|
//
|
|
10
11
|
// What it locks:
|
|
11
12
|
// * ONE ARCHETYPE SHEET. panel.css holds the row, the switch, the field, the
|
|
12
|
-
// chip control and the family's voices; the
|
|
13
|
-
//
|
|
14
|
-
// * THE ONE h1. None of
|
|
13
|
+
// chip control and the family's voices; the page sheets hold only what one
|
|
14
|
+
// page has. Every page links panel.css BEFORE its own sheet.
|
|
15
|
+
// * THE ONE h1. None of them had one — the hall predates the floor — so
|
|
15
16
|
// every render failed one-header-main-h1. The h1 is the page's name in the
|
|
16
17
|
// room voice, and it lives in the static markup so the DOM has it in every
|
|
17
18
|
// state, not only the happy path.
|
|
@@ -26,14 +27,14 @@
|
|
|
26
27
|
// * WHITE ON THE ACCENT, AND THE ONE EXCEPTION. Eight literals across three
|
|
27
28
|
// sheets are gone; the Discord blurple pair stays, because it is Discord's
|
|
28
29
|
// mark and not the world's, and it is named as the exception in a comment.
|
|
29
|
-
// * THE COMPAT ALIASES.
|
|
30
|
+
// * THE COMPAT ALIASES. Every sheet in the family, zero aliases spent.
|
|
30
31
|
//
|
|
31
32
|
// Reproduce the renders these came from (the family needs the fixture harness —
|
|
32
33
|
// every panel is filled from /me/* reads the kit stub does not carry):
|
|
33
34
|
// node scripts/hall-preview/server.js --port 4621 --fixture-me
|
|
34
35
|
// node modules/ui-design/kit/render.js --states modules/hall-ui/public/settings.states.json \
|
|
35
36
|
// --base-in http://builders.localhost:4621 --base-out http://builders.localhost:4621
|
|
36
|
-
// (and profile
|
|
37
|
+
// (and profile .states.json the same way)
|
|
37
38
|
|
|
38
39
|
import { test } from 'node:test';
|
|
39
40
|
import assert from 'node:assert/strict';
|
|
@@ -48,9 +49,10 @@ const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
|
48
49
|
|
|
49
50
|
// not-ready was the fourth until task 1003450 deleted the page — see
|
|
50
51
|
// tests/hall_page_gate_map.mjs (GONE_PAGES) for why, and for the row that stops
|
|
51
|
-
// it coming back ungated
|
|
52
|
-
|
|
53
|
-
const
|
|
52
|
+
// it coming back ungated; pair left the same way in task 1003890. The archetype
|
|
53
|
+
// is unaffected: the pages that remain still spend it.
|
|
54
|
+
const PAGES = ['settings', 'profile'];
|
|
55
|
+
const SHEETS = ['panel.css', 'settings.css', 'profile.css'];
|
|
54
56
|
|
|
55
57
|
const panelCss = read('panel.css');
|
|
56
58
|
const settingsCss = read('settings.css');
|
|
@@ -94,7 +96,7 @@ test('the archetype holds the row, the switch, the field and the voices; the pag
|
|
|
94
96
|
for (const sel of ['.prow', '.sw', '.sw__track', '.sw__knob', '.field-label', '.pinput', '.chip-ctl', '.panel-note', '.panel-status', '.gate']) {
|
|
95
97
|
assert.ok(ruleOf(panelCss, sel), `panel.css defines ${sel}`);
|
|
96
98
|
}
|
|
97
|
-
for (const sheet of ['settings.css', '
|
|
99
|
+
for (const sheet of ['settings.css', 'profile.css']) {
|
|
98
100
|
const css = read(sheet);
|
|
99
101
|
for (const sel of ['.prow', '.sw', '.sw__track', '.pinput', '.chip-ctl']) {
|
|
100
102
|
assert.equal(ruleOf(css, sel), null, `${sheet} must not redefine ${sel} — that is how the family would drift`);
|
|
@@ -104,7 +106,7 @@ test('the archetype holds the row, the switch, the field and the voices; the pag
|
|
|
104
106
|
|
|
105
107
|
// ---- 2. the one h1, in the static markup ----------------------------------
|
|
106
108
|
|
|
107
|
-
test('each
|
|
109
|
+
test('each page in the family has exactly one h1, in the family\'s page head', () => {
|
|
108
110
|
for (const p of PAGES) {
|
|
109
111
|
const html = read(`${p}.html`).replace(/<!--[\s\S]*?-->/g, '');
|
|
110
112
|
const h1s = html.match(/<h1\b/g) || [];
|
|
@@ -301,7 +303,7 @@ test('no sheet in the family spends a compat alias', () => {
|
|
|
301
303
|
});
|
|
302
304
|
|
|
303
305
|
test('white on the accent is gone, and the Discord mark is the one named exception', () => {
|
|
304
|
-
for (const sheet of ['panel.css', '
|
|
306
|
+
for (const sheet of ['panel.css', 'profile.css']) {
|
|
305
307
|
const bare = stripComments(read(sheet));
|
|
306
308
|
assert.doesNotMatch(bare, /#[0-9a-f]{3,8}\b/i, `${sheet} carries no colour literal`);
|
|
307
309
|
assert.doesNotMatch(bare, /:\s*white\b/i, `${sheet} carries no named white`);
|
|
@@ -318,16 +320,17 @@ test('white on the accent is gone, and the Discord mark is the one named excepti
|
|
|
318
320
|
assert.match(selector, /\.settings-btn--discord/, `${lit} is spent only on the Discord button`);
|
|
319
321
|
}
|
|
320
322
|
assert.match(settingsCss, /THIRD-PARTY MARK, KEPT ON PURPOSE/, 'the exception is named in a comment so a literal sweep leaves it alone');
|
|
321
|
-
// the save/accent buttons take --accent-on. `.
|
|
322
|
-
//
|
|
323
|
-
//
|
|
323
|
+
// the save/accent buttons take --accent-on. `.craft-save` is a member of the
|
|
324
|
+
// base pill rule AND the save rule, so the save rule is found by its own name
|
|
325
|
+
// and the member is asserted against it. (`.pair-approve` was its sibling here
|
|
326
|
+
// until task 1003890 removed the pair page.)
|
|
324
327
|
const save = ruleOf(panelCss, '.pbtn--save');
|
|
325
328
|
assert.ok(save, '.pbtn--save has a rule');
|
|
326
329
|
assert.match(save, /background:\s*var\(--accent\)/);
|
|
327
330
|
assert.match(save, /color:\s*var\(--accent-on\)/, 'the dark ink for the salmon — white on it fails AA');
|
|
328
331
|
const saveSelector = stripComments(panelCss).match(/([^{}]*\.pbtn--save[^{}]*)\{/);
|
|
329
332
|
assert.ok(saveSelector, 'the save rule\'s selector list is readable');
|
|
330
|
-
for (const member of ['.craft-save'
|
|
333
|
+
for (const member of ['.craft-save']) {
|
|
331
334
|
assert.ok(saveSelector[1].split(',').map((s) => s.trim()).includes(member), `${member} is the accent door on its page`);
|
|
332
335
|
}
|
|
333
336
|
});
|
|
@@ -394,14 +397,14 @@ test('the profile identity band is a MODIFIER on the shell\'s .profile, never a
|
|
|
394
397
|
|
|
395
398
|
// ---- 6. the floors are reproducible ------------------------------------------
|
|
396
399
|
|
|
397
|
-
test('
|
|
400
|
+
test('every page in the family has a states file, so the floors are reproducible', () => {
|
|
398
401
|
for (const p of PAGES) {
|
|
399
402
|
const json = JSON.parse(read(`${p}.states.json`));
|
|
400
403
|
assert.equal(json.surface, 'hall-ui', `${p}.states.json names its surface`);
|
|
401
404
|
assert.ok(Object.keys(json.states).length >= 2, `${p}.states.json renders more than the default state`);
|
|
402
405
|
}
|
|
403
406
|
assert.ok(Object.keys(JSON.parse(read('settings.states.json')).states).length >= 4, 'settings renders every rail group');
|
|
404
|
-
for (const fx of ['me__sessions'
|
|
407
|
+
for (const fx of ['me__sessions']) {
|
|
405
408
|
assert.ok(fs.existsSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', `${fx}.json`)),
|
|
406
409
|
`the harness serves ${fx} — without it the page only ever rendered its empty state`);
|
|
407
410
|
}
|