@bongos/core 1.25.31 → 1.25.33
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 +36 -41
- package/docs/copy-inventory.md +28 -28
- package/docs/copy-registry.json +30 -30
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +202 -175
- package/modules/autonomy/runner-health.js +19 -0
- package/modules/hall-ui/public/gate.js +9 -5
- package/modules/hall-ui/public/index.html +4 -4
- package/modules/hall-ui/public/runner-button.js +7 -0
- package/modules/hall-ui/public/shell-game.css +2 -3
- package/modules/hall-ui/public/shell-game.js +6 -5
- package/modules/hall-ui/public/welcome-boot.css +18 -31
- package/modules/hall-ui/public/welcome-boot.js +81 -97
- package/modules/hall-ui/records/start-screen.md +6 -6
- package/modules/hall-ui/records/welcome-tower.md +29 -20
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/autobongos-host.js +22 -11
- package/scripts/bongos/autobongos-run.js +37 -5
- package/src/module-api.js +1 -1
- package/tests/autobongos_cannot_fetch.mjs +123 -0
- package/tests/hall_sky_time.mjs +3 -2
- package/tests/hall_start_screen.mjs +3 -2
- package/tests/welcome_boot.mjs +35 -27
- package/tests/welcome_boot_browser.mjs +25 -19
- package/modules/hall-ui/public/welcome-day.webp +0 -0
- package/modules/hall-ui/public/welcome-night.webp +0 -0
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
# The welcome — Home's first visit of the day (tasks 1004785, 1004818)
|
|
2
2
|
|
|
3
3
|
The owner's Builders' Hall idea 1001262, second half (the first is [`home-reshuffle.md`](home-reshuffle.md)).
|
|
4
|
-
Files: `public/welcome-boot.js` + `public/welcome-boot.css`,
|
|
5
|
-
`
|
|
4
|
+
Files: `public/welcome-boot.js` + `public/welcome-boot.css`, on Home only. The scene is drawn by
|
|
5
|
+
Home's own city painters (`sky-time.js`, `city-draw.js`, `home-city-draw.js`).
|
|
6
6
|
|
|
7
7
|
**How the look got here (2026-10-23, one day of owner rounds).** Task 1004785 built v5 "the tower"
|
|
8
8
|
([`docs/design/mocks/hall/welcome/v5.src.html`](../../../docs/design/mocks/hall/welcome/v5.src.html)),
|
|
9
9
|
drawn live from the Repo Atlas. The owner then asked for day and night and a slow way out, then
|
|
10
10
|
swapped the tower for a glowing module city with a lunch crew on a beam, dropped the crew, and
|
|
11
11
|
finally had Gemini paint the city by day and by night and asked for those to be animated (task
|
|
12
|
-
1004818). The atlas-drawn scene is gone
|
|
12
|
+
1004818). The atlas-drawn scene is gone.
|
|
13
|
+
|
|
14
|
+
**The painting is retired too (task 1005308, owner 2026-11-14: "retire it everywhere").** The scene
|
|
15
|
+
is now the city in the clouds from task 1005196, the same city Home and the City page draw, in the
|
|
16
|
+
hour's sky. The two WebP paintings and every light that was pinned to their pixels (the circuit
|
|
17
|
+
towers' pulses, the hover cars, the moon and stars) are deleted. The game's start and load screens
|
|
18
|
+
stand on the same scene (`records/start-screen.md`).
|
|
13
19
|
|
|
14
20
|
- **When.** A signed-in builder's first Home load of the LOCAL day, per builder id
|
|
15
21
|
(`localStorage['hall-welcome-day:<id>']`). Storage that throws is never shown it, because it could not
|
|
@@ -20,15 +26,16 @@ finally had Gemini paint the city by day and by night and asked for those to be
|
|
|
20
26
|
`welcome-boot.js` loads before the `shell-trail.js` / `craft-mode.js` / `shell.js` trio, which
|
|
21
27
|
`tests/craft_mode.mjs` keeps adjacent.
|
|
22
28
|
- **Day and night.** `timeOfDay` makes 06:00 to 17:59 local `day`, the rest `night`; the root wears
|
|
23
|
-
`.wb--day` or `.wb--night
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
`.wb--day` or `.wb--night`, which sets the ink. It opens from a veil (`.wb__veil`), white by day
|
|
30
|
+
and black by night, that lifts two frames after the scene is painted (or after 1.5s). The scene's
|
|
31
|
+
own colours follow the hour more finely (`sky-time.js`: night, dawn, midday, dusk, blended).
|
|
32
|
+
- **The scene is the city, as a backdrop.** `paintScene(host)` asks `OTBHomeCityDraw.citySvg` for an
|
|
33
|
+
empty week (the Cloud Bongos core and the museum, standing in cloud) and rewrites it with
|
|
34
|
+
`sceneShape`: every link becomes a plain group, tooltips and goal hooks are dropped, the faded
|
|
35
|
+
`hc-quiet` look is lifted so the core leads, and every id is prefixed `cs-` so it never meets the
|
|
36
|
+
ids of Home's own city on the same page. It covers the screen, standing on the bottom edge
|
|
37
|
+
(`xMidYMax slice`), over `.cloud-scene`'s sky gradient, with a slow drift and two clouds. The same
|
|
38
|
+
`paintScene` is handed to the game's start screen as `scene`.
|
|
32
39
|
- **Banner.** "Working on the core of <project>" from `__BRANDING__.identity` (never a hardcoded word),
|
|
33
40
|
with `core <version>` under it when the public `GET /version` reports one.
|
|
34
41
|
- **Sequence.** Veil lifts, the city powers up (pulses brighten), the greeting comes up at ~3.4s.
|
|
@@ -40,11 +47,13 @@ finally had Gemini paint the city by day and by night and asked for those to be
|
|
|
40
47
|
Artisan → stays on Home (the Ideator door, once `/thinking`, folded into Artist). A pick also calls
|
|
41
48
|
`OTBCraftMode.choose()` with patron / artist / artisan. **Patron shows only where
|
|
42
49
|
`OTBCraftMode.offered('patron')`** — the patron craft is a seat — and the list is built when "Start with your role" is pressed, after the seats have settled.
|
|
43
|
-
- **No
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
- **No city painters** on the page: the root wears `.wb--bare`, the hour's sky gradient stands alone,
|
|
51
|
+
and everything else works.
|
|
52
|
+
- **The scene's colours are the hour's sky tokens only** (`--sky-now-*`); the welcome's ink, veil and
|
|
53
|
+
panels keep their fixed day/night literals, on `tests/hall_audit.mjs`'s literal exemptions.
|
|
54
|
+
- **Tests.** `tests/welcome_boot.mjs` (the day rule, that the painting and its lights are gone, the
|
|
55
|
+
scene's shapes with no door, no unprefixed id and no typed colour, the banner, the role map, the
|
|
56
|
+
load order); `tests/welcome_boot_browser.mjs` runs Home's markup with the real shell, craft mode,
|
|
57
|
+
welcome and city painters in a browser: once a day, Skip, Esc, no double picker, the seat rule, no
|
|
58
|
+
painters, reduced motion, the city in the hour's sky by day and night, the slow exit, and that the
|
|
59
|
+
painting's lights are gone.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.33",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.33",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.33",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11961,5 +11961,17 @@
|
|
|
11961
11961
|
"id": "1005310",
|
|
11962
11962
|
"text": "The governor's street asks the server for less and gets its answers sooner."
|
|
11963
11963
|
}
|
|
11964
|
+
],
|
|
11965
|
+
"1.25.32": [
|
|
11966
|
+
{
|
|
11967
|
+
"id": "1005308",
|
|
11968
|
+
"text": "The old circuit-city painting is gone. The welcome screen and the game's start screens now open on the city in the clouds, coloured for the time of day: deep night with lit windows, a warm dawn, a bright midday."
|
|
11969
|
+
}
|
|
11970
|
+
],
|
|
11971
|
+
"1.25.33": [
|
|
11972
|
+
{
|
|
11973
|
+
"id": "1005202",
|
|
11974
|
+
"text": "A runner that cannot download new code from GitHub now shows \"Stuck on old code\" on its card, with the fix, instead of looking healthy."
|
|
11975
|
+
}
|
|
11964
11976
|
]
|
|
11965
11977
|
}
|
|
@@ -308,18 +308,28 @@ function git(args, opts = {}) {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
// refreshCheckout — the .cmd's fetch + reset, on the runner's own copy only.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
311
|
+
//
|
|
312
|
+
// Straight fetch failures are counted in `counter` and handed to the supervisor
|
|
313
|
+
// through `env` (AUTOBONGOS_FETCH_FAILURES), which seeds its own count, so its
|
|
314
|
+
// check-in can say `cannot_fetch` and the device's card stops reading healthy
|
|
315
|
+
// (task 1005202). The collaborators are injectable for the test only.
|
|
316
|
+
function refreshCheckout(log = () => {}, {
|
|
317
|
+
git: gitFn = git, assert = assertRunnerCopy, remedy = githubRemedy, counter = null, env = null,
|
|
318
|
+
} = {}) {
|
|
319
|
+
assert();
|
|
320
|
+
const f = gitFn(['fetch', '--quiet', 'origin', 'main']);
|
|
321
|
+
const fetched = f.status === 0;
|
|
322
|
+
if (counter) counter.fetchFailures = fetched ? 0 : (counter.fetchFailures || 0) + 1;
|
|
323
|
+
if (counter && env) env.AUTOBONGOS_FETCH_FAILURES = String(counter.fetchFailures);
|
|
324
|
+
if (!fetched) {
|
|
325
|
+
// A fetch that fails for a credential reason fails every loop, and the runner then
|
|
326
|
+
// works forever on the code it happens to have (task 1005096), so the log carries
|
|
327
|
+
// the remedy, and the count above carries the fact to the card.
|
|
318
328
|
log(`git fetch failed — running the code already here (${String(f.stderr || '').trim().slice(0, 200)})`);
|
|
319
|
-
log(` if that keeps happening, it is not transient: ${
|
|
329
|
+
log(` if that keeps happening, it is not transient: ${remedy()}`);
|
|
320
330
|
return false;
|
|
321
331
|
}
|
|
322
|
-
const r =
|
|
332
|
+
const r = gitFn(['reset', '--quiet', '--hard', 'origin/main']);
|
|
323
333
|
if (r.status !== 0) { log(`git reset failed (${String(r.stderr || '').trim().slice(0, 200)})`); return false; }
|
|
324
334
|
return true;
|
|
325
335
|
}
|
|
@@ -821,6 +831,7 @@ async function runCommand({ paths = hostPaths() } = {}) {
|
|
|
821
831
|
const log = makeLogger(paths, { echo: !!process.stdout.isTTY });
|
|
822
832
|
const env = supervisorEnv(process.env, paths, dirName);
|
|
823
833
|
const state = { child: null, stopping: false };
|
|
834
|
+
const fetchCounter = { fetchFailures: 0 }; // straight failed fetches, handed to the supervisor (task 1005202)
|
|
824
835
|
const keepAwake = makeKeepAwake();
|
|
825
836
|
const selfSha = fileSha(__filename);
|
|
826
837
|
writeLock(paths);
|
|
@@ -836,7 +847,7 @@ async function runCommand({ paths = hostPaths() } = {}) {
|
|
|
836
847
|
rotateLog(paths);
|
|
837
848
|
const out = await runLoop({}, {
|
|
838
849
|
log, shouldStop: () => state.stopping,
|
|
839
|
-
refresh: () => refreshCheckout(log),
|
|
850
|
+
refresh: () => refreshCheckout(log, { counter: fetchCounter, env }),
|
|
840
851
|
ensureDeps: () => ensureDeps(paths, log),
|
|
841
852
|
spawnSupervisor: () => spawnSupervisorReal({ paths, env, log, keepAwake, state }),
|
|
842
853
|
selfChanged: () => fileSha(__filename) !== selfSha,
|
|
@@ -924,5 +935,5 @@ module.exports = {
|
|
|
924
935
|
windowsRunArgv, windowsRunCommand, windowsStartSpawn, legacyBootTaskInstalled, launchdPlist,
|
|
925
936
|
stopRunner, stopReport, exchangeCode, claudeState, waitForCheckIn,
|
|
926
937
|
signIn, cleanBase, install, removeStartEntry, installedState, status, digestArgv,
|
|
927
|
-
githubRemedy, GITHUB_REMEDY_GENERIC,
|
|
938
|
+
githubRemedy, GITHUB_REMEDY_GENERIC, refreshCheckout,
|
|
928
939
|
};
|
|
@@ -511,8 +511,19 @@ const MAX_CLAIM_ATTEMPTS = 5;
|
|
|
511
511
|
// guess that is wrong costs one wasted spawn per half hour, not one per wake.
|
|
512
512
|
const LIMIT_UNKNOWN_RESET_MS = 30 * 60 * 1000;
|
|
513
513
|
|
|
514
|
+
// How many straight failures to reach origin before the check-in says
|
|
515
|
+
// `cannot_fetch` (task 1005202). A blip fails once or twice and clears; a
|
|
516
|
+
// credential fault (the active GitHub account cannot see the project) fails
|
|
517
|
+
// every time, forever, and the runner then works on whatever code it has. The
|
|
518
|
+
// polls inside a wait are a minute apart, so this is a few minutes of failing.
|
|
519
|
+
const CANNOT_FETCH_AFTER = 3;
|
|
520
|
+
|
|
521
|
+
// fetchFailures starts from the host wrapper's own count (AUTOBONGOS_FETCH_FAILURES,
|
|
522
|
+
// set by autobongos-host.js), so a restart does not reset the card to healthy.
|
|
514
523
|
function newRunnerState() {
|
|
515
|
-
|
|
524
|
+
const seeded = Number.parseInt(process.env.AUTOBONGOS_FETCH_FAILURES, 10);
|
|
525
|
+
return { setAside: new Map(), queueGate: null, limitUntil: null, diskHead: null, mainHead: null, landSweepAt: null,
|
|
526
|
+
fetchFailures: Number.isInteger(seeded) && seeded > 0 ? seeded : 0 };
|
|
516
527
|
}
|
|
517
528
|
const RUNNER_STATE = newRunnerState();
|
|
518
529
|
|
|
@@ -920,9 +931,23 @@ function heartbeatIdentity(deps = {}) {
|
|
|
920
931
|
return out;
|
|
921
932
|
}
|
|
922
933
|
|
|
934
|
+
// cannotFetchEvent — what a beat says once the runner has failed to reach the
|
|
935
|
+
// project CANNOT_FETCH_AFTER times in a row (task 1005202). It replaces the idle
|
|
936
|
+
// words ("waiting", "loop", nothing), which would otherwise read as healthy. It
|
|
937
|
+
// never hides a restart, a setting holding the runner, or the task in its hands:
|
|
938
|
+
// each of those is its own truth about this minute.
|
|
939
|
+
function cannotFetchEvent(fields = {}, state = RUNNER_STATE) {
|
|
940
|
+
if (!state || !((state.fetchFailures || 0) >= CANNOT_FETCH_AFTER)) return fields.last_event;
|
|
941
|
+
if (fields.working_task_id) return fields.last_event;
|
|
942
|
+
const ev = fields.last_event;
|
|
943
|
+
if (ev === 'upgrade_exit' || (typeof ev === 'string' && ev.startsWith('config_idle:'))) return ev;
|
|
944
|
+
return 'cannot_fetch';
|
|
945
|
+
}
|
|
946
|
+
|
|
923
947
|
async function postHeartbeat(fields = {}, deps = {}) {
|
|
924
948
|
try {
|
|
925
949
|
const api = deps.api || await (require('./cli-lib').cliClient)();
|
|
950
|
+
const lastEvent = cannotFetchEvent(fields, deps.state || RUNNER_STATE);
|
|
926
951
|
await api.autonomy.postAutonomyHeartbeat({
|
|
927
952
|
body: {
|
|
928
953
|
host: os.hostname(),
|
|
@@ -930,6 +955,7 @@ async function postHeartbeat(fields = {}, deps = {}) {
|
|
|
930
955
|
started_at: RUNNER_STARTED_AT,
|
|
931
956
|
...heartbeatIdentity(deps),
|
|
932
957
|
...fields,
|
|
958
|
+
...(lastEvent ? { last_event: lastEvent } : {}),
|
|
933
959
|
},
|
|
934
960
|
});
|
|
935
961
|
return true;
|
|
@@ -1165,11 +1191,17 @@ async function refreshCheckout(deps = {}) {
|
|
|
1165
1191
|
// The head of origin/main, or null. `git ls-remote` rather than `fetch`: it moves
|
|
1166
1192
|
// no refs and writes nothing, so polling it every few minutes cannot interfere
|
|
1167
1193
|
// with a worker building in a tree beside it.
|
|
1194
|
+
//
|
|
1195
|
+
// Every call also counts toward `fetchFailures` (task 1005202): a success clears
|
|
1196
|
+
// it, anything else adds one, so the heartbeat can say the runner cannot reach
|
|
1197
|
+
// the project instead of reading healthy on code it can never update.
|
|
1168
1198
|
async function runFetchHead(deps = {}) {
|
|
1169
1199
|
const runCmd = deps.run || run;
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1200
|
+
const st = deps.state || RUNNER_STATE;
|
|
1201
|
+
let r = null;
|
|
1202
|
+
try { r = await runCmd('git', ['ls-remote', 'origin', 'refs/heads/main']); } catch (_) { r = null; }
|
|
1203
|
+
const m = r && r.ok ? /^([0-9a-f]{40})/.exec(String(r.stdout).trim()) : null;
|
|
1204
|
+
st.fetchFailures = m ? 0 : (st.fetchFailures || 0) + 1;
|
|
1173
1205
|
return m ? m[1] : null;
|
|
1174
1206
|
}
|
|
1175
1207
|
|
|
@@ -1395,6 +1427,6 @@ module.exports = {
|
|
|
1395
1427
|
worktreeName, readFence, gradeLedgerPath, gradeCapReader, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX, codeDrifted, loadedHead, UPGRADE_EXIT_CODE, MIN_UPTIME_BEFORE_UPGRADE_MS,
|
|
1396
1428
|
heartbeatPath, writeHeartbeat, readHeartbeat, pidAlive, anotherRunnerIsAlive, HEARTBEAT_STALE_MS,
|
|
1397
1429
|
isRunnerTree, RUNNER_TREE_RE, postHeartbeat, claudeAccountLabel, RUNNER_STARTED_AT, failureReason,
|
|
1398
|
-
newRunnerState, loadedFiles, supervisorTouched, refreshCheckout, LIMIT_UNKNOWN_RESET_MS, QUEUE_GATED_EXIT, CLAIM_SET_ASIDE_MS, MAX_CLAIM_ATTEMPTS, owedTaskIds,
|
|
1430
|
+
newRunnerState, runFetchHead, cannotFetchEvent, CANNOT_FETCH_AFTER, loadedFiles, supervisorTouched, refreshCheckout, LIMIT_UNKNOWN_RESET_MS, QUEUE_GATED_EXIT, CLAIM_SET_ASIDE_MS, MAX_CLAIM_ATTEMPTS, owedTaskIds,
|
|
1399
1431
|
readRunLogRows, recheckPendingLands, LAND_SWEEP_MS,
|
|
1400
1432
|
};
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.33'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// tests/autobongos_cannot_fetch.mjs — a runner that can never fetch says so on
|
|
2
|
+
// its card (task 1005202).
|
|
3
|
+
//
|
|
4
|
+
// Found on a real Mac: two GitHub accounts were signed in, the active one could
|
|
5
|
+
// not see the private project, and every `git fetch` 404'd. The runner checked in
|
|
6
|
+
// as healthy and worked on the code it happened to clone, forever; only its own
|
|
7
|
+
// log said otherwise. These pin the signal end to end: the host and the
|
|
8
|
+
// supervisor count straight fetch failures, past CANNOT_FETCH_AFTER the check-in
|
|
9
|
+
// says `cannot_fetch`, the verdict is not ok and carries the remedy, and the
|
|
10
|
+
// device's button and checklist stop reading healthy. One good fetch clears it.
|
|
11
|
+
//
|
|
12
|
+
// DB-free, no network. Run: node --test tests/autobongos_cannot_fetch.mjs
|
|
13
|
+
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const req = createRequire(import.meta.url);
|
|
22
|
+
const runner = req(path.join(ROOT, 'scripts/bongos/autobongos-run.js'));
|
|
23
|
+
const host = req(path.join(ROOT, 'scripts/bongos/autobongos-host.js'));
|
|
24
|
+
const health = req(path.join(ROOT, 'modules/autonomy/runner-health.js'));
|
|
25
|
+
const RB = req(path.join(ROOT, 'modules/hall-ui/public/runner-button.js'));
|
|
26
|
+
|
|
27
|
+
const A = 'a'.repeat(40);
|
|
28
|
+
const NOW = Date.parse('2026-11-14T12:00:00.000Z');
|
|
29
|
+
const agoS = (s) => new Date(NOW - s * 1000).toISOString();
|
|
30
|
+
|
|
31
|
+
const failing = async () => ({ ok: false, stdout: '', stderr: 'remote: Repository not found.' });
|
|
32
|
+
const working = async () => ({ ok: true, stdout: `${A}\trefs/heads/main\n` });
|
|
33
|
+
|
|
34
|
+
function recorder() {
|
|
35
|
+
const bodies = [];
|
|
36
|
+
const api = { autonomy: { postAutonomyHeartbeat: async ({ body }) => { bodies.push(body); return { ok: true }; } } };
|
|
37
|
+
return { bodies, api };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('the supervisor counts straight failures to reach main, and one success clears the count', async () => {
|
|
41
|
+
const state = runner.newRunnerState();
|
|
42
|
+
assert.equal(state.fetchFailures, 0);
|
|
43
|
+
for (let i = 1; i <= 3; i++) {
|
|
44
|
+
assert.equal(await runner.runFetchHead({ run: failing, state }), null);
|
|
45
|
+
assert.equal(state.fetchFailures, i);
|
|
46
|
+
}
|
|
47
|
+
await runner.runFetchHead({ run: async () => { throw new Error('spawn git ENOENT'); }, state });
|
|
48
|
+
assert.equal(state.fetchFailures, 4, 'a git that cannot even start is a failure too');
|
|
49
|
+
assert.equal(await runner.runFetchHead({ run: working, state }), A);
|
|
50
|
+
assert.equal(state.fetchFailures, 0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('past the threshold the check-in says cannot_fetch instead of a healthy idle', async () => {
|
|
54
|
+
const { bodies, api } = recorder();
|
|
55
|
+
const state = runner.newRunnerState();
|
|
56
|
+
const deps = { api, state, claudeAccountLabel: () => null };
|
|
57
|
+
for (let i = 0; i < runner.CANNOT_FETCH_AFTER - 1; i++) await runner.runFetchHead({ run: failing, state });
|
|
58
|
+
await runner.postHeartbeat({ last_event: 'waiting' }, deps);
|
|
59
|
+
assert.equal(bodies.at(-1).last_event, 'waiting', 'a blip below the threshold is not an alarm');
|
|
60
|
+
|
|
61
|
+
await runner.runFetchHead({ run: failing, state });
|
|
62
|
+
for (const ev of ['waiting', 'loop', 'nothing_claimable', undefined]) {
|
|
63
|
+
await runner.postHeartbeat(ev ? { last_event: ev } : {}, deps);
|
|
64
|
+
assert.equal(bodies.at(-1).last_event, 'cannot_fetch', String(ev));
|
|
65
|
+
}
|
|
66
|
+
// What it must never hide: a restart, a setting holding it, or the task in its hands.
|
|
67
|
+
await runner.postHeartbeat({ last_event: 'upgrade_exit' }, deps);
|
|
68
|
+
assert.equal(bodies.at(-1).last_event, 'upgrade_exit');
|
|
69
|
+
await runner.postHeartbeat({ last_event: 'config_idle:no_goals' }, deps);
|
|
70
|
+
assert.equal(bodies.at(-1).last_event, 'config_idle:no_goals');
|
|
71
|
+
await runner.postHeartbeat({ last_event: 'claimed', working_task_id: 7 }, deps);
|
|
72
|
+
assert.equal(bodies.at(-1).last_event, 'claimed');
|
|
73
|
+
|
|
74
|
+
await runner.runFetchHead({ run: working, state });
|
|
75
|
+
await runner.postHeartbeat({ last_event: 'waiting' }, deps);
|
|
76
|
+
assert.equal(bodies.at(-1).last_event, 'waiting', 'one good fetch and the card reads healthy again');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('the host counts its own failed fetches and hands the count to the supervisor it starts', () => {
|
|
80
|
+
const env = {};
|
|
81
|
+
const counter = { fetchFailures: 0 };
|
|
82
|
+
const bad = () => ({ status: 128, stdout: '', stderr: 'fatal: repository not found' });
|
|
83
|
+
const good = () => ({ status: 0, stdout: '', stderr: '' });
|
|
84
|
+
const opts = { git: bad, assert: () => {}, remedy: () => 'switch account', counter, env };
|
|
85
|
+
assert.equal(host.refreshCheckout(() => {}, opts), false);
|
|
86
|
+
assert.equal(host.refreshCheckout(() => {}, opts), false);
|
|
87
|
+
assert.equal(counter.fetchFailures, 2);
|
|
88
|
+
assert.equal(env.AUTOBONGOS_FETCH_FAILURES, '2');
|
|
89
|
+
// The supervisor it starts begins from that count, so the card does not wait
|
|
90
|
+
// for a fresh run of failures after every restart.
|
|
91
|
+
const saved = process.env.AUTOBONGOS_FETCH_FAILURES;
|
|
92
|
+
process.env.AUTOBONGOS_FETCH_FAILURES = env.AUTOBONGOS_FETCH_FAILURES;
|
|
93
|
+
try { assert.equal(runner.newRunnerState().fetchFailures, 2); } finally {
|
|
94
|
+
if (saved === undefined) delete process.env.AUTOBONGOS_FETCH_FAILURES; else process.env.AUTOBONGOS_FETCH_FAILURES = saved;
|
|
95
|
+
}
|
|
96
|
+
assert.equal(host.refreshCheckout(() => {}, { ...opts, git: good }), true);
|
|
97
|
+
assert.equal(counter.fetchFailures, 0);
|
|
98
|
+
assert.equal(env.AUTOBONGOS_FETCH_FAILURES, '0');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('the verdict is cannot_fetch: not ok, and it carries the remedy', () => {
|
|
102
|
+
const d = health.describeRunner({ host: 'mac-mini', mode: 'full', last_event: 'cannot_fetch', last_seen_at: agoS(30) }, { nowMs: NOW });
|
|
103
|
+
assert.equal(d.state, 'cannot_fetch');
|
|
104
|
+
assert.equal(d.ok, false, 'a runner pinned to old code is not healthy');
|
|
105
|
+
assert.match(d.detail, /mac-mini/);
|
|
106
|
+
assert.match(d.remedy, /gh auth switch/);
|
|
107
|
+
// Silence still wins: a runner nobody has heard from cannot vouch for anything.
|
|
108
|
+
assert.equal(health.describeRunner({ host: 'mac-mini', last_event: 'cannot_fetch', last_seen_at: agoS(3 * 3600) }, { nowMs: NOW }).state, 'dead');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("the device's button says it is stuck on old code, and names the fix", () => {
|
|
112
|
+
const fence = { enabled: true, scope: { goals: [1] }, project: { enabled: true, goals: [{ goal_id: 1 }] } };
|
|
113
|
+
const runnerRow = {
|
|
114
|
+
host: 'mac-mini', run_requested: true, run_changed_at: agoS(3600), last_seen_at: agoS(30),
|
|
115
|
+
health: { state: 'cannot_fetch', host: 'mac-mini', age: '30s', remedy: 'Run gh auth switch on it.' },
|
|
116
|
+
};
|
|
117
|
+
const b = RB.buttonState({ runner: runnerRow, fence, nowMs: NOW });
|
|
118
|
+
assert.equal(b.status, 'Stuck on old code');
|
|
119
|
+
assert.equal(b.tone, 'warn');
|
|
120
|
+
assert.equal(b.act, 'stop');
|
|
121
|
+
assert.match(b.sub, /cannot fetch new code/);
|
|
122
|
+
assert.match(b.sub, /gh auth switch/);
|
|
123
|
+
});
|
package/tests/hall_sky_time.mjs
CHANGED
|
@@ -112,8 +112,9 @@ test('the sky tokens are art: no control, no text and no chrome spends one', ()
|
|
|
112
112
|
const sel = m[1].trim();
|
|
113
113
|
if (f === 'style.css' && /(^|\n):root$/.test(sel)) continue; // the token block itself
|
|
114
114
|
// only the two scenes spend them: the City page's frame, stage and cloud; Home's scene, its
|
|
115
|
-
// stage, and (in game mode) the ground of the Home view
|
|
116
|
-
|
|
115
|
+
// stage, and (in game mode) the ground of the Home view; and the cloud scene behind the welcome
|
|
116
|
+
// and the game's start screens (task 1005308)
|
|
117
|
+
if (!/^(\.city-frame|\.city-stage|\.city-cloud|\.city-stars|\.hc-svg|\.hc-cloud|\.hc-stage|\.cloud-scene|html\[data-game="on"\] body\[data-page="home"\]$)/.test(sel)) { hits.push(`${f}: ${sel}`); continue; }
|
|
117
118
|
for (const d of m[2].split(';')) {
|
|
118
119
|
const prop = d.split(':')[0].trim();
|
|
119
120
|
if (/--sky-/.test(d) && (prop === 'color' || /^border|^outline|^caret|^text-/.test(prop))) hits.push(`${f}: ${sel} { ${prop} }`);
|
|
@@ -95,13 +95,14 @@ test('the load screen fills one segment per week, up to the week it is showing (
|
|
|
95
95
|
assert.equal(G.segsDone(0, 0), 0);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
test('a fresh sign-in is handed to the start screen with the
|
|
98
|
+
test('a fresh sign-in is handed to the start screen with the city in the clouds, and the craft picker waits for the world', () => {
|
|
99
99
|
const s = sandbox({ store: { 'hall-welcome-signin': '1' } });
|
|
100
100
|
const access = { authed: true }, builder = { id: 7, display_name: 'Ada' };
|
|
101
101
|
assert.equal(s.window.OTBWelcome.onAccess(access, builder), true, 'the shell is told a screen is up, so it asks no picker');
|
|
102
102
|
assert.equal(s.started.length, 1);
|
|
103
103
|
assert.equal(s.started[0].name, 'Harbor');
|
|
104
|
-
assert.
|
|
104
|
+
assert.equal(typeof s.started[0].scene, 'function', 'the scene is painted, not a picture (task 1005308)');
|
|
105
|
+
assert.equal(s.started[0].art, undefined, 'the circuit-city painting is retired');
|
|
105
106
|
assert.equal(s.store['hall-welcome-signin'], undefined, 'the flag is spent');
|
|
106
107
|
assert.deepEqual(s.asked, [], 'nothing is asked while the start screen is up');
|
|
107
108
|
s.started[0].done();
|
package/tests/welcome_boot.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// tests/welcome_boot.mjs — the welcome's pure parts (tasks 1004785, 1004818).
|
|
2
2
|
//
|
|
3
3
|
// WHY. The welcome decides things a reader would only notice going wrong over days: whether today
|
|
4
|
-
// is the first visit, whether it is day or night
|
|
5
|
-
//
|
|
4
|
+
// is the first visit, whether it is day or night, what the banner names, where each role goes,
|
|
5
|
+
// and that the scene is the city in the clouds with its doors taken out (task 1005308). Those are pure, so this
|
|
6
6
|
// runs the REAL welcome-boot.js (it is Node-require-safe). The overlay itself is
|
|
7
7
|
// tests/welcome_boot_browser.mjs.
|
|
8
8
|
import assert from 'node:assert/strict';
|
|
@@ -39,40 +39,48 @@ test('the greeting follows the local hour, and the name falls back sensibly', ()
|
|
|
39
39
|
assert.equal(W.nameOf({}), 'builder');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
test('day is 06:00 to 17:59 local, night the rest
|
|
42
|
+
test('day is 06:00 to 17:59 local, night the rest (the ink and the veil follow it)', () => {
|
|
43
43
|
const at = (h, m = 0) => W.timeOfDay(new Date(2026, 9, 23, h, m));
|
|
44
44
|
assert.deepEqual([at(5, 59), at(6), at(12), at(17, 59), at(18), at(23, 30), at(0)],
|
|
45
45
|
['night', 'day', 'day', 'day', 'night', 'night', 'night']);
|
|
46
|
-
assert.equal(W.artFor('day'), '/builders/welcome-day.webp');
|
|
47
|
-
assert.equal(W.artFor('night'), '/builders/welcome-night.webp');
|
|
48
46
|
});
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
assert.equal(buf.toString('latin1', 12, 16), 'VP8X', url + ' carries its size');
|
|
58
|
-
const w = 1 + buf.readUIntLE(24, 3), h = 1 + buf.readUIntLE(27, 3);
|
|
59
|
-
assert.deepEqual([w, h], [W.AW, W.AH], url + ' is the painting the lights were placed on');
|
|
48
|
+
// Task 1005308 (owner, 2026-11-14): the circuit-city painting is retired everywhere; the scene is the
|
|
49
|
+
// city in the clouds Home's own painters draw (task 1005196), as a backdrop.
|
|
50
|
+
test('the circuit-city painting is retired: no picture file, no lights pinned to its pixels', () => {
|
|
51
|
+
for (const name of ['welcome-day.webp', 'welcome-night.webp']) assert.equal(fs.existsSync(path.join(HALL, name)), false, name + ' is gone');
|
|
52
|
+
for (const gone of ['ART', 'artFor', 'TOWERS', 'LANES', 'AW', 'AH', 'pulseRuns', 'BEACONS']) assert.equal(W[gone], undefined, gone + ' went with the painting');
|
|
53
|
+
for (const file of ['welcome-boot.js', 'welcome-boot.css', 'shell-game.js', 'shell-game.css', 'index.html']) {
|
|
54
|
+
assert.doesNotMatch(fs.readFileSync(path.join(HALL, file), 'utf8'), /welcome-(day|night)\.webp|wb__img|gs__img|wb-motion/, file + ' names no part of the painting');
|
|
60
55
|
}
|
|
61
56
|
});
|
|
62
57
|
|
|
63
|
-
test(
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
58
|
+
test("the scene is the city in the clouds, drawn by Home's own painters, with every door taken out", async () => {
|
|
59
|
+
const vm = await import('node:vm');
|
|
60
|
+
const ctx = { window: {}, console };
|
|
61
|
+
vm.createContext(ctx);
|
|
62
|
+
vm.runInContext(fs.readFileSync(path.join(HALL, 'city-draw.js'), 'utf8'), ctx);
|
|
63
|
+
const Gov = ctx.window.GovCity;
|
|
64
|
+
const Draw = require(path.join(HALL, 'home-city-draw.js'));
|
|
65
|
+
assert.ok(Gov && Draw, 'the painters load');
|
|
66
|
+
const s = W.sceneShapes(Draw, Gov);
|
|
67
|
+
assert.match(s.viewBox, /^0 0 1000 \d+$/);
|
|
68
|
+
const all = [];
|
|
69
|
+
(function walk(n) { all.push(n); n.kids.forEach(walk); })({ tag: 'root', attrs: {}, kids: s.nodes });
|
|
70
|
+
assert.ok(all.some((n) => /hc-cloud/.test(n.attrs.class || '')), 'it stands in cloud');
|
|
71
|
+
assert.ok(all.filter((n) => n.tag === 'path').length > 50, 'the core and the museum are drawn');
|
|
72
|
+
assert.equal(all.filter((n) => n.tag === 'a').length, 0, 'no link: it is a backdrop');
|
|
73
|
+
for (const n of all) {
|
|
74
|
+
for (const k of ['href', 'tabindex', 'aria-label', 'data-tip', 'data-tip-body', 'data-goal']) assert.equal(n.attrs[k], undefined, k + ' is taken out');
|
|
75
|
+
if (n.attrs.id) assert.ok(n.attrs.id.startsWith(W.SCENE_ID), "its ids never meet Home's own city's");
|
|
76
|
+
for (const v of Object.values(n.attrs)) assert.doesNotMatch(String(v), /url\(#(?!cs-)/, 'every reference points at its own prefixed id');
|
|
77
|
+
assert.doesNotMatch(n.attrs.class || '', /hc-quiet/, 'nothing is faded: the core leads');
|
|
74
78
|
}
|
|
75
|
-
|
|
79
|
+
const json = JSON.stringify(s);
|
|
80
|
+
assert.doesNotMatch(json, /#[0-9a-f]{6}\b/i, "no typed colour: the hour's sky tokens only");
|
|
81
|
+
assert.match(json, /--sky-now-/, "it paints in the hour's sky");
|
|
82
|
+
assert.equal(W.sceneShapes(null, Gov), null, 'no painters: no city, and the sky stands alone');
|
|
83
|
+
assert.equal(W.paintScene(null), false);
|
|
76
84
|
});
|
|
77
85
|
|
|
78
86
|
test('the banner is the logo, named from the branding pack, never a hardcoded word (task 1004953)', () => {
|