@bongos/core 1.24.95 → 1.24.96
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 +56 -36
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +49 -1
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +7 -7
- package/docs/copy-registry.json +7 -7
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +2 -0
- package/docs/page-readings.json +6 -6
- package/modules/autonomy/db.js +48 -0
- package/modules/autonomy/routes/autonomy.js +15 -0
- package/modules/government/catalog.js +1 -1
- package/modules/hall-ui/public/home-city-draw.js +24 -2
- package/modules/hall-ui/public/home-city.css +15 -0
- package/modules/hall-ui/public/home-city.js +3 -1
- package/modules/hall-ui/public/home-runner.js +248 -0
- package/modules/hall-ui/public/index.html +8 -5
- package/modules/hall-ui/public/visor.css +27 -0
- package/modules/hall-ui/public/visor.js +65 -2
- package/modules/hall-ui/records/home-runner.md +19 -0
- package/modules/hall-ui/records/visor.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/hall-preview/server.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/autonomy_runner_goal_stats.mjs +92 -0
- package/tests/hall_visor.mjs +20 -0
- package/tests/home_runner.mjs +115 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Your runner on Home's towers — send it, see it, recall it (task 1005243, goal 1000142)
|
|
2
|
+
|
|
3
|
+
Covers: `home-runner.js` (new), the tower pick and runner marker in `home-city-draw.js` / `home-city.css`, the visor's card (`visor.js` `OTBVisor.focus`, `visor.css` `.visor__focus*`), and the read `GET /api/bongos/autonomy/me/goals/:goalId/stats` (`modules/autonomy`). Game mode only.
|
|
4
|
+
|
|
5
|
+
## What it settled
|
|
6
|
+
|
|
7
|
+
- **The owner's brief (2026-11-12):** putting your Autobongos runner to work should feel like putting a builder on a building in a base-building game, and stay uncluttered. In game mode you tap a tower on Home (a tower is a goal), the visor offers **Send runner here**, and the tower then carries your runner. Tap it again for what it has done there and **Recall runner**.
|
|
8
|
+
- **One tower at a time (owner, 2026-11-12).** Sending sets your goal picks to that one goal (`PUT /autonomy/me/goals`) and switches on any of your devices that are stopped (`POST /autonomy/runners/:host/run`). Recall switches them off and leaves the pick. Several picks made in Settings are "working on several goals": no tower carries the marker until you send it somewhere.
|
|
9
|
+
- **ADR 0403 kept:** the scene only gets picked (the tower is lit and edged in the accent); the words and the one button are the visor's. `OTBVisor.focus({ title, status, lines, lead: { label, run }, onClose })` is the general mechanism: a scene's pick puts a small card on the glass and its action takes the accent button; Escape (or the card's Esc mark, or a tap on open ground) closes it before the options menu sees the press, and the character's own main action comes back. This answers ADR 0403 §4's open question for one action.
|
|
10
|
+
- **Who sees it:** only a viewer the server says may run a runner (`GET /autonomy/fence` → `scope.may_run`, the `autonomy.run` atom). Signed out, without the atom, or in classic mode a tower stays the plain link to its goal and nothing is new.
|
|
11
|
+
- **The marker** is the agent's robot face in the accent colour (yours, not anyone's agent), ringed by its state: working (accent ring that turns, CSS only, stilled by the hall's reduced-motion rule), starting, resting (dotted info ring), ready (soft ring), recalled (faded). It replaces the agent face `/home/city` would draw on that tower. Tapping it picks its tower.
|
|
12
|
+
- **The numbers are the ledger's**, never the runner's own report: tasks the caller's runner shipped in that goal (`claims.autonomous` + outcome `shipped`), the tokens their sessions took (`session_records.total_tokens`), their average length, and the goal's `ready`/`active` tasks. Time left is that count × the average and is shown only when there is an average ("about 3 h"); with no runner history there it promises no time. The device's face (Running, Resting until 12:31 · 42 min, Device is off…) is `runner-button.js` `buttonState`, the same decision the Settings card draws.
|
|
13
|
+
- **Not yet:** the percent of your Claude usage limit. That number lives only on the runner's PC; task 1005244 has the runner report it in its check-in.
|
|
14
|
+
|
|
15
|
+
## Looking at it
|
|
16
|
+
|
|
17
|
+
`hall-preview-fixture` in `.claude/launch.json` (port 4602, `--fixture-me`; the plain preview borrows the live `/me`, whose character may not be the Artisan). Set `localStorage hall-game=on`, `OTBCraftMode.setCharacter('artisan')`, and edit `autonomy__fence.json` / `autonomy__runners.json` to a project that allows goals (the checked-in fence has the switch off and no goals, which renders "Closed to runners" — a real state). The harness reads fixtures per request, so `OTBHomeRunner.refresh()` picks a change up without a restart. Fixtures added: `autonomy__me__goals__stats.json`, `autonomy__me__goals.put.json`.
|
|
18
|
+
|
|
19
|
+
Tests: `tests/home_runner.mjs` (the card and marker decisions, run for real), `tests/autonomy_runner_goal_stats.mjs` (the read and route), `tests/hall_visor.mjs` (the card on the glass, its button, Escape order).
|
|
@@ -42,6 +42,10 @@ The glass was redrawn to the owner's mock-up "Cloud city: in the visor, minimal"
|
|
|
42
42
|
- **Scenes.** The top arc, the notch and the place sit inside the top 64px (`--visor-arc`; the segments under them take the inset to 88px, `--visor-inset-top`), so no scene's top row is struck through. Home no longer shifts its greeting row left (there is no pill top-right to clear). The city's door rises above the foot row at every width, because the accent button is centred where the door stood. The sky's `visor-on` offsets are unchanged.
|
|
43
43
|
- **Looking at it.** The stand-in scene in `tests/hall_visor.mjs` over the hall's real sheets; put a bright gradient behind it to check midday.
|
|
44
44
|
|
|
45
|
+
## A scene's pick on the glass (task 1005243)
|
|
46
|
+
|
|
47
|
+
`OTBVisor.focus({ key, title, status: { text, tone }, lines, lead: { label, run }, onClose })` puts a small card above the accent button (the night panel between two strong hairlines, like the sheet) and hands `lead` the accent button while it is up; `focus(null)` takes both away. Escape closes it in capture before the options menu, but never while the sheet is open. First user: Home's towers and your runner (`records/home-runner.md`).
|
|
48
|
+
|
|
45
49
|
## What it settled (task 1005148)
|
|
46
50
|
|
|
47
51
|
- **The first look (2026-11-10, five mock-up rounds; replaced by the minimal visor above).** A quiet layout with "hairline instruments" from the helmet round: thin curved visor lines across the top, bottom and both sides, a small centre cross, and a compass of nine slanted cells along the top line with the one salmon marker under its centre. No tracker or radar (tried and dropped), no clipped corners: the pills stay round (DESIGN.md "pill or nothing"). No glow (DESIGN.md's rejected near-black-plus-neon HUD). The frame is one SVG in a 1000×1000 box stretched over the stage with non-scaling strokes, so it is a hairline at every size.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.96",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.96",
|
|
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.24.
|
|
3
|
+
"version": "1.24.96",
|
|
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
|
@@ -11683,5 +11683,11 @@
|
|
|
11683
11683
|
"id": "1005218",
|
|
11684
11684
|
"text": "The three characters have a new look: space-faring aliens made of two animals each, drawn at a larger size. Each has its own colour, and in the game the visor now takes your character's colour and shows your character wearing"
|
|
11685
11685
|
}
|
|
11686
|
+
],
|
|
11687
|
+
"1.24.96": [
|
|
11688
|
+
{
|
|
11689
|
+
"id": "1005243",
|
|
11690
|
+
"text": "In game mode, tap a goal tower on Home to send your Autobongos runner there with one visor button; the tower then shows your runner at work, and tapping it shows tasks done, tokens used, time left and a Recall button."
|
|
11691
|
+
}
|
|
11686
11692
|
]
|
|
11687
11693
|
}
|
|
@@ -112,6 +112,8 @@ let liveWhy = null;
|
|
|
112
112
|
// me__recent-ships.json. Query strings are ignored when matching. Id-bearing
|
|
113
113
|
// paths fall back to the generic names below so one fixture serves any id.
|
|
114
114
|
const FIXTURE_FALLBACKS = [
|
|
115
|
+
// Home's runner card (task 1005243): one goal's runner numbers, the same for every goal
|
|
116
|
+
[/^autonomy\/me\/goals\/\d+\/stats$/, 'autonomy__me__goals__stats'],
|
|
115
117
|
[/^goals\/\d+\/conflicts$/, 'goals__conflicts'],
|
|
116
118
|
[/^goals\/\d+\/dependency-forest$/, 'goals__dependency-forest'],
|
|
117
119
|
[/^goals\/\d+\/requests$/, 'goals__requests'],
|
|
@@ -221,6 +223,8 @@ const CANNED_WRITES = [
|
|
|
221
223
|
['POST', /^auth\/runner-code\/issue$/, 'auth__runner-code__issue.post'],
|
|
222
224
|
['POST', /^me\/sessions\/revoke$/, 'me__sessions__revoke.post'],
|
|
223
225
|
['DELETE', /^autonomy\/runners\/[^/]+$/, 'autonomy__runners.delete'],
|
|
226
|
+
// Home's runner card (task 1005243): Send runner here writes the one pick.
|
|
227
|
+
['PUT', /^autonomy\/me\/goals$/, 'autonomy__me__goals.put'],
|
|
224
228
|
];
|
|
225
229
|
|
|
226
230
|
// A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
|
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.24.
|
|
84
|
+
const CORE_VERSION = '1.24.96'; // 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,92 @@
|
|
|
1
|
+
// tests/autonomy_runner_goal_stats.mjs — one goal, seen from the caller's runner
|
|
2
|
+
// (task 1005243): the numbers the game's runner card on Home shows.
|
|
3
|
+
//
|
|
4
|
+
// The route is driven as a REAL request through the assembled router with only the
|
|
5
|
+
// auth gates stubbed, and the SQL against a stubbed pool (the
|
|
6
|
+
// autonomy_builder_scope precedent). DB-free, no network.
|
|
7
|
+
// Run: node --test tests/autonomy_runner_goal_stats.mjs
|
|
8
|
+
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const req = createRequire(import.meta.url);
|
|
17
|
+
const express = req('express');
|
|
18
|
+
const { attachFail } = req(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
|
|
19
|
+
const api = req(path.join(ROOT, 'src/module-api.js'));
|
|
20
|
+
const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
|
|
21
|
+
|
|
22
|
+
const permStub = (key) => { const mw = (_q, _s, next) => next(); mw.permKey = key; return mw; };
|
|
23
|
+
const passBuilder = (q, _s, next) => { q.builder = { id: 7, rank: 'metic' }; next(); };
|
|
24
|
+
|
|
25
|
+
function router() {
|
|
26
|
+
const saved = { b: api.requireBuilder, p: api.requirePermission };
|
|
27
|
+
api.requireBuilder = passBuilder;
|
|
28
|
+
api.requirePermission = permStub;
|
|
29
|
+
const p = req.resolve(path.join(ROOT, 'modules/autonomy/routes/autonomy.js'));
|
|
30
|
+
delete req.cache[p];
|
|
31
|
+
try { return req(p)(); } finally { api.requireBuilder = saved.b; api.requirePermission = saved.p; }
|
|
32
|
+
}
|
|
33
|
+
async function serve(r) {
|
|
34
|
+
const app = express();
|
|
35
|
+
app.use(express.json()); app.use(attachFail); app.use(r);
|
|
36
|
+
const server = app.listen(0);
|
|
37
|
+
await new Promise((ok) => server.once('listening', ok));
|
|
38
|
+
return { base: `http://127.0.0.1:${server.address().port}`, close: () => server.close() };
|
|
39
|
+
}
|
|
40
|
+
async function withPool(answer, fn) {
|
|
41
|
+
const saved = api.pool.query;
|
|
42
|
+
const calls = [];
|
|
43
|
+
api.pool.query = async (sql, params) => { calls.push({ sql, params }); return answer(sql, params); };
|
|
44
|
+
try { return await fn(calls); } finally { api.pool.query = saved; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('the read counts only the caller\'s RUNNER claims that shipped in that goal, and their sessions\' tokens', async () => {
|
|
48
|
+
await withPool(() => ({ rows: [{ tasks_done: 7, tokens: '4820000', avg_s: '2280.4', tasks_left: 5 }] }), async (calls) => {
|
|
49
|
+
const out = await db.readRunnerGoalStats(7, 1000090);
|
|
50
|
+
assert.deepEqual(out, { tasks_done: 7, tokens: 4820000, avg_task_seconds: 2280, tasks_left: 5 });
|
|
51
|
+
const { sql, params } = calls[0];
|
|
52
|
+
assert.deepEqual(params, [7, 1000090], 'scoped to the caller and the goal');
|
|
53
|
+
assert.match(sql, /c\.autonomous/, 'a runner\'s claim, not a person\'s');
|
|
54
|
+
assert.match(sql, /c\.outcome = 'shipped'/, 'shipped, by the ledger');
|
|
55
|
+
assert.match(sql, /sr\.builder_id = \$1/, 'only the caller\'s sessions');
|
|
56
|
+
assert.match(sql, /status IN \('ready', 'active'\)/, 'what is left is what a runner could still take');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('a goal not opened to runners tells no count of what is left', async () => {
|
|
61
|
+
await withPool(() => ({ rows: [{ tasks_done: 0, tokens: '0', avg_s: null, tasks_left: null }] }), async () => {
|
|
62
|
+
assert.equal((await db.readRunnerGoalStats(7, 9)).tasks_left, null);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('nothing done yet reads as zeros and no average, never a made-up pace', async () => {
|
|
67
|
+
await withPool(() => ({ rows: [{ tasks_done: 0, tokens: '0', avg_s: null, tasks_left: 3 }] }), async () => {
|
|
68
|
+
assert.deepEqual(await db.readRunnerGoalStats(7, 1), { tasks_done: 0, tokens: 0, avg_task_seconds: null, tasks_left: 3 });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('GET /autonomy/me/goals/:id/stats: behind autonomy.run, the caller\'s own, a bad id refused', async () => {
|
|
73
|
+
const r = router();
|
|
74
|
+
const layer = r.stack.find((l) => l.route && l.route.path === '/autonomy/me/goals/:goalId/stats' && l.route.methods.get);
|
|
75
|
+
assert.ok(layer, 'the route is registered');
|
|
76
|
+
assert.deepEqual(layer.route.stack.map((s) => s.handle.permKey).filter(Boolean), ['autonomy.run']);
|
|
77
|
+
const s = await serve(r);
|
|
78
|
+
try {
|
|
79
|
+
await withPool(() => ({ rows: [{ tasks_done: 2, tokens: 1000, avg_s: 600, tasks_left: 1 }] }), async (calls) => {
|
|
80
|
+
const ok = await fetch(`${s.base}/autonomy/me/goals/1000090/stats`);
|
|
81
|
+
assert.equal(ok.status, 200);
|
|
82
|
+
assert.deepEqual(await ok.json(), { goal_id: 1000090, tasks_done: 2, tokens: 1000, avg_task_seconds: 600, tasks_left: 1 });
|
|
83
|
+
assert.deepEqual(calls[0].params, [7, 1000090], 'keyed by the session\'s builder, never the request');
|
|
84
|
+
const bad = await fetch(`${s.base}/autonomy/me/goals/abc/stats`);
|
|
85
|
+
assert.equal(bad.status, 400);
|
|
86
|
+
});
|
|
87
|
+
await withPool(() => { throw new Error('db down'); }, async () => {
|
|
88
|
+
const down = await fetch(`${s.base}/autonomy/me/goals/5/stats`);
|
|
89
|
+
assert.equal(down.status, 503);
|
|
90
|
+
});
|
|
91
|
+
} finally { s.close(); }
|
|
92
|
+
});
|
package/tests/hall_visor.mjs
CHANGED
|
@@ -271,6 +271,26 @@ test('Escape closes one layer, top first: the sheet, then the scene\'s pick, the
|
|
|
271
271
|
await ctx.close();
|
|
272
272
|
});
|
|
273
273
|
|
|
274
|
+
test('a scene\'s pick: the card is on the glass, its action takes the accent button, Escape closes it first (task 1005243)', { skip }, async () => {
|
|
275
|
+
const { ctx, page, errors } = await open();
|
|
276
|
+
await page.evaluate(() => OTBVisor.focus({ key: 'runner:2', title: 'Area 2', status: { text: 'Running', tone: 'ok' }, lines: ['7 tasks done here'],
|
|
277
|
+
lead: { label: 'Recall runner', run() { window.led = (window.led || 0) + 1; } }, onClose() { window.unpicked = true; } }));
|
|
278
|
+
const g = await page.evaluate(() => ({
|
|
279
|
+
title: document.querySelector('.visor__focus-title').textContent,
|
|
280
|
+
status: document.querySelector('.visor__focus-status').textContent,
|
|
281
|
+
main: document.querySelector('.visor__main').textContent,
|
|
282
|
+
takes: getComputedStyle(document.querySelector('.visor__focus')).pointerEvents,
|
|
283
|
+
}));
|
|
284
|
+
assert.deepEqual(g, { title: 'Area 2', status: 'Running', main: 'Recall runner', takes: 'auto' });
|
|
285
|
+
await page.click('[data-visor-lead]');
|
|
286
|
+
assert.equal(await page.evaluate(() => window.led), 1, 'the accent button runs the scene\'s action');
|
|
287
|
+
await page.keyboard.press('Escape');
|
|
288
|
+
const s = await page.evaluate(() => ({ focus: OTBVisor.focused(), unpicked: window.unpicked, picked: window.picked, menu: window.optionsOpened, main: document.querySelector('.visor__main').textContent }));
|
|
289
|
+
assert.deepEqual(s, { focus: null, unpicked: true, picked: true, menu: 0, main: 'Goal skyG' }, 'the card took the press; the character\'s own button is back');
|
|
290
|
+
assert.deepEqual(errors, []);
|
|
291
|
+
await ctx.close();
|
|
292
|
+
});
|
|
293
|
+
|
|
274
294
|
test('a lead scene action is the first key hint, and its click and its key run it (task 1005209)', { skip }, async () => {
|
|
275
295
|
const { ctx, page, errors } = await open();
|
|
276
296
|
await page.evaluate(() => OTBVisor.scene({ name: 'test', place: 'Test city', actions: [{ label: 'Decide 2 in a row', key: 'd', bind: true, lead: true, run() { window.decided = (window.decided || 0) + 1; } }] }));
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// tests/home_runner.mjs — your Autobongos runner on Home's city (task 1005243).
|
|
2
|
+
//
|
|
3
|
+
// The decision is pure (home-runner.js cardFor / stateOf), so it runs here for
|
|
4
|
+
// real against the same runner-button.js the Settings card draws from. The page
|
|
5
|
+
// wiring is pinned by its source: the scene paints the pick, every button is the
|
|
6
|
+
// visor's (ADR 0403), and only a viewer the server says may run gets any of it.
|
|
7
|
+
// Run: node --test tests/home_runner.mjs
|
|
8
|
+
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const R = require(path.join(HALL, 'home-runner.js'));
|
|
20
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
21
|
+
|
|
22
|
+
const NOW = Date.parse('2026-11-12T10:00:00Z');
|
|
23
|
+
const seen = new Date(NOW - 20000).toISOString();
|
|
24
|
+
const fenceOf = (picks, allowed = [1, 2, 3], extra = {}) => ({ enabled: true, goals: picks, scope: { may_run: true, goals: picks }, project: { enabled: true, goals: allowed }, ...extra });
|
|
25
|
+
const dev = (o = {}) => ({ host: 'pc', run_requested: true, run_changed_at: new Date(NOW - 3600e3).toISOString(), last_seen_at: seen, health: { state: 'alive', workingTaskId: null }, ...o });
|
|
26
|
+
const STATS = { tasks_done: 7, tokens: 4820000, avg_task_seconds: 2280, tasks_left: 5 };
|
|
27
|
+
const card = (o) => R.cardFor({ goal: { id: 2, title: 'Area 2' }, nowMs: NOW, titleOf: (g) => `Area ${g}`, stats: STATS, ...o });
|
|
28
|
+
|
|
29
|
+
test('no permission to run, or no fence read: no card, the tower stays a link', () => {
|
|
30
|
+
assert.equal(card({ runners: [dev()], fence: null }), null);
|
|
31
|
+
assert.equal(card({ runners: [dev()], fence: { ...fenceOf([]), scope: { may_run: false, goals: [] } } }), null);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('no runner set up yet: the card offers setting one up', () => {
|
|
35
|
+
const c = card({ runners: [], fence: fenceOf([]) });
|
|
36
|
+
assert.equal(c.lead.act, 'setup');
|
|
37
|
+
assert.equal(c.status.text, 'No runner yet');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('a goal the project has not opened to runners: no send, only the goal', () => {
|
|
41
|
+
const c = card({ runners: [dev()], fence: fenceOf([], [1, 3]) });
|
|
42
|
+
assert.equal(c.lead.act, 'open');
|
|
43
|
+
assert.match(c.lines[0], /not opened this goal to runners/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('your runner is elsewhere or resting: Send runner here, with what it did here and what is left', () => {
|
|
47
|
+
const away = card({ runners: [dev()], fence: fenceOf([1]) });
|
|
48
|
+
assert.deepEqual(away.lead, { label: 'Send runner here', act: 'send' });
|
|
49
|
+
assert.equal(away.status.text, 'Your runner is at Area 1');
|
|
50
|
+
assert.deepEqual(away.lines, ['7 tasks done here · 4.8M tokens', '5 tasks ready · about 3 h for your runner']);
|
|
51
|
+
const off = card({ runners: [dev({ run_requested: false })], fence: fenceOf([]) });
|
|
52
|
+
assert.equal(off.status.text, 'Your runner is resting');
|
|
53
|
+
assert.equal(off.lead.act, 'send');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('your runner is here and on: its device face, the numbers, and Recall runner', () => {
|
|
57
|
+
const work = card({ runners: [dev({ health: { state: 'alive', workingTaskId: '9' } })], fence: fenceOf([2]) });
|
|
58
|
+
assert.deepEqual(work.lead, { label: 'Recall runner', act: 'recall' });
|
|
59
|
+
assert.deepEqual([work.status.text, work.status.tone], ['Running', 'ok']);
|
|
60
|
+
assert.equal(work.lines[0], 'Working on task 9.');
|
|
61
|
+
const rest = card({ runners: [dev({ health: { state: 'alive', workingTaskId: null, holdUntil: new Date(NOW + 42 * 60e3).toISOString() } })], fence: fenceOf([2]) });
|
|
62
|
+
assert.match(rest.status.text, /^Resting until .+ · 42 min$/);
|
|
63
|
+
assert.equal(rest.lead.act, 'recall');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('recalled here: it says so and offers to send it back', () => {
|
|
67
|
+
const c = card({ runners: [dev({ run_requested: false })], fence: fenceOf([2]) });
|
|
68
|
+
assert.equal(c.status.text, 'Recalled');
|
|
69
|
+
assert.equal(c.lead.act, 'send');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('nothing done yet: no invented numbers; nothing ready: says so', () => {
|
|
73
|
+
const c = card({ runners: [dev()], fence: fenceOf([1]), stats: { tasks_done: 0, tokens: 0, avg_task_seconds: null, tasks_left: 0 } });
|
|
74
|
+
assert.deepEqual(c.lines, ['Nothing ready to build here right now.']);
|
|
75
|
+
const noPace = card({ runners: [dev()], fence: fenceOf([1]), stats: { tasks_done: 0, tokens: 0, avg_task_seconds: null, tasks_left: 4 } });
|
|
76
|
+
assert.deepEqual(noPace.lines, ['4 tasks ready'], 'no time is promised without a pace to measure it by');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('the marker\'s state follows the device; several picks are not one tower', () => {
|
|
80
|
+
assert.equal(R.stateOf({ runners: [dev({ health: { state: 'alive', workingTaskId: '9' } })], fence: fenceOf([2]), nowMs: NOW }), 'working');
|
|
81
|
+
assert.equal(R.stateOf({ runners: [dev({ run_requested: false })], fence: fenceOf([2]), nowMs: NOW }), 'off');
|
|
82
|
+
assert.equal(R.stateOf({ runners: [dev({ health: { state: 'alive', holdUntil: new Date(NOW + 60e3).toISOString() } })], fence: fenceOf([2]), nowMs: NOW }), 'resting');
|
|
83
|
+
assert.equal(R.assignedGoal(fenceOf([2])), '2');
|
|
84
|
+
assert.equal(R.assignedGoal(fenceOf([1, 2])), null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('numbers read like a person would say them', () => {
|
|
88
|
+
assert.equal(R.tokensText(4820000), '4.8M tokens');
|
|
89
|
+
assert.equal(R.tokensText(340400), '340k tokens');
|
|
90
|
+
assert.equal(R.aboutText(5 * 2280), 'about 3 h');
|
|
91
|
+
assert.equal(R.aboutText(1200), 'about 20 min');
|
|
92
|
+
assert.equal(R.aboutText(3 * 86400), 'about 3 days');
|
|
93
|
+
assert.equal(R.aboutText(0), '');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('source: the scene paints the pick, the visor holds every button (ADR 0403), the writes are Settings\' routes', () => {
|
|
97
|
+
const src = read('home-runner.js');
|
|
98
|
+
assert.match(src, /OTBVisor\.focus/, 'the card and its button are the visor\'s');
|
|
99
|
+
assert.doesNotMatch(src, /createElement\(['"]button/, 'no button is drawn in the scene');
|
|
100
|
+
assert.match(src, /scope\.may_run === true/, 'only a viewer the server says may run');
|
|
101
|
+
assert.match(src, /dataset\.game === 'on'/, 'game mode only: in classic a tower stays a link');
|
|
102
|
+
for (const route of ["'PUT', '/autonomy/me/goals'", '/autonomy/runners/${encodeURIComponent(r.host)}/run', '/autonomy/me/goals/${encodeURIComponent(picked)}/stats']) assert.ok(src.includes(route), route);
|
|
103
|
+
const draw = read('home-city-draw.js');
|
|
104
|
+
assert.match(draw, /'data-goal': a\.id/, 'every tower names its goal');
|
|
105
|
+
assert.match(draw, /hc-tower--picked/);
|
|
106
|
+
const html = read('index.html');
|
|
107
|
+
const at = (f) => html.indexOf(`/builders/${f}`);
|
|
108
|
+
assert.ok(at('runner-button.js') > 0 && at('runner-button.js') < at('home-runner.js') && at('home-runner.js') < at('home-city.js'), 'runner-button.js, then home-runner.js, before home-city.js');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('source: the marker moves only in CSS, and the hall\'s reduced-motion rule stills it', () => {
|
|
112
|
+
const css = read('home-city.css');
|
|
113
|
+
assert.match(css, /\.hc-runner--working \.hc-runner__ring \{[^}]*animation: hc-turn/);
|
|
114
|
+
assert.doesNotMatch(read('home-runner.js'), /requestAnimationFrame|setInterval/, 'no motion in script');
|
|
115
|
+
});
|