@bongos/core 1.24.28 → 1.24.30
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 +100 -70
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +36 -1
- package/docs/api-reference.md +7 -1
- package/docs/copy-inventory.md +247 -280
- package/docs/copy-registry.json +381 -691
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +5 -10
- package/docs/page-readings.json +327 -393
- package/modules/builder-settings/interaction-prefs.js +2 -2
- package/modules/government/db.js +29 -0
- package/modules/government/docket.js +61 -4
- package/modules/government/routes/government.js +2 -0
- package/modules/hall-ui/public/builders.js +3 -79
- package/modules/hall-ui/public/hall-render.js +6 -81
- package/modules/hall-ui/public/hall-widgets.js +4 -45
- package/modules/hall-ui/public/home-city-draw.js +275 -0
- package/modules/hall-ui/public/home-city.css +66 -0
- package/modules/hall-ui/public/home-city.js +223 -0
- package/modules/hall-ui/public/index.html +5 -6
- package/modules/hall-ui/public/index.states.json +73 -8
- package/modules/hall-ui/public/my-agents.js +35 -2
- package/modules/hall-ui/public/palette.js +8 -0
- package/modules/hall-ui/public/settings-moved.js +41 -0
- package/modules/hall-ui/public/settings.css +4 -28
- package/modules/hall-ui/public/settings.html +19 -107
- package/modules/hall-ui/public/settings.js +20 -215
- package/modules/hall-ui/public/settings.states.json +2 -4
- package/modules/hall-ui/records/home-city.md +66 -0
- package/modules/hall-ui/records/my-agents.md +3 -2
- package/modules/hall-ui/records/panel-family.md +2 -0
- package/modules/lifecycle/home-city.js +340 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/home-city.js +29 -0
- package/modules/lifecycle/ship-card.js +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/module-api.js +1 -1
- package/tests/claim_from_task_and_home.mjs +8 -43
- package/tests/government_tidy_up.mjs +21 -24
- package/tests/hall_board_world.mjs +10 -12
- package/tests/hall_home_ships_bounded.mjs +4 -3
- package/tests/hall_my_agents.mjs +88 -2
- package/tests/hall_palette.mjs +12 -0
- package/tests/hall_settings_world.mjs +64 -100
- package/tests/helpers.mjs +6 -5
- package/tests/home_bounded_reads.mjs +16 -31
- package/tests/home_city.mjs +385 -0
- package/tests/home_city_widget.mjs +120 -0
- package/tests/interaction_posture.mjs +3 -1
- package/tests/leaderboard_roles.mjs +3 -2
- package/tests/speciality_offer_ui.mjs +6 -5
- package/tests/speciality_walkthrough_ui.mjs +7 -5
- package/modules/hall-ui/public/settings-interaction.js +0 -132
- package/modules/hall-ui/public/settings-law-locks.js +0 -53
- package/modules/hall-ui/public/settings-render.js +0 -138
|
@@ -122,14 +122,15 @@ function loadRender({ view = 'home' } = {}) {
|
|
|
122
122
|
|
|
123
123
|
const homePage = (total, firstId = 3000) => ({ ships: [ship(firstId), ship(firstId - 1)], total });
|
|
124
124
|
|
|
125
|
-
test('Home: the
|
|
125
|
+
test('Home: the Standing chip\'s count comes from the small page, with no ledger fetch', async () => {
|
|
126
|
+
// task 1005054: the Shipped tile and the five-row table left Home with the stats
|
|
127
|
+
// strip and "Your work"; the small page still feeds the Standing chip's count.
|
|
126
128
|
const { r, el, fetched } = loadRender();
|
|
127
129
|
assert.equal(r.HOME_SHIPS_LIMIT, 5);
|
|
128
130
|
r.setRecentShips(homePage(3000));
|
|
129
131
|
await new Promise((done) => setTimeout(done, 0));
|
|
130
132
|
assert.deepEqual(fetched, [], 'Home never asks for the full history');
|
|
131
|
-
assert.equal(el('
|
|
132
|
-
assert.match(el('home-recent-table tbody').innerHTML, /#3000[\s\S]*#2999/);
|
|
133
|
+
assert.equal(el('standing-shipped').textContent, '3000', 'the chip shows the server total, not rows held');
|
|
133
134
|
assert.equal(el('recent-ships tbody').innerHTML, '', 'the ledger is untouched until its view opens');
|
|
134
135
|
});
|
|
135
136
|
|
package/tests/hall_my_agents.mjs
CHANGED
|
@@ -556,10 +556,11 @@ test('DEFAULTS: the real shapers take the way back for a skill voice', () => {
|
|
|
556
556
|
assert.equal(row(viewOf({ skills: back.body }), 'skills').skills[0].options[0].pressed, true);
|
|
557
557
|
});
|
|
558
558
|
|
|
559
|
-
test('PAGE: My agents mounts the
|
|
559
|
+
test('PAGE: My agents mounts the speciality component, in a section of its own, and is its only host (task 1005044)', () => {
|
|
560
560
|
const html = read('my-agents.html');
|
|
561
561
|
assert.ok(!fs.existsSync(path.join(HALL, 'settings-specialities.js')), 'one copy: the Settings file was carved into the shared one');
|
|
562
|
-
|
|
562
|
+
assert.match(read('my-agents.html'), /src="\/builders\/speciality-tools\.js/, 'My agents loads it');
|
|
563
|
+
assert.doesNotMatch(read('settings.html'), /speciality-tools\.(js|css)/, 'Settings no longer mounts it: one page edits a speciality');
|
|
563
564
|
const at = html.indexOf('id="specialities-scroll"');
|
|
564
565
|
assert.ok(at > 0, 'the section is on the page');
|
|
565
566
|
assert.match(html.slice(html.lastIndexOf('<section', at), at), /class="scroll ma-know"/, 'a panel of its own');
|
|
@@ -582,3 +583,88 @@ test('PAGE: when a speciality changes, what your agent reads first is read again
|
|
|
582
583
|
assert.match(code, /document\.addEventListener\('otb:specialities-changed', /);
|
|
583
584
|
assert.match(codeOf(read('speciality-tools.js')), /new CustomEvent\('otb:specialities-changed'\)/);
|
|
584
585
|
});
|
|
586
|
+
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
// 6. moved from Settings (task 1005044): the five agent settings live HERE only.
|
|
589
|
+
// Each case is a Settings assertion carried over — what the old panel promised a
|
|
590
|
+
// builder, proven on this page instead (tests/hall_settings_world.mjs pins that
|
|
591
|
+
// Settings no longer carries them).
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
test('MOVED: every interaction field is a row offering every value, written to PATCH /me/interaction', () => {
|
|
595
|
+
const v = viewOf();
|
|
596
|
+
for (const f of fx('me').interaction.fields) {
|
|
597
|
+
const r = row(v, f.field);
|
|
598
|
+
assert.ok(r, f.field + ' is a row');
|
|
599
|
+
assert.deepEqual(r.options.map((o) => o.value), f.values.map((x) => x.value), f.field + ': every value the old radio group offered');
|
|
600
|
+
for (const o of r.options) {
|
|
601
|
+
const w = MyAgents.writeFor(f.field, o.value, {});
|
|
602
|
+
assert.deepEqual(w, { method: 'PATCH', path: '/me/interaction', body: { [f.field]: o.value }, slice: 'interaction' }, 'one field per write, as Settings sent');
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test('MOVED: a default value is named once — a chip never repeats the API label\'s "(default)"', () => {
|
|
608
|
+
for (const r of rows(viewOf())) for (const o of r.options || []) assert.doesNotMatch(String(o.label), /\(default\)|DEFAULT/, r.key + ': ' + o.label);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test('MOVED: wandering offers every level the rank allows, writes its number, and never a level above the ceiling', () => {
|
|
612
|
+
const me = JSON.parse(JSON.stringify(fx('me')));
|
|
613
|
+
me.wandering.levels[3].allowed = false; // a rank whose ceiling is balanced
|
|
614
|
+
const r = row(viewOf({ me }), 'wandering');
|
|
615
|
+
assert.deepEqual(r.options.map((o) => o.value), ['locked', 'focused', 'balanced'], 'the level above the rank ceiling is not offered');
|
|
616
|
+
me.wandering.levels.forEach((l) => {
|
|
617
|
+
assert.deepEqual(MyAgents.writeFor('wandering', l.key, { me }).body, { wandering: l.n }, l.key + ' writes its level number, as Settings did');
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test('MOVED: following the profile names the level it derives, and choosing it clears the pick', () => {
|
|
622
|
+
const me = JSON.parse(JSON.stringify(fx('me')));
|
|
623
|
+
me.render.override_level = null; me.render.derived_level = 'text'; me.render.level = 'text';
|
|
624
|
+
const r = row(viewOf({ me }), 'render');
|
|
625
|
+
assert.equal(r.fact, 'plain text', 'the fact is the level the profile derives, not a bare "default"');
|
|
626
|
+
assert.equal(r.note, 'follows your profile');
|
|
627
|
+
assert.equal(r.options[0].pressed, true);
|
|
628
|
+
assert.deepEqual(MyAgents.writeFor('render', '', {}).body, { widgets: null }, 'the way back clears the override rather than pinning today\'s level');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test('MOVED: a law lock on these rows still holds here — a fixed setting is locked, a cap offers only what it allows, an unenforced lock locks nothing', () => {
|
|
632
|
+
const me = JSON.parse(JSON.stringify(fx('me')));
|
|
633
|
+
me.laws = { inject: true, contract: 'Project laws', laws: [], locks: [
|
|
634
|
+
{ setting: 'verbosity', law_id: '2', law_no: 2, law_text: 'Agents keep replies short', value: 'concise', enforced: true, allowed: ['concise'] },
|
|
635
|
+
{ setting: 'wandering', law_id: '3', law_no: 3, law_text: 'Agents stay on task', max: 'focused', enforced: true, allowed: ['locked', 'focused'] },
|
|
636
|
+
{ setting: 'machinery', law_id: '4', law_no: 4, value: 'nonsense', enforced: false, reason: 'value_not_allowed' },
|
|
637
|
+
] };
|
|
638
|
+
const v = viewOf({ me });
|
|
639
|
+
assert.equal(row(v, 'verbosity').locked, true);
|
|
640
|
+
assert.match(row(v, 'verbosity').lock.text, /Agents keep replies short · Law 2$/);
|
|
641
|
+
assert.equal(row(v, 'wandering').locked, false, 'a cap is not a lock');
|
|
642
|
+
assert.deepEqual(row(v, 'wandering').options.map((o) => o.value), ['locked', 'focused']);
|
|
643
|
+
assert.equal(row(v, 'machinery').locked, false, 'a lock that enforces nothing locks nothing');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('MOVED: each skill voice offers default plus every allowed model, written to PATCH /me/skill-prefs', () => {
|
|
647
|
+
const sk = fx('me__skill-prefs');
|
|
648
|
+
const r = row(viewOf(), 'skills');
|
|
649
|
+
assert.equal(r.skills.length, sk.effective.length, 'every skill Settings listed');
|
|
650
|
+
for (const s of r.skills) assert.deepEqual(s.options.map((o) => o.value), [''].concat(sk.allowed_models));
|
|
651
|
+
assert.deepEqual(MyAgents.writeFor('skill.x', 'opus', {}), { method: 'PATCH', path: '/me/skill-prefs', body: { x: 'opus' }, slice: 'skills' });
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test('ANCHORS: every My agents anchor names a group this page draws, and #skills opens the per-skill list', () => {
|
|
655
|
+
const groups = MyAgents.GROUPS.map((g) => g.key);
|
|
656
|
+
for (const [hash, a] of Object.entries(MyAgents.ANCHORS)) {
|
|
657
|
+
if (a.group) assert.ok(groups.includes(a.group), hash + ' lands on a real group');
|
|
658
|
+
}
|
|
659
|
+
assert.deepEqual(MyAgents.anchorFor('#talk'), { id: 'ma-group-talk', openSkills: false });
|
|
660
|
+
assert.deepEqual(MyAgents.anchorFor('#work'), { id: 'ma-group-work', openSkills: false });
|
|
661
|
+
assert.deepEqual(MyAgents.anchorFor('#models'), { id: 'ma-group-models', openSkills: false });
|
|
662
|
+
assert.deepEqual(MyAgents.anchorFor('#skills'), { id: 'ma-group-models', openSkills: true });
|
|
663
|
+
assert.deepEqual(MyAgents.anchorFor('#knows'), { id: 'specialities-scroll', openSkills: false });
|
|
664
|
+
assert.equal(MyAgents.anchorFor('#nope'), null);
|
|
665
|
+
assert.equal(MyAgents.anchorFor(''), null);
|
|
666
|
+
const code = codeOf(read('my-agents.js'));
|
|
667
|
+
assert.match(code, /id="ma-group-' \+ esc\(g\.key\) \+ '"/, 'each drawn group carries the id its anchor names');
|
|
668
|
+
assert.match(code, /addEventListener\('hashchange'/, 'a later hash lands too');
|
|
669
|
+
assert.match(read('my-agents.html'), /id="specialities-scroll"/);
|
|
670
|
+
});
|
package/tests/hall_palette.mjs
CHANGED
|
@@ -182,6 +182,18 @@ test('Settings panel anchors exist in settings.js RAIL_GROUPS', () => {
|
|
|
182
182
|
}
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
test('My agents anchors are ones my-agents.js lands, and the moved Settings panels are offered there (task 1005044)', () => {
|
|
186
|
+
const src = read('my-agents.js');
|
|
187
|
+
const block = (src.match(/const ANCHORS = \{([\s\S]*?)\n \};/) || [, ''])[1];
|
|
188
|
+
const keys = [...block.matchAll(/^\s*([a-z-]+): \{/gm)].map((m) => m[1]);
|
|
189
|
+
assert.ok(keys.length >= 5, `sanity: my-agents.js ANCHORS (${keys})`);
|
|
190
|
+
const declared = declaredHrefs((x) => x.startsWith('/my-agents#'));
|
|
191
|
+
assert.ok(declared.length >= 5, 'the palette offers the agent settings where they live now');
|
|
192
|
+
for (const h of declared) assert.ok(keys.includes(h.split('#')[1]), `${h} must be a my-agents.js anchor`);
|
|
193
|
+
assert.equal(declaredHrefs((x) => /\/settings#(interaction|wandering|rendering|voices|specialities)/.test(x)).length, 0,
|
|
194
|
+
'no palette row points at a Settings panel that left');
|
|
195
|
+
});
|
|
196
|
+
|
|
185
197
|
test('the anchor guard actually CATCHES a declared id that does not exist', () => {
|
|
186
198
|
const groups = ['work', 'karma', 'progress', 'achievements'];
|
|
187
199
|
assert.equal(groups.includes('standing-nope'.replace('standing-', '')), false,
|
|
@@ -41,6 +41,7 @@ import assert from 'node:assert/strict';
|
|
|
41
41
|
import fs from 'node:fs';
|
|
42
42
|
import path from 'node:path';
|
|
43
43
|
import { fileURLToPath } from 'node:url';
|
|
44
|
+
import { createRequire } from 'node:module';
|
|
44
45
|
|
|
45
46
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
46
47
|
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
@@ -153,120 +154,83 @@ test('the switch clears the floor: a 46x38 target drawing a 44x24 track', () =>
|
|
|
153
154
|
assert.match(on, /background:\s*var\(--accent-tint\)/, 'on is the tint, the accent arrives on the knob');
|
|
154
155
|
});
|
|
155
156
|
|
|
156
|
-
test('the
|
|
157
|
-
const render = settingsJs.match(/function renderWandering\([\s\S]*?\n \}\n/);
|
|
158
|
-
assert.ok(render, 'renderWandering is a whole function');
|
|
159
|
-
assert.match(render[0], /<label class="prow prow--pick" for="\$\{id\}"/, 'the whole row is the radio\'s label');
|
|
160
|
-
assert.match(render[0], /<input class="pradio" type="radio" name="wandering"/, 'a real radio group');
|
|
161
|
-
assert.match(render[0], /locked \? ' disabled' : ''/, 'a level above the rank ceiling is a disabled radio');
|
|
162
|
-
assert.doesNotMatch(render[0], /Rank-locked<\/button>|Choose<\/button>/, 'the four "Choose" buttons are gone');
|
|
163
|
-
const load = settingsJs.match(/async function loadWandering\([\s\S]*?\n \}\n/);
|
|
164
|
-
assert.match(load[0], /addEventListener\('change'/, 'a radio fires change, not click');
|
|
165
|
-
assert.match(load[0], /closest\('\.pradio'\)/);
|
|
157
|
+
test('the family radio clears the floor: a native radio is 13px, the family one is 24', () => {
|
|
166
158
|
const radio = ruleOf(panelCss, '.pradio');
|
|
167
159
|
assert.match(radio, /width:\s*24px/);
|
|
168
160
|
assert.match(radio, /height:\s*24px/, 'a native radio is 13px; the floor is 24');
|
|
169
161
|
});
|
|
170
162
|
|
|
171
|
-
// ---- the
|
|
163
|
+
// ---- the five agent settings live on My agents only (task 1005044) ----------
|
|
172
164
|
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
165
|
+
// How Claude talks to you, Wandering, Rendering (session cards), Skill voices and
|
|
166
|
+
// Your specialities used to be edited on BOTH pages, on the same routes, with no
|
|
167
|
+
// hint that the two were one setting. The owner kept them on My agents only. What
|
|
168
|
+
// each old panel promised a builder is now proven on My agents instead
|
|
169
|
+
// (tests/hall_my_agents.mjs "MOVED:", tests/speciality_offer_ui.mjs,
|
|
170
|
+
// tests/speciality_walkthrough_ui.mjs); this pins that Settings no longer carries
|
|
171
|
+
// them, says where they went, and sends every old link there.
|
|
172
|
+
|
|
173
|
+
const MOVED_SECTIONS = ['interaction-scroll', 'specialities-scroll', 'wandering-scroll', 'render-scroll', 'voices-scroll'];
|
|
174
|
+
const requireHall = (f) => createRequire(import.meta.url)(path.join(HALL, f));
|
|
175
|
+
|
|
176
|
+
test('Settings carries none of the five agent settings: no section, no script, no write', () => {
|
|
179
177
|
const html = read('settings.html');
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// The script half self-boots and reveals the section. If it stopped doing
|
|
192
|
-
// either, the panel would simply never appear and no test would notice.
|
|
193
|
-
assert.match(panelJs, /\$\('#interaction-scroll'\)\.hidden = false/, 'the script reveals its own section');
|
|
194
|
-
assert.match(panelJs, /setup\(\);/, 'it self-boots — settings.js does not call it');
|
|
195
|
-
|
|
196
|
-
// settings.js keeps ONLY the anchor entry, so /settings#interaction activates
|
|
197
|
-
// the group before the panel paints.
|
|
198
|
-
assert.match(settingsJs, /'interaction': 'interaction-scroll'/, 'the deep-link anchor stays in settings.js');
|
|
199
|
-
assert.doesNotMatch(settingsJs, /function renderInteraction\(/, 'the renderer moved out and must not come back inline');
|
|
178
|
+
for (const id of MOVED_SECTIONS) assert.doesNotMatch(html, new RegExp(`id="${id}"`), `${id} is gone from Settings`);
|
|
179
|
+
for (const f of ['settings-interaction.js', 'settings-render.js', 'settings-law-locks.js', 'speciality-tools.js', 'speciality-tools.css']) {
|
|
180
|
+
assert.doesNotMatch(html, new RegExp(f.replace('.', '\\.')), `Settings no longer loads ${f}`);
|
|
181
|
+
}
|
|
182
|
+
for (const f of ['settings-interaction.js', 'settings-render.js', 'settings-law-locks.js']) {
|
|
183
|
+
assert.ok(!fs.existsSync(path.join(HALL, f)), `${f} had one host, Settings, and went with its panel`);
|
|
184
|
+
}
|
|
185
|
+
for (const route of ['/me/skill-prefs', '/me/wandering', '/me/interaction', '/me/render', '/specialities']) {
|
|
186
|
+
assert.ok(!settingsJs.includes(route), `settings.js no longer reads or writes ${route}`);
|
|
187
|
+
}
|
|
188
|
+
assert.doesNotMatch(settingsJs, /function (renderVoices|renderWandering|loadVoices|loadWandering)\(/);
|
|
200
189
|
});
|
|
201
190
|
|
|
202
|
-
test('
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
191
|
+
test('Settings says where they went: one line to My agents in Preferences and one in Sound', () => {
|
|
192
|
+
const html = read('settings.html').replace(/<!--[\s\S]*?-->/g, '');
|
|
193
|
+
for (const [group, anchor] of [['preferences', '#talk'], ['sound', '#skills']]) {
|
|
194
|
+
const m = html.match(new RegExp(`<section class="scroll[^"]*" id="agents-moved-${group}"[^>]*>[\\s\\S]*?</section>`));
|
|
195
|
+
assert.ok(m, `a section in ${group} points to My agents`);
|
|
196
|
+
assert.match(m[0], new RegExp(`data-group="${group}"`), `it sits in the ${group} group`);
|
|
197
|
+
assert.doesNotMatch(m[0].match(/<section[^>]*>/)[0], /\bhidden\b/, 'drawn without waiting on any read');
|
|
198
|
+
assert.match(m[0], new RegExp(`href="/my-agents${anchor}"`), `it links to /my-agents${anchor}`);
|
|
199
|
+
}
|
|
200
|
+
// Sound still carries the sounds-off note (task 1004554), revealed at boot
|
|
201
|
+
assert.match(html, /id="sounds-off-scroll"[^>]*data-group="sound"/);
|
|
202
|
+
assert.match(settingsJs, /\$\('#sounds-off-scroll'\)\.hidden = false/);
|
|
208
203
|
});
|
|
209
204
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
205
|
+
test('every old link to those sections lands on My agents', () => {
|
|
206
|
+
const Moved = requireHall('settings-moved.js');
|
|
207
|
+
const expect = {
|
|
208
|
+
interaction: '/my-agents#talk', rendering: '/my-agents#talk', render: '/my-agents#talk',
|
|
209
|
+
wandering: '/my-agents#work', voices: '/my-agents#skills', specialities: '/my-agents#knows',
|
|
210
|
+
};
|
|
211
|
+
for (const [anchor, to] of Object.entries(expect)) {
|
|
212
|
+
assert.equal(Moved.movedTarget('#' + anchor), to, `/settings#${anchor}`);
|
|
213
|
+
}
|
|
214
|
+
// the old section ids resolved as deep links too (resolveHash falls back to an element id)
|
|
215
|
+
for (const id of MOVED_SECTIONS) assert.match(String(Moved.movedTarget('#' + id)), /^\/my-agents#/, `/settings#${id}`);
|
|
216
|
+
for (const kept of ['', '#preferences', '#sound', '#cli', '#art-key', '#software-update', '#account']) {
|
|
217
|
+
assert.equal(Moved.movedTarget(kept), null, `${kept || 'no hash'} stays on Settings`);
|
|
218
|
+
}
|
|
219
|
+
// the anchors it sends to are ones My agents lands
|
|
220
|
+
const MyAgents = requireHall('my-agents.js');
|
|
221
|
+
for (const to of new Set(Object.values(expect))) assert.ok(MyAgents.anchorFor(to.slice(to.indexOf('#'))), `${to} is a My agents anchor`);
|
|
222
|
+
// settings.js asks before it draws anything, and on every later hash
|
|
220
223
|
const html = read('settings.html');
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
assert.ok(section, 'the panel has a section');
|
|
225
|
-
assert.match(section[0], /data-group="preferences"/, 'it joins a group the rail already renders');
|
|
226
|
-
assert.match(section[0], /\bhidden\b/, 'hidden until its own read succeeds');
|
|
227
|
-
assert.match(html, /<script src="\/builders\/settings-render\.js/, 'the page loads the panel script');
|
|
228
|
-
|
|
229
|
-
assert.match(panelJs, /\$\('#render-scroll'\)\.hidden = false/, 'the script reveals its own section');
|
|
230
|
-
assert.match(panelJs, /setup\(\);/, 'it self-boots — settings.js does not call it');
|
|
231
|
-
|
|
232
|
-
// settings.js keeps ONLY the anchor entry.
|
|
233
|
-
assert.match(settingsJs, /'rendering': 'render-scroll'/, 'the deep-link anchor stays in settings.js');
|
|
234
|
-
assert.doesNotMatch(settingsJs, /function renderRender\(/, 'the renderer moved out and must not come back inline');
|
|
235
|
-
assert.doesNotMatch(settingsJs, /loadRender\(\)/, 'and settings.js no longer calls it');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test('the rendering panel is a radio group, wired on change, and can clear the override', () => {
|
|
239
|
-
const panelJs = read('settings-render.js');
|
|
240
|
-
const render = panelJs.match(/function render\([\s\S]*?\n \}\n/);
|
|
241
|
-
assert.ok(render, 'render is a whole function');
|
|
242
|
-
assert.match(render[0], /<input class="pradio" type="radio" name="render-level"/, 'a real radio group');
|
|
243
|
-
assert.match(render[0], /<label class="prow prow--pick"/, 'the whole row is the radio\'s label');
|
|
244
|
-
const setup = panelJs.match(/async function setup\([\s\S]*?\n \}\n/);
|
|
245
|
-
assert.match(setup[0], /addEventListener\('change'/, 'a radio fires change, not click');
|
|
246
|
-
assert.match(setup[0], /closest\('\.pradio'\)/);
|
|
247
|
-
// The sentinel row must send null, not a level: pinning the rung that happens
|
|
248
|
-
// to be derived today would silently stop tracking the interaction profile.
|
|
249
|
-
assert.match(panelJs, /widgets: value === USE_DEFAULT \? null : value/,
|
|
250
|
-
'choosing "match my profile" CLEARS the override rather than pinning a level');
|
|
224
|
+
assert.ok(html.indexOf('settings-moved.js') > 0 && html.indexOf('settings-moved.js') < html.indexOf('/builders/settings.js'), 'loaded before settings.js');
|
|
225
|
+
assert.match(settingsJs, /location\.replace\(/, 'a replace, so Back does not bounce through Settings');
|
|
226
|
+
assert.doesNotMatch(settingsJs, /'(interaction|wandering|rendering|voices)': '[a-z-]+-scroll'/, 'no PANEL_ANCHORS entry for a section that left');
|
|
251
227
|
});
|
|
252
228
|
|
|
253
|
-
test('
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
assert.match(panelJs, /derived_reason/, 'the row states WHY the default landed where it did');
|
|
259
|
-
assert.match(panelJs, /derived_level/, 'and WHICH rung that is');
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test('a default value is named once, by the API, not twice by the row', () => {
|
|
263
|
-
// The first render read "Non-technical (default) · DEFAULT": interaction-prefs.js
|
|
264
|
-
// already ends every default value's LABEL with "(default)", and the row also
|
|
265
|
-
// carried the wandering picker's .prow__tag. One source for the word.
|
|
266
|
-
const panelJs = read('settings-interaction.js');
|
|
267
|
-
const render = panelJs.match(/function render\([\s\S]*?\n \}\n/);
|
|
268
|
-
assert.ok(render, 'render is a whole function');
|
|
269
|
-
assert.doesNotMatch(render[0], /prow__tag/, 'the row must not re-state "default" beside a label that already says it');
|
|
229
|
+
test('settings.js stays under the size ratchet', () => {
|
|
230
|
+
// Not style: inlining a panel once put settings.js at 1601 lines and regressed
|
|
231
|
+
// oversized_file_count 0→1 (fitness-ratchets.js, OVERSIZE_LINES = 1500).
|
|
232
|
+
const lines = settingsJs.split('\n').length;
|
|
233
|
+
assert.ok(lines < 1500, `settings.js is ${lines} lines — at or over the 1500 ratchet; carve the next panel out too`);
|
|
270
234
|
});
|
|
271
235
|
|
|
272
236
|
test('.craft-pref is a 24px target with a 14px drawn box — it was 13x13, the smallest control in the hall', () => {
|
package/tests/helpers.mjs
CHANGED
|
@@ -1016,13 +1016,14 @@ export function bootSkyPage({
|
|
|
1016
1016
|
}
|
|
1017
1017
|
|
|
1018
1018
|
// bootSpecialityTools — boot THE SPECIALITY TOOLS for real on a page's OWN markup
|
|
1019
|
-
// (task 1005052). One component, modules/hall-ui/public/speciality-tools.js,
|
|
1020
|
-
// mounted by Settings and by My agents
|
|
1019
|
+
// (task 1005052). One component, modules/hall-ui/public/speciality-tools.js, was
|
|
1020
|
+
// mounted by Settings and by My agents; since task 1005044 My agents is its only
|
|
1021
|
+
// host (Settings links there). It finds every host by id. So, on the
|
|
1021
1022
|
// bootSkyPage rule, the DOM here is the page's own: its specialities section
|
|
1022
1023
|
// (`#specialities-scroll`) is parsed out of `<page>.html` into the mini-DOM above,
|
|
1023
1024
|
// an id the page does not carry is null here too, and an id the component renders
|
|
1024
1025
|
// itself (the create form's skills picker) exists only once its markup says so. A
|
|
1025
|
-
// host lost from
|
|
1026
|
+
// host lost from a page fails that page's run.
|
|
1026
1027
|
//
|
|
1027
1028
|
// The fake API keeps the builder's adoptions as STATE: adopt adds one, abandon
|
|
1028
1029
|
// drops it, the walkthrough's PUT sets its enabled skills, so a reload after a
|
|
@@ -1034,7 +1035,7 @@ export function bootSkyPage({
|
|
|
1034
1035
|
// await h.click('#specialities-rows button[data-act="adopt"]');
|
|
1035
1036
|
// h.writes() → the non-GET requests, with their bodies
|
|
1036
1037
|
// h.text('specialities-rows'), h.toasts, h.events (the DOM events it dispatched)
|
|
1037
|
-
export const SPECIALITY_PAGES = ['
|
|
1038
|
+
export const SPECIALITY_PAGES = ['my-agents'];
|
|
1038
1039
|
function specialitySection(page) {
|
|
1039
1040
|
const html = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public', `${page}.html`), 'utf8').replace(/<!--[\s\S]*?-->/g, '');
|
|
1040
1041
|
const at = html.indexOf('id="specialities-scroll"');
|
|
@@ -1043,7 +1044,7 @@ function specialitySection(page) {
|
|
|
1043
1044
|
return html.slice(start, html.indexOf('</section>', at) + '</section>'.length);
|
|
1044
1045
|
}
|
|
1045
1046
|
export function bootSpecialityTools({
|
|
1046
|
-
page = '
|
|
1047
|
+
page = 'my-agents', catalog = [], adoptions = [], suggestions = [], skills = {}, installed = [], answer = null,
|
|
1047
1048
|
} = {}) {
|
|
1048
1049
|
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/speciality-tools.js'), 'utf8');
|
|
1049
1050
|
let focused = null;
|
|
@@ -28,7 +28,6 @@ const require = createRequire(import.meta.url);
|
|
|
28
28
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
29
|
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
30
30
|
const BUILDERS_JS = read('modules/hall-ui/public/builders.js');
|
|
31
|
-
const WIDGETS_JS = read('modules/hall-ui/public/hall-widgets.js');
|
|
32
31
|
const ROUTE = read('modules/lifecycle/routes/tasks.js');
|
|
33
32
|
const DB_TASKS = read('modules/lifecycle/db-tasks.js');
|
|
34
33
|
|
|
@@ -48,26 +47,24 @@ await test('no Home read downloads a feed and slices it in the browser', () => {
|
|
|
48
47
|
// DISPLAYED is fine; narrowing what was FETCHED is the bug, and the two look
|
|
49
48
|
// identical at the call site — which is why this is a test and not a code review.
|
|
50
49
|
const src = noComments(BUILDERS_JS);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
assert.ok(url, `${name} must issue a request`);
|
|
58
|
-
assert.match(url[0], /[?&]limit=\d+/, `${name} must ask the server for its page size`);
|
|
59
|
-
}
|
|
50
|
+
const from = src.indexOf('function loadTriageInbox(');
|
|
51
|
+
assert.ok(from > 0, 'loadTriageInbox must exist');
|
|
52
|
+
const body = src.slice(from, src.indexOf('\n }\n', from));
|
|
53
|
+
const url = body.match(/\$\{API\}\/[^`'"]+/);
|
|
54
|
+
assert.ok(url, 'loadTriageInbox must issue a request');
|
|
55
|
+
assert.match(url[0], /[?&]limit=\d+/, 'loadTriageInbox must ask the server for its page size');
|
|
60
56
|
});
|
|
61
57
|
|
|
62
|
-
await test('the
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
assert.
|
|
70
|
-
assert.doesNotMatch(
|
|
58
|
+
await test('the project city asks for ONE weekly summary, and the Next-up read left with its card', () => {
|
|
59
|
+
// task 1005054: Home's body is the project city. It draws from one bounded read
|
|
60
|
+
// the server counts per week (modules/lifecycle/home-city.js), never a task feed;
|
|
61
|
+
// the Next-up card and its GET /tasks/claimable read left Home with "Your work".
|
|
62
|
+
const CITY_JS = noComments(read('modules/hall-ui/public/home-city.js'));
|
|
63
|
+
const reads = [...CITY_JS.matchAll(/api\.request\('GET',\s*`([^`]+)`/g)].map((m) => m[1]);
|
|
64
|
+
assert.deepEqual(reads, ['${API}/home/city?weeks=${WEEKS}'], 'one read, asked for its window');
|
|
65
|
+
assert.doesNotMatch(CITY_JS, /\/tasks|claimable|recent-ships/, 'the city never downloads a feed');
|
|
66
|
+
assert.doesNotMatch(noComments(BUILDERS_JS), /function loadNextUp\(|tasks\/claimable/, 'Home no longer reads the claimable feed');
|
|
67
|
+
assert.match(BUILDERS_JS, /\$\{API\}\/inbox\?limit=5/, 'the triage card still draws five');
|
|
71
68
|
});
|
|
72
69
|
|
|
73
70
|
// ---------------------------------------------------------------------------
|
|
@@ -153,16 +150,4 @@ await test('the limit is applied AFTER the JS post-filters, not as a SQL LIMIT',
|
|
|
153
150
|
assert.match(routeBody, /feed\.slice\(0, claimLimit\)/, 'the slice happens on the finished feed');
|
|
154
151
|
});
|
|
155
152
|
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// 4. what the reader sees
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
|
|
160
|
-
await test('the foot names the queue the two rows came off', () => {
|
|
161
|
-
assert.match(WIDGETS_JS, /id="home-next-foot"/, 'the link has an id to write into');
|
|
162
|
-
assert.match(BUILDERS_JS, /See all \$\{fmtNum\(total\)\} ready work/);
|
|
163
|
-
// Silent rather than wrong when the count is missing or not larger than what is
|
|
164
|
-
// shown: "See all 2" under two rows is noise, and a guessed number is worse.
|
|
165
|
-
assert.match(BUILDERS_JS, /Number\.isFinite\(total\) && total > rows\.length/);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
153
|
summary();
|