@bongos/core 1.19.1074 → 1.19.1076
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 +196 -211
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/README.md +1 -1
- package/bin/bongos.js +1 -3
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +1 -1
- package/docs/adr/0035-builder-onboarding-three-paths.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0045-devbox-desktop-app.md +1 -1
- package/docs/adr/0046-sandbox-first-review-gate.md +1 -1
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +1 -1
- package/docs/adr/0053-scoped-dev-box-session.md +1 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0057-container-cost-ledger.md +1 -1
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +1 -1
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +1 -1
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +1 -1
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +1 -1
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +1 -1
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0145-devbox-app-branding-driven-module.md +1 -1
- package/docs/adr/0148-task-scoped-box-source-access.md +2 -2
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +1 -1
- package/docs/adr/0193-pause-task-scoped-box-slices.md +1 -1
- package/docs/adr/0277-a-box-is-in-use-only-while-a-human-is-attached.md +1 -1
- package/docs/adr/0346-dev-boxes-are-retired.md +69 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +4 -4
- package/docs/architecture.md +5 -6
- package/docs/branding-contract.md +2 -2
- package/docs/canonical-permissions.md +10 -18
- package/docs/copy-inventory.md +3 -3
- package/docs/copy-registry.json +3 -3
- package/docs/design/gate-navigation-direction.md +1 -1
- package/docs/design/hall-direction-v2.md +3 -3
- package/docs/design/modular-architecture/00-research-report.md +1 -1
- package/docs/design/modular-architecture/01-architecture.md +3 -3
- package/docs/design/modular-architecture/02-module-map.md +1 -2
- package/docs/design/modular-architecture/03-builder-flows.md +22 -20
- package/docs/design/modular-architecture/04-module-lifecycle.md +1 -1
- package/docs/design/modular-architecture/README.md +4 -2
- package/docs/design/reviews/hall-v2/README.md +2 -2
- package/docs/design/vanilla-hall-ui-redesign-scope.md +4 -4
- package/docs/file-map.md +5 -5
- package/docs/handoff-template.md +1 -1
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +11 -14
- package/docs/page-readings.json +63 -63
- package/docs/recipes/bongos-cli-release.md +1 -2
- package/docs/recipes/multi-builder-merge.md +1 -1
- package/docs/recipes/ops-gotchas.md +0 -9
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/windows-builders.md +1 -1
- package/migrations/core_258_drop_dev_box_tables.sql +3 -0
- package/modules/copy-desk/routes/copy-desk.js +1 -1
- package/modules/discord/ship-broadcast.js +3 -3
- package/modules/economy/credits.js +2 -2
- package/modules/hall-ui/public/palette.js +143 -3
- package/modules/ideas/projection.js +1 -1
- package/modules/lifecycle/db-ship.js +4 -4
- package/modules/lifecycle/github-push.js +12 -14
- package/modules/lifecycle/publish-reconciler.js +10 -10
- package/modules/lifecycle/routes/tasks.js +8 -8
- package/modules/lifecycle/ship-card.js +1 -1
- package/modules/lifecycle/task-visuals.js +2 -2
- package/modules/memory/routes/memory.js +2 -2
- package/modules/platform-identity/routes/sso.js +3 -2
- package/modules/provisioning/capacity.js +1 -1
- package/modules/provisioning/provisioning.js +4 -3
- package/modules/provisioning/routes/provisioning.js +4 -4
- package/modules/sessions/db.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/gds/artifact-staleness.js +2 -2
- package/scripts/gds/backfill-task-rewards.js +5 -5
- package/scripts/gds/claude-materialize.js +2 -2
- package/scripts/gds/cli-lib.js +7 -7
- package/scripts/gds/codemod-rename-src-gds.js +1 -1
- package/scripts/gds/context-pack.js +5 -7
- package/scripts/gds/dev-box-guard.js +165 -0
- package/scripts/gds/doctor.js +3 -3
- package/scripts/gds/fitness-checks-packaging.js +4 -4
- package/scripts/gds/fitness-lib.js +10 -0
- package/scripts/gds/fitness-ratchets.js +1 -1
- package/scripts/gds/fitness.js +2 -10
- package/scripts/gds/gen-api-client.js +4 -4
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/gen-repo-map.js +13 -14
- package/scripts/gds/gen-session-index.js +8 -8
- package/scripts/gds/leak-scan-allowlist.js +1 -1
- package/scripts/gds/plain-cards.js +1 -1
- package/scripts/gds/push-path-brief.js +10 -10
- package/scripts/gds/rename-history-check.js +1 -1
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/session-digest-build.js +2 -2
- package/scripts/gds/ship-deploy-target.js +13 -13
- package/scripts/gds/ship-flow.js +1 -1
- package/scripts/gds/ship-land.js +6 -6
- package/scripts/gds/ship-merge.js +4 -4
- package/scripts/gds/ship-preflight-steps.js +4 -4
- package/scripts/gds/ship-regen.js +9 -12
- package/scripts/gds/ship.js +9 -9
- package/scripts/gds/skill-preflight.js +9 -8
- package/src/bongos/api-prefix.js +4 -4
- package/src/bongos/module-scope-map.js +1 -1
- package/src/bongos/platform-visibility-gate.js +1 -1
- package/src/bongos/routes/me.js +1 -1
- package/src/bongos/routes/security.js +3 -3
- package/src/bongos/routes.js +2 -2
- package/src/bongos/serve-internal.js +5 -6
- package/src/bongos-downloads.js +1 -6
- package/src/branding.js +2 -2
- package/src/build-info.js +4 -6
- package/src/instance-config.js +2 -2
- package/src/module-api.js +3 -3
- package/src/module-loader/manifest-schema.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +1 -1
- package/tests/api_alias_exceptions.mjs +1 -1
- package/tests/api_client.mjs +1 -1
- package/tests/api_path_404.mjs +1 -1
- package/tests/backfill_task_rewards.mjs +1 -1
- package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
- package/tests/cli_sessions.mjs +1 -1
- package/tests/context_pack.mjs +13 -13
- package/tests/copy_inventory.mjs +7 -7
- package/tests/core_258_box_session_claims_db.mjs +180 -0
- package/tests/credit_grant.mjs +2 -2
- package/tests/criterion_suggest.mjs +1 -1
- package/tests/design_tokens_sync.mjs +3 -2
- package/tests/fitness.mjs +89 -0
- package/tests/fitness_ratchets.mjs +2 -2
- package/tests/github_push_land_proof.mjs +1 -1
- package/tests/goal_advisory.mjs +3 -3
- package/tests/goal_suggest.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +4 -3
- package/tests/hall_audit.mjs +1 -1
- package/tests/hall_palette.mjs +224 -11
- package/tests/helpers.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -2
- package/tests/migration_namespace.mjs +6 -6
- package/tests/module_api.mjs +2 -2
- package/tests/module_cli.mjs +5 -5
- package/tests/module_manifest.mjs +13 -13
- package/tests/module_route_rank.mjs +2 -2
- package/tests/modules_route_wiring.mjs +1 -1
- package/tests/onboarding_route_signals.mjs +1 -1
- package/tests/paste_token_session_store.mjs +1 -1
- package/tests/platform_visibility_gate.mjs +1 -1
- package/tests/project_modules_ui.mjs +17 -17
- package/tests/projects_hub_module_picker.mjs +75 -74
- package/tests/provisioning_capacity.mjs +1 -1
- package/tests/provisioning_recommendations.mjs +3 -2
- package/tests/publish_branch_route.mjs +1 -1
- package/tests/publish_manifest.mjs +1 -1
- package/tests/publish_reconciler.mjs +8 -8
- package/tests/push_path_brief.mjs +10 -9
- package/tests/regrade_eligibility.mjs +1 -1
- package/tests/rename_history_restraint.mjs +1 -1
- package/tests/repo_map.mjs +9 -9
- package/tests/seam_wiring_guard.mjs +11 -11
- package/tests/search_isolation.mjs +3 -3
- package/tests/session_records.mjs +1 -1
- package/tests/session_rename_fallback.mjs +1 -1
- package/tests/session_start_freshness.mjs +3 -3
- package/tests/session_token_hash_db.mjs +1 -1
- package/tests/ship_card.mjs +1 -1
- package/tests/ship_ci_deploy.mjs +17 -17
- package/tests/ship_premerge.mjs +1 -1
- package/tests/ship_resume.mjs +1 -1
- package/tests/skill_preflight.mjs +2 -2
- package/tests/skip_is_not_pass.mjs +1 -1
- package/tests/terms_acceptance.mjs +1 -1
- package/docs/design/reviews/hall-v2/harbor--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/harbor--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/harbor--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/pair--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/pair--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/pair--xenos.webp +0 -0
|
@@ -56,7 +56,7 @@ for (const fn of ['function ensureModsCatalog(', 'function renderMods(', 'functi
|
|
|
56
56
|
const CATALOG = {
|
|
57
57
|
core: ['memory', 'lifecycle', 'hall-ui'],
|
|
58
58
|
optional: [
|
|
59
|
-
{ key: '
|
|
59
|
+
{ key: 'npm-release', label: 'npm release', summary: 'Publishes each release of your project to npm.' },
|
|
60
60
|
{ key: 'discord', label: 'Discord', summary: 'Mirrors what ships into a Discord channel.' },
|
|
61
61
|
{ key: 'agents', label: 'Agents', summary: 'Lets AI builders claim and ship work on their own.' },
|
|
62
62
|
],
|
|
@@ -76,7 +76,7 @@ function parseRows(html) {
|
|
|
76
76
|
|
|
77
77
|
// Boot the region against a scripted catalog + detail channel and hand back the
|
|
78
78
|
// handles a test drives it with.
|
|
79
|
-
function boot({ selected = ['
|
|
79
|
+
function boot({ selected = ['npm-release'], source = 'chosen', catalog = CATALOG, catalogOk = true, patchAnswer } = {}) {
|
|
80
80
|
const requests = [];
|
|
81
81
|
const els = new Map();
|
|
82
82
|
const listeners = new Map();
|
|
@@ -156,14 +156,14 @@ function boot({ selected = ['dev-box'], source = 'chosen', catalog = CATALOG, ca
|
|
|
156
156
|
|
|
157
157
|
test('the band renders the optional catalog the server offered — a row and a sentence each', async () => {
|
|
158
158
|
// Arrange
|
|
159
|
-
const ui = boot({ selected: ['
|
|
159
|
+
const ui = boot({ selected: ['npm-release'] });
|
|
160
160
|
|
|
161
161
|
// Act
|
|
162
162
|
await ui.render();
|
|
163
163
|
|
|
164
164
|
// Assert
|
|
165
|
-
assert.deepEqual(ui.rows().map((r) => r.key), ['
|
|
166
|
-
assert.deepEqual(ui.rows().map((r) => r.label), ['
|
|
165
|
+
assert.deepEqual(ui.rows().map((r) => r.key), ['npm-release', 'discord', 'agents']);
|
|
166
|
+
assert.deepEqual(ui.rows().map((r) => r.label), ['npm release', 'Discord', 'Agents']);
|
|
167
167
|
for (const r of ui.rows()) assert.ok(r.summary.length > 20, `${r.label} says what it is for`);
|
|
168
168
|
});
|
|
169
169
|
|
|
@@ -171,11 +171,11 @@ test('what the project runs today is what is switched on — resolved by the ser
|
|
|
171
171
|
// A project that SKIPPED the picker reads as its own starter bundle, and the
|
|
172
172
|
// band must show that set rather than an empty one: the whole point of
|
|
173
173
|
// effectiveModules resolving `source: 'bundle'` on the way out.
|
|
174
|
-
const ui = boot({ selected: ['
|
|
174
|
+
const ui = boot({ selected: ['npm-release', 'discord'], source: 'bundle' });
|
|
175
175
|
|
|
176
176
|
await ui.render();
|
|
177
177
|
|
|
178
|
-
assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['
|
|
178
|
+
assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['npm-release', 'discord']);
|
|
179
179
|
assert.deepEqual(ui.rows().filter((r) => !r.on).map((r) => r.key), ['agents']);
|
|
180
180
|
});
|
|
181
181
|
|
|
@@ -185,7 +185,7 @@ test('the always-on core is COUNTED and stated, and is not a row anyone can swit
|
|
|
185
185
|
await ui.render();
|
|
186
186
|
|
|
187
187
|
assert.match(ui.slot(), /the same 3 core parts/, 'the clause names how many, from the server\'s own list');
|
|
188
|
-
assert.deepEqual(ui.rows().map((r) => r.key), ['
|
|
188
|
+
assert.deepEqual(ui.rows().map((r) => r.key), ['npm-release', 'discord', 'agents'], 'and no core key is on the band');
|
|
189
189
|
for (const key of CATALOG.core) {
|
|
190
190
|
assert.equal(ui.rows().some((r) => r.key === key), false, `${key} is not a choice`);
|
|
191
191
|
}
|
|
@@ -196,26 +196,26 @@ test('a toggle issues the write — the WHOLE list, in catalog order, on the own
|
|
|
196
196
|
const ui = boot({ selected: ['discord'] });
|
|
197
197
|
await ui.render();
|
|
198
198
|
|
|
199
|
-
// Act — switch
|
|
200
|
-
await ui.click('
|
|
199
|
+
// Act — switch npm release ON
|
|
200
|
+
await ui.click('npm-release');
|
|
201
201
|
|
|
202
202
|
// Assert
|
|
203
203
|
const patch = ui.requests.find((r) => r.method === 'PATCH');
|
|
204
204
|
assert.ok(patch, 'a PATCH was sent');
|
|
205
205
|
assert.equal(patch.url, '/api/bongos/provisioning/instances/18/detail');
|
|
206
|
-
assert.deepEqual(JSON.parse(patch.body), { modules: ['
|
|
206
|
+
assert.deepEqual(JSON.parse(patch.body), { modules: ['npm-release', 'discord'] },
|
|
207
207
|
'catalog order, exactly as the column stores it — never append order');
|
|
208
208
|
assert.equal(patch.credentials, 'same-origin', 'the cookie session is the auth — this page carries no token');
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
test('a toggle can REMOVE one too — the same list, one key shorter', async () => {
|
|
212
|
-
const ui = boot({ selected: ['
|
|
212
|
+
const ui = boot({ selected: ['npm-release', 'agents'] });
|
|
213
213
|
await ui.render();
|
|
214
214
|
|
|
215
215
|
await ui.click('agents');
|
|
216
216
|
|
|
217
217
|
const patch = ui.requests.find((r) => r.method === 'PATCH');
|
|
218
|
-
assert.deepEqual(JSON.parse(patch.body), { modules: ['
|
|
218
|
+
assert.deepEqual(JSON.parse(patch.body), { modules: ['npm-release'] });
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
test('switching the last one off sends an empty list — a real answer, not a skipped write', async () => {
|
|
@@ -243,8 +243,8 @@ test('the band repaints from the PLATFORM’S answer, and says the save changed
|
|
|
243
243
|
|
|
244
244
|
test('a refusal is SAID in the server’s own words, and the band goes back to what the row still holds', async () => {
|
|
245
245
|
const ui = boot({
|
|
246
|
-
selected: ['
|
|
247
|
-
patchAnswer: Promise.resolve({ ok: false, status: 400, _json: { error: { code: 'bad_modules', message: 'Unknown module "discrod" — the choices are:
|
|
246
|
+
selected: ['npm-release'],
|
|
247
|
+
patchAnswer: Promise.resolve({ ok: false, status: 400, _json: { error: { code: 'bad_modules', message: 'Unknown module "discrod" — the choices are: npm-release, discord, agents.' } } }),
|
|
248
248
|
});
|
|
249
249
|
await ui.render();
|
|
250
250
|
|
|
@@ -252,7 +252,7 @@ test('a refusal is SAID in the server’s own words, and the band goes back to w
|
|
|
252
252
|
|
|
253
253
|
assert.equal(ui.msg().className, 'projMsg err');
|
|
254
254
|
assert.match(ui.msg().textContent, /Unknown module "discrod"/);
|
|
255
|
-
assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['
|
|
255
|
+
assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['npm-release'],
|
|
256
256
|
'a refused write leaves the band showing the project’s real set');
|
|
257
257
|
});
|
|
258
258
|
|
|
@@ -269,7 +269,7 @@ test('a signed-out save offers a way back in rather than a dead end', async () =
|
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
test('a catalog that ANSWERED badly says so; nothing about the project is claimed to have changed', async () => {
|
|
272
|
-
const ui = boot({ selected: ['
|
|
272
|
+
const ui = boot({ selected: ['npm-release'], catalogOk: false });
|
|
273
273
|
|
|
274
274
|
await ui.render();
|
|
275
275
|
await ui.settle();
|
|
@@ -59,21 +59,22 @@ const PAYLOAD = {
|
|
|
59
59
|
// `summary` is part of every real bundle row and is the why-line's FLOOR since task
|
|
60
60
|
// 1003684 — the sentence shown when no rule moved anything. The fixture carried none
|
|
61
61
|
// before that, which quietly made every "no adjustment, no sentence" case pass for the
|
|
62
|
-
// wrong reason.
|
|
62
|
+
// wrong reason. Shaped like the real strings in starter-bundles.js (the live payload is pinned
|
|
63
|
+
// separately, at the bottom of the team-shape section).
|
|
63
64
|
bundles: [
|
|
64
|
-
{ type: 'game', bundle: ['
|
|
65
|
+
{ type: 'game', bundle: ['npm-release', 'discord'], summary: 'Somewhere for players to gather, and a way to publish each release of the game.' },
|
|
65
66
|
{ type: 'research', bundle: ['agents'], summary: 'Extra agents to run the reading and the experiments the question needs.' },
|
|
66
|
-
{ type: 'business', bundle: ['agents', '
|
|
67
|
+
{ type: 'business', bundle: ['agents', 'npm-release'], summary: 'A way to publish each release, and agents to look it over first.' },
|
|
67
68
|
{ type: 'non-profit', bundle: ['discord'], summary: 'Somewhere the community around the cause can gather and be heard.' },
|
|
68
69
|
{ type: 'not-sure', bundle: [], summary: 'Just the core, and nothing else to decide today. Add modules whenever you know what you need.' },
|
|
69
70
|
],
|
|
70
71
|
// `aliases` — search words a row is findable under that its own copy never uses
|
|
71
|
-
// (task 1003500, amending ADR 0244 §3). Never rendered, like the key.
|
|
72
|
+
// (task 1003500, amending ADR 0244 §3). Never rendered, like the key. npm-release
|
|
72
73
|
// carries none on purpose, so the "a row without aliases still matches on its own
|
|
73
74
|
// words" path is exercised by the same fixture rather than by a special case.
|
|
74
75
|
optional: [
|
|
75
76
|
{ key: 'agents', label: 'Extra agents', summary: 'More AI agents alongside the ones every project gets.', aliases: ['bots', 'assistants', 'automation'] },
|
|
76
|
-
{ key: '
|
|
77
|
+
{ key: 'npm-release', label: 'npm releases', summary: 'Publishes each release of your package to the registry.' },
|
|
77
78
|
{ key: 'discord', label: 'Discord', summary: 'A Discord server that carries your build news.', aliases: ['chatroom', 'talk', 'community'] },
|
|
78
79
|
],
|
|
79
80
|
};
|
|
@@ -92,18 +93,18 @@ const TAILORED = {
|
|
|
92
93
|
bundles: PAYLOAD.bundles.map((b) => {
|
|
93
94
|
if (b.type === 'game') {
|
|
94
95
|
return { ...b, byTeamShape: {
|
|
95
|
-
// solo: the
|
|
96
|
-
solo: { bundle: ['discord'], adjustments: [{ rule: 'building-alone-needs-no-
|
|
97
|
-
// small-team: the rule wants
|
|
96
|
+
// solo: the release pipeline comes off
|
|
97
|
+
solo: { bundle: ['discord'], adjustments: [{ rule: 'building-alone-needs-no-release-pipeline', effect: 'remove', module: 'npm-release', reason: 'You are building this alone, so the release pipeline is off.' }] },
|
|
98
|
+
// small-team: the rule wants npm-release, the bundle already has it — a rule
|
|
98
99
|
// that changed nothing claims nothing
|
|
99
|
-
'small-team': { bundle: ['
|
|
100
|
-
community: { bundle: ['
|
|
100
|
+
'small-team': { bundle: ['npm-release', 'discord'], adjustments: [] },
|
|
101
|
+
community: { bundle: ['npm-release', 'discord'], adjustments: [] },
|
|
101
102
|
} };
|
|
102
103
|
}
|
|
103
104
|
if (b.type === 'research') {
|
|
104
105
|
return { ...b, byTeamShape: {
|
|
105
106
|
solo: { bundle: ['agents'], adjustments: [] },
|
|
106
|
-
'small-team': { bundle: ['agents', '
|
|
107
|
+
'small-team': { bundle: ['agents', 'npm-release'], adjustments: [{ rule: 'a-team-shares-one-release-pipeline', effect: 'add', module: 'npm-release', reason: 'A small team gets one release pipeline to publish through.' }] },
|
|
107
108
|
community: { bundle: ['agents', 'discord'], adjustments: [{ rule: 'a-community-needs-somewhere-to-gather', effect: 'add', module: 'discord', reason: 'You said an open community is building this, so somewhere for it to gather is switched on.' }] },
|
|
108
109
|
} };
|
|
109
110
|
}
|
|
@@ -232,16 +233,16 @@ test('the declared project type preselects its bundle, and the rest of the catal
|
|
|
232
233
|
|
|
233
234
|
// Assert — every opt-in module is a row; the bundle's two are on
|
|
234
235
|
const rows = h.rows();
|
|
235
|
-
assert.deepEqual(rows.map((r) => r.key), ['agents', '
|
|
236
|
-
assert.deepEqual(rows.filter((r) => r.pressed).map((r) => r.key), ['
|
|
236
|
+
assert.deepEqual(rows.map((r) => r.key), ['agents', 'npm-release', 'discord'], 'the whole opt-in catalog is offered');
|
|
237
|
+
assert.deepEqual(rows.filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord'], 'the game bundle is preselected');
|
|
237
238
|
assert.equal(rows.find((r) => r.key === 'agents').pressed, false, 'and nothing else is');
|
|
238
239
|
// each row carries the copy an owner can actually read
|
|
239
|
-
assert.equal(rows[1].label, '
|
|
240
|
-
assert.match(rows[1].summary, /
|
|
240
|
+
assert.equal(rows[1].label, 'npm releases');
|
|
241
|
+
assert.match(rows[1].summary, /package to the registry/);
|
|
241
242
|
});
|
|
242
243
|
|
|
243
244
|
test('a different type preselects a different bundle, and "not sure" preselects nothing', async () => {
|
|
244
|
-
for (const [type, expected] of [['research', ['agents']], ['business', ['agents', '
|
|
245
|
+
for (const [type, expected] of [['research', ['agents']], ['business', ['agents', 'npm-release']], ['non-profit', ['discord']], ['not-sure', []], [null, []]]) {
|
|
245
246
|
const h = boot({ state: { projType: type } });
|
|
246
247
|
await h.open();
|
|
247
248
|
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), expected, `type=${type}`);
|
|
@@ -272,8 +273,8 @@ test('a toggle flips one module and leaves the rest of the preselect standing',
|
|
|
272
273
|
h.click('agents');
|
|
273
274
|
|
|
274
275
|
// Assert
|
|
275
|
-
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['agents', '
|
|
276
|
-
assert.deepEqual(h.state.modules, ['agents', '
|
|
276
|
+
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['agents', 'npm-release']);
|
|
277
|
+
assert.deepEqual(h.state.modules, ['agents', 'npm-release'], 'and the draft holds the real answer, in catalog order');
|
|
277
278
|
assert.ok(h.saves() >= 2, 'each toggle is written to the draft');
|
|
278
279
|
});
|
|
279
280
|
|
|
@@ -285,7 +286,7 @@ test('the first toggle is what turns "not answered" into an answer', async () =>
|
|
|
285
286
|
assert.equal(h.forCreate(), undefined, 'so the create request carries no modules key');
|
|
286
287
|
|
|
287
288
|
h.click('agents');
|
|
288
|
-
assert.deepEqual(h.forCreate(), ['agents', '
|
|
289
|
+
assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord'], 'after a touch it carries the whole set');
|
|
289
290
|
});
|
|
290
291
|
|
|
291
292
|
test('switching every module off is a real answer, not a reset to the bundle', async () => {
|
|
@@ -293,7 +294,7 @@ test('switching every module off is a real answer, not a reset to the bundle', a
|
|
|
293
294
|
// said "none", and Next must not hand them the bundle back.
|
|
294
295
|
const h = boot({ state: { projType: 'game' } });
|
|
295
296
|
await h.open();
|
|
296
|
-
h.click('
|
|
297
|
+
h.click('npm-release');
|
|
297
298
|
h.click('discord');
|
|
298
299
|
|
|
299
300
|
assert.deepEqual(h.state.modules, []);
|
|
@@ -314,7 +315,7 @@ test('Skip walks past WITHOUT answering, so the server applies the bundle the pa
|
|
|
314
315
|
assert.deepEqual(h.shown, [5], 'it advances to the Home step');
|
|
315
316
|
assert.equal(h.state.modules, null, 'and answers nothing');
|
|
316
317
|
assert.equal(h.forCreate(), undefined);
|
|
317
|
-
assert.deepEqual(h.picked(), ['
|
|
318
|
+
assert.deepEqual(h.picked(), ['npm-release', 'discord'], 'while the set they were shown is still the set they get');
|
|
318
319
|
});
|
|
319
320
|
|
|
320
321
|
test('Skip does not erase a toggle already made', async () => {
|
|
@@ -325,7 +326,7 @@ test('Skip does not erase a toggle already made', async () => {
|
|
|
325
326
|
h.click('agents');
|
|
326
327
|
h.skip();
|
|
327
328
|
|
|
328
|
-
assert.deepEqual(h.state.modules, ['agents', '
|
|
329
|
+
assert.deepEqual(h.state.modules, ['agents', 'npm-release', 'discord']);
|
|
329
330
|
});
|
|
330
331
|
|
|
331
332
|
test('Next CONFIRMS the set on screen — looking at it and accepting it is not the same as never being asked', async () => {
|
|
@@ -334,8 +335,8 @@ test('Next CONFIRMS the set on screen — looking at it and accepting it is not
|
|
|
334
335
|
|
|
335
336
|
h.next();
|
|
336
337
|
|
|
337
|
-
assert.deepEqual(h.state.modules, ['
|
|
338
|
-
assert.deepEqual(h.forCreate(), ['
|
|
338
|
+
assert.deepEqual(h.state.modules, ['npm-release', 'discord'], 'the preselect becomes the answer');
|
|
339
|
+
assert.deepEqual(h.forCreate(), ['npm-release', 'discord'], 'and rides the create request');
|
|
339
340
|
});
|
|
340
341
|
|
|
341
342
|
test('changing the project type re-preselects while nothing has been said, and never after', async () => {
|
|
@@ -425,7 +426,7 @@ test('the review names the set even when the picker was never opened (task 10023
|
|
|
425
426
|
|
|
426
427
|
await h.settle();
|
|
427
428
|
assert.equal(h.reviews(), 1, 'the review repaints itself once the names are known');
|
|
428
|
-
assert.equal(h.extrasRow(), '
|
|
429
|
+
assert.equal(h.extrasRow(), 'npm releases, Discord', 'and then it names them');
|
|
429
430
|
});
|
|
430
431
|
|
|
431
432
|
test('the review reads an answered picker back, empty included', async () => {
|
|
@@ -454,26 +455,26 @@ test('the team shape adjusts the type bundle, and the panel says why in the owne
|
|
|
454
455
|
// Act
|
|
455
456
|
await h.open();
|
|
456
457
|
|
|
457
|
-
// Assert — the
|
|
458
|
+
// Assert — the release pipeline came off, and the reason is on screen
|
|
458
459
|
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['discord'],
|
|
459
|
-
'solo drops
|
|
460
|
+
'solo drops npm releases, which the game bundle would have preselected');
|
|
460
461
|
assert.equal(h.whyShown(), true);
|
|
461
|
-
assert.deepEqual(h.why(), ['You are building this alone, so the
|
|
462
|
+
assert.deepEqual(h.why(), ['You are building this alone, so the release pipeline is off.']);
|
|
462
463
|
});
|
|
463
464
|
|
|
464
465
|
test('a rule that changed nothing claims nothing', async () => {
|
|
465
|
-
// small-team ADDS
|
|
466
|
+
// small-team ADDS npm-release, and the game bundle already carries it. A reason
|
|
466
467
|
// shown for a change that did not happen is a claim the owner cannot check.
|
|
467
468
|
const h = boot({ state: { projType: 'game', teamShape: 'small-team' }, payload: TAILORED });
|
|
468
469
|
await h.open();
|
|
469
470
|
|
|
470
|
-
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['
|
|
471
|
+
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord']);
|
|
471
472
|
// No adjustment, so no adjustment-sentence — the rule still claims nothing. But the
|
|
472
473
|
// panel is not silent: the TYPE explains its own preselect (task 1003684), because two
|
|
473
474
|
// extras switched on with no reason beside them is the state ADR 0237 calls worse than
|
|
474
475
|
// no preselect at all. The rule's voice and the type's voice are different things.
|
|
475
|
-
assert.deepEqual(h.why(), ['Somewhere for players to gather, and a
|
|
476
|
-
assert.ok(!h.why().some((s) => /
|
|
476
|
+
assert.deepEqual(h.why(), ['Somewhere for players to gather, and a way to publish each release of the game.']);
|
|
477
|
+
assert.ok(!h.why().some((s) => /release pipeline|somewhere for it to gather is switched on/.test(s)),
|
|
477
478
|
'and no RULE sentence is invented for a change that did not happen');
|
|
478
479
|
assert.equal(h.whyShown(), true, 'the block carries the type\'s reason');
|
|
479
480
|
});
|
|
@@ -496,7 +497,7 @@ test('answering the About step later re-preselects, with no second read', async
|
|
|
496
497
|
h.state.teamShape = 'small-team';
|
|
497
498
|
h.render();
|
|
498
499
|
|
|
499
|
-
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['agents', '
|
|
500
|
+
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['agents', 'npm-release']);
|
|
500
501
|
assert.equal(h.fetches(), 1, 'and the answer cost no round trip');
|
|
501
502
|
});
|
|
502
503
|
|
|
@@ -520,9 +521,9 @@ test('once the owner has answered, the reasons go — the panel stops arguing',
|
|
|
520
521
|
await h.open();
|
|
521
522
|
assert.equal(h.why().length, 1);
|
|
522
523
|
|
|
523
|
-
h.click('
|
|
524
|
+
h.click('npm-release');
|
|
524
525
|
|
|
525
|
-
assert.deepEqual(h.state.modules, ['
|
|
526
|
+
assert.deepEqual(h.state.modules, ['npm-release', 'discord'], 'their toggle stands over the rule');
|
|
526
527
|
assert.deepEqual(h.why(), [], 'and the rule stops explaining a set that is no longer its own');
|
|
527
528
|
assert.equal(h.whyShown(), false);
|
|
528
529
|
});
|
|
@@ -533,10 +534,10 @@ test('a payload with no byTeamShape degrades to the untailored bundle, never to
|
|
|
533
534
|
const h = boot({ state: { projType: 'game', teamShape: 'solo' }, payload: PAYLOAD });
|
|
534
535
|
await h.open();
|
|
535
536
|
|
|
536
|
-
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['
|
|
537
|
+
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord']);
|
|
537
538
|
// Degraded, but still explained: with no byTeamShape there is no adjustment to report,
|
|
538
539
|
// and the type's own summary is exactly the untailored reason (task 1003684).
|
|
539
|
-
assert.deepEqual(h.why(), ['Somewhere for players to gather, and a
|
|
540
|
+
assert.deepEqual(h.why(), ['Somewhere for players to gather, and a way to publish each release of the game.']);
|
|
540
541
|
});
|
|
541
542
|
|
|
542
543
|
test('an off-vocabulary team shape adjusts nothing', async () => {
|
|
@@ -545,10 +546,10 @@ test('an off-vocabulary team shape adjusts nothing', async () => {
|
|
|
545
546
|
const h = boot({ state: { projType: 'game', teamShape: 'a-huge-corporation' }, payload: TAILORED });
|
|
546
547
|
await h.open();
|
|
547
548
|
|
|
548
|
-
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['
|
|
549
|
+
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord']);
|
|
549
550
|
// No rule fires, so nothing is CLAIMED about the unknown shape — the sentence shown is
|
|
550
551
|
// the type's, which is true regardless of who is building it (task 1003684).
|
|
551
|
-
assert.deepEqual(h.why(), ['Somewhere for players to gather, and a
|
|
552
|
+
assert.deepEqual(h.why(), ['Somewhere for players to gather, and a way to publish each release of the game.']);
|
|
552
553
|
});
|
|
553
554
|
|
|
554
555
|
test('a couldn\'t-load read clears any reasons it had already painted', async () => {
|
|
@@ -617,17 +618,17 @@ test('the picker and the server resolve the same set for every answer an owner c
|
|
|
617
618
|
// not move.
|
|
618
619
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
619
620
|
|
|
620
|
-
const
|
|
621
|
+
const NPM_RELEASE = PAYLOAD.optional[1];
|
|
621
622
|
|
|
622
623
|
test('the matcher reads a row\'s own words — its name, its summary and its key', () => {
|
|
623
624
|
// Still lexical, never semantic: the only words it knows are the ones the owner
|
|
624
625
|
// can see, plus the two it cannot — the key, and the declared aliases below.
|
|
625
626
|
const h = boot({ state: { projType: 'game' } });
|
|
626
627
|
|
|
627
|
-
assert.equal(h.matches(
|
|
628
|
-
assert.equal(h.matches(
|
|
629
|
-
assert.equal(h.matches(
|
|
630
|
-
assert.equal(h.matches(
|
|
628
|
+
assert.equal(h.matches(NPM_RELEASE, 'releases'), true, 'by a word in the name');
|
|
629
|
+
assert.equal(h.matches(NPM_RELEASE, 'package'), true, 'by a word in the summary');
|
|
630
|
+
assert.equal(h.matches(NPM_RELEASE, 'npm-release'), true, 'by the key');
|
|
631
|
+
assert.equal(h.matches(NPM_RELEASE, 'discord'), false, 'and never by a word that is in none of them');
|
|
631
632
|
});
|
|
632
633
|
|
|
633
634
|
// ── task 1003500: the aliases, and the gap they close ────────────────────────
|
|
@@ -686,9 +687,9 @@ test('aliases narrow like any other word, and a row without them still matches o
|
|
|
686
687
|
const discord = PAYLOAD.optional[2];
|
|
687
688
|
|
|
688
689
|
assert.equal(h.matches(discord, 'chat news'), true, 'an alias AND a summary word still narrows to the row that has both');
|
|
689
|
-
assert.equal(h.matches(discord, 'chat
|
|
690
|
-
assert.equal(h.matches(
|
|
691
|
-
assert.equal(h.matches(
|
|
690
|
+
assert.equal(h.matches(discord, 'chat package'), false, 'an alias does not rescue a term the row cannot answer');
|
|
691
|
+
assert.equal(h.matches(NPM_RELEASE, 'chat'), false, 'and a row declaring no aliases is not matched by another row\'s');
|
|
692
|
+
assert.equal(h.matches(NPM_RELEASE, 'package'), true, 'while its own words keep working untouched');
|
|
692
693
|
});
|
|
693
694
|
|
|
694
695
|
test('typing an alias filters the band to that row, and the count says so', async () => {
|
|
@@ -704,14 +705,14 @@ test('typing an alias filters the band to that row, and the count says so', asyn
|
|
|
704
705
|
test('a term matches as a SUBSTRING, so a half-typed word still finds its module', () => {
|
|
705
706
|
const h = boot({ state: { projType: 'game' } });
|
|
706
707
|
|
|
707
|
-
assert.equal(h.matches(
|
|
708
|
-
assert.equal(h.matches(
|
|
708
|
+
assert.equal(h.matches(NPM_RELEASE, 'pack'), true, 'the owner is still typing');
|
|
709
|
+
assert.equal(h.matches(NPM_RELEASE, 'kag'), true, 'even mid-word — a dumb filter beats a clever one that finds nothing');
|
|
709
710
|
});
|
|
710
711
|
|
|
711
712
|
test('matching is case-insensitive in both directions', () => {
|
|
712
713
|
const h = boot({ state: { projType: 'game' } });
|
|
713
714
|
|
|
714
|
-
assert.equal(h.matches(
|
|
715
|
+
assert.equal(h.matches(NPM_RELEASE, 'NPM RELEASES'), true, 'a shouted query finds a title-cased row');
|
|
715
716
|
assert.equal(h.matches(PAYLOAD.optional[0], 'ai'), true, 'and a quiet one finds a shouted row');
|
|
716
717
|
});
|
|
717
718
|
|
|
@@ -719,15 +720,15 @@ test('every term must match — typing more words narrows, it never widens', ()
|
|
|
719
720
|
// AND, not OR. An owner adding a word is telling us the list is still too long.
|
|
720
721
|
const h = boot({ state: { projType: 'game' } });
|
|
721
722
|
|
|
722
|
-
assert.equal(h.matches(
|
|
723
|
-
assert.equal(h.matches(
|
|
723
|
+
assert.equal(h.matches(NPM_RELEASE, 'package registry'), true, 'both words are in the row');
|
|
724
|
+
assert.equal(h.matches(NPM_RELEASE, 'package discord'), false, 'one of them is not, so the row is out');
|
|
724
725
|
});
|
|
725
726
|
|
|
726
727
|
test('an empty search is not a search — every row stays', () => {
|
|
727
728
|
const h = boot({ state: { projType: 'game' } });
|
|
728
729
|
|
|
729
|
-
assert.equal(h.matches(
|
|
730
|
-
assert.equal(h.matches(
|
|
730
|
+
assert.equal(h.matches(NPM_RELEASE, ''), true);
|
|
731
|
+
assert.equal(h.matches(NPM_RELEASE, ' '), true, 'and a box holding only spaces is the same as an empty one');
|
|
731
732
|
});
|
|
732
733
|
|
|
733
734
|
test('typing filters the band to the rows that answer, in catalog order', async () => {
|
|
@@ -737,21 +738,21 @@ test('typing filters the band to the rows that answer, in catalog order', async
|
|
|
737
738
|
assert.equal(h.rows().length, 3, 'the whole catalog before anything is typed');
|
|
738
739
|
|
|
739
740
|
// Act — a word from one row's NAME
|
|
740
|
-
h.type('
|
|
741
|
+
h.type('releases');
|
|
741
742
|
|
|
742
743
|
// Assert
|
|
743
|
-
assert.deepEqual(h.rows().map((r) => r.key), ['
|
|
744
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['npm-release']);
|
|
744
745
|
});
|
|
745
746
|
|
|
746
747
|
test('a summary word and a key find their row just as a name word does', async () => {
|
|
747
748
|
const h = boot({ state: { projType: 'game' } });
|
|
748
749
|
await h.open();
|
|
749
750
|
|
|
750
|
-
h.type('
|
|
751
|
-
assert.deepEqual(h.rows().map((r) => r.key), ['
|
|
751
|
+
h.type('package');
|
|
752
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['npm-release'], 'by summary');
|
|
752
753
|
|
|
753
|
-
h.type('
|
|
754
|
-
assert.deepEqual(h.rows().map((r) => r.key), ['
|
|
754
|
+
h.type('npm-release');
|
|
755
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['npm-release'], 'by key');
|
|
755
756
|
|
|
756
757
|
h.type('DISCORD');
|
|
757
758
|
assert.deepEqual(h.rows().map((r) => r.key), ['discord'], 'and case is not part of the question');
|
|
@@ -761,10 +762,10 @@ test('two terms narrow to the rows that carry both', async () => {
|
|
|
761
762
|
const h = boot({ state: { projType: 'game' } });
|
|
762
763
|
await h.open();
|
|
763
764
|
|
|
764
|
-
h.type('
|
|
765
|
-
assert.deepEqual(h.rows().map((r) => r.key), ['
|
|
765
|
+
h.type('package registry');
|
|
766
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['npm-release']);
|
|
766
767
|
|
|
767
|
-
h.type('
|
|
768
|
+
h.type('package discord');
|
|
768
769
|
assert.deepEqual(h.rows(), [], 'and no row carries both of these');
|
|
769
770
|
});
|
|
770
771
|
|
|
@@ -785,7 +786,7 @@ test('a switched-on module the search is HIDING is still switched on', async ()
|
|
|
785
786
|
// toggles, and a view must not be able to edit it.
|
|
786
787
|
const h = boot({ state: { projType: 'game' } });
|
|
787
788
|
await h.open();
|
|
788
|
-
assert.deepEqual(h.picked(), ['
|
|
789
|
+
assert.deepEqual(h.picked(), ['npm-release', 'discord'], 'the game bundle is on');
|
|
789
790
|
|
|
790
791
|
// Act — search for the one module that is OFF, hiding both that are on
|
|
791
792
|
h.type('agents');
|
|
@@ -793,7 +794,7 @@ test('a switched-on module the search is HIDING is still switched on', async ()
|
|
|
793
794
|
// Assert
|
|
794
795
|
assert.deepEqual(h.rows().map((r) => r.key), ['agents'], 'only the match is on screen');
|
|
795
796
|
assert.equal(h.rows()[0].pressed, false, 'and it is off, as it was');
|
|
796
|
-
assert.deepEqual(h.picked(), ['
|
|
797
|
+
assert.deepEqual(h.picked(), ['npm-release', 'discord'], 'the two hidden ones are still switched on');
|
|
797
798
|
assert.equal(h.state.modules, null, 'and typing answered nothing on the owner\'s behalf');
|
|
798
799
|
assert.match(h.filterNote(), /2 extras are switched on/, 'the count under the field says so out loud');
|
|
799
800
|
assert.match(h.filterNote(), /including any this search is hiding/, 'and that some of them are hidden');
|
|
@@ -823,13 +824,13 @@ test('the review names the hidden picks too — a filter never reaches the creat
|
|
|
823
824
|
const h = boot({ state: { projType: 'game', step: 8 } }); // 8 = the review since task 1004351
|
|
824
825
|
await h.open();
|
|
825
826
|
h.click('agents');
|
|
826
|
-
assert.deepEqual(h.forCreate(), ['agents', '
|
|
827
|
+
assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord']);
|
|
827
828
|
|
|
828
829
|
h.type('discord');
|
|
829
830
|
|
|
830
831
|
assert.deepEqual(h.rows().map((r) => r.key), ['discord'], 'one row on screen');
|
|
831
|
-
assert.deepEqual(h.forCreate(), ['agents', '
|
|
832
|
-
assert.equal(h.extrasRow(), 'Extra agents,
|
|
832
|
+
assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord'], 'three modules in the answer');
|
|
833
|
+
assert.equal(h.extrasRow(), 'Extra agents, npm releases, Discord', 'and the review names all three');
|
|
833
834
|
});
|
|
834
835
|
|
|
835
836
|
test('clearing the search brings every row back, untouched', async () => {
|
|
@@ -840,8 +841,8 @@ test('clearing the search brings every row back, untouched', async () => {
|
|
|
840
841
|
|
|
841
842
|
h.type('');
|
|
842
843
|
|
|
843
|
-
assert.deepEqual(h.rows().map((r) => r.key), ['agents', '
|
|
844
|
-
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['
|
|
844
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['agents', 'npm-release', 'discord']);
|
|
845
|
+
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['npm-release', 'discord'],
|
|
845
846
|
'with the preselect exactly as it was before the search');
|
|
846
847
|
assert.equal(h.filterNote(), '', 'and the count goes quiet again');
|
|
847
848
|
});
|
|
@@ -851,11 +852,11 @@ test('a toggle made while filtered lands in the answer in catalog order, and the
|
|
|
851
852
|
const h = boot({ state: { projType: 'non-profit' } });
|
|
852
853
|
await h.open();
|
|
853
854
|
|
|
854
|
-
h.type('
|
|
855
|
-
h.click('
|
|
855
|
+
h.type('package');
|
|
856
|
+
h.click('npm-release');
|
|
856
857
|
|
|
857
|
-
assert.deepEqual(h.state.modules, ['
|
|
858
|
-
assert.deepEqual(h.rows().map((r) => r.key), ['
|
|
858
|
+
assert.deepEqual(h.state.modules, ['npm-release', 'discord'], 'catalog order, not click order');
|
|
859
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['npm-release'], 'and the search is still the search');
|
|
859
860
|
assert.equal(h.rows()[0].pressed, true, 'the row they just switched on shows it');
|
|
860
861
|
});
|
|
861
862
|
|
|
@@ -248,7 +248,7 @@ await ta('dry-run measures nothing and refuses nothing, even on a full box; an u
|
|
|
248
248
|
|
|
249
249
|
await ta('statusCapacityLine prints "box at N of M" for the CLI', async () => {
|
|
250
250
|
delete process.env.PROVISION_BOX_CEILING;
|
|
251
|
-
// fleetCapacity reads THIS host's /proc/meminfo (absent on a Windows
|
|
251
|
+
// fleetCapacity reads THIS host's /proc/meminfo (absent on a Windows machine → default);
|
|
252
252
|
// the line must carry the count and the source whichever it is.
|
|
253
253
|
const line = await statusCapacityLine(fakeDb(12));
|
|
254
254
|
assert.match(line, /control-plane box: 12 of \d+ project slots taken \((measured|default)/);
|
|
@@ -103,7 +103,7 @@ test('an open community adds somewhere to gather to a bundle that lacks one', ()
|
|
|
103
103
|
|
|
104
104
|
test('a community adds the gathering place to a bundle that lacks it', () => {
|
|
105
105
|
// The surviving bundle rule's ADD path. The two rules that added and removed
|
|
106
|
-
//
|
|
106
|
+
// the dev box by team shape went with the module (task 1003886), which leaves
|
|
107
107
|
// `effect: 'remove'` declared in adjustBundle but named by no live rule - it is
|
|
108
108
|
// kept for the next rule that needs it, not because anything exercises it today.
|
|
109
109
|
// Arrange - the research bundle is `agents` alone, so the addition bites.
|
|
@@ -147,7 +147,8 @@ test('a rule whose module the bundle already carries records no adjustment', ()
|
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
test('a removal rule whose module is absent records no adjustment', () => {
|
|
150
|
-
// Arrange — `non-profit` is `discord` alone,
|
|
150
|
+
// Arrange — `non-profit` is `discord` alone, and `solo` names no live rule (the removal rule for
|
|
151
|
+
// it left with the dev-box module, task 1003886), so there is nothing to remove or to claim.
|
|
151
152
|
const answers = { type: 'non-profit', team_shape: 'solo' };
|
|
152
153
|
|
|
153
154
|
// Act
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// tests/publish_branch_route.mjs
|
|
2
2
|
//
|
|
3
3
|
// Coverage for server-mediated branch publish (ADR 0055 / task 1025) — the path
|
|
4
|
-
// that lets a
|
|
4
|
+
// that lets a builder on a checkout with no GitHub push credential ship: the client uploads a
|
|
5
5
|
// thin git bundle, the server pushes it + opens the PR + auto-merges with the
|
|
6
6
|
// server-side GITHUB_PUSH_TOKEN.
|
|
7
7
|
//
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Unit tests for the server-owned publish reconciler (task 1134) — the durable
|
|
4
4
|
// cure for the "strands at confirmed" class. A server-mediated publish (ADR 0055)
|
|
5
5
|
// is async; ship.js stops polling after ~9 min, so the confirmed→shipped flip had
|
|
6
|
-
// no driver once the
|
|
6
|
+
// no driver once the client exited. This module sweeps confirmed tasks on a timer and
|
|
7
7
|
// runs the EXACT shipTask + postShipBroadcast the publish-status route runs.
|
|
8
8
|
//
|
|
9
9
|
// runSweep is pure w.r.t. injected deps (db / githubPush / emit / loggers)
|
|
@@ -1366,7 +1366,7 @@ test('1002606 the aged stale strand now ALSO emits task.strand-flagged', async (
|
|
|
1366
1366
|
|
|
1367
1367
|
// ---------------------------------------------------------------------------
|
|
1368
1368
|
// task 1002645 — the reconciler was blind to laptop/wave ships. The sweep derives
|
|
1369
|
-
// claude/<worktree_name>
|
|
1369
|
+
// claude/<worktree_name> first; a wave ship pushes
|
|
1370
1370
|
// worktree-<worktree_name>, so getPublishStatus saw pr_state:'none' and the whole
|
|
1371
1371
|
// PR path (mergeIfGreen nudge — the ONLY merger where native auto-merge is
|
|
1372
1372
|
// repo-disallowed — conflict heal, land proof) never engaged. PRs 34/35/36 all
|
|
@@ -1378,7 +1378,7 @@ test('1002645 THE PIN: a worktree-named branch (laptop wave ship) lands UNATTEND
|
|
|
1378
1378
|
const deps = mkDeps({
|
|
1379
1379
|
rows: [{ task_id: 2645, builder_id: 95, worktree_name: 'task-2645', published_head_sha: ON_MAIN }],
|
|
1380
1380
|
statusByBranch: {
|
|
1381
|
-
'claude/task-2645': NONE, // the derived
|
|
1381
|
+
'claude/task-2645': NONE, // the derived claude/ branch has no PR
|
|
1382
1382
|
'worktree-task-2645': LANDED_NO_WORKFLOW, // the REAL branch: merged, pin-bump repo
|
|
1383
1383
|
},
|
|
1384
1384
|
taskPrByTask: { 2645: { branch: 'worktree-task-2645', pr_number: 36, pr_state: 'closed', merged: true } },
|
|
@@ -1418,14 +1418,14 @@ test('1002645 a legacy laptop strand (no recorded tip) heals retroactively via t
|
|
|
1418
1418
|
assert.deepEqual(deps._calls.ship, [{ taskId: 2616, builderId: 95 }]);
|
|
1419
1419
|
});
|
|
1420
1420
|
|
|
1421
|
-
test('1002645 the fallback does NOT fire when the derived branch already has a PR (
|
|
1421
|
+
test('1002645 the fallback does NOT fire when the derived branch already has a PR (derived-branch path unchanged)', async () => {
|
|
1422
1422
|
const deps = mkDeps({
|
|
1423
|
-
rows: [{ task_id: 900, builder_id: 1, worktree_name: 'wt-
|
|
1424
|
-
statusByBranch: { 'claude/wt-
|
|
1423
|
+
rows: [{ task_id: 900, builder_id: 1, worktree_name: 'wt-derived' }],
|
|
1424
|
+
statusByBranch: { 'claude/wt-derived': PENDING },
|
|
1425
1425
|
taskPrByTask: { 900: new Error('search must not run') },
|
|
1426
1426
|
});
|
|
1427
1427
|
await reconciler.runSweep(deps);
|
|
1428
|
-
assert.equal(deps._calls.search.length, 0, 'a
|
|
1428
|
+
assert.equal(deps._calls.search.length, 0, 'a ship whose derived branch has a PR keeps that path — no extra API call');
|
|
1429
1429
|
});
|
|
1430
1430
|
|
|
1431
1431
|
test('1002645 a PR-search failure is swallowed — the sweep continues to the next task and retries next sweep', async () => {
|
|
@@ -1481,7 +1481,7 @@ test('1002723 THE PIN: a PR on the BARE worktree branch (task-<id>, no prefix) l
|
|
|
1481
1481
|
});
|
|
1482
1482
|
const r = await reconciler.runSweep(deps);
|
|
1483
1483
|
assert.equal(r.reconciled, 1, 'the merged-PR-on-bare-branch shape must land with no manual flip');
|
|
1484
|
-
assert.ok(deps._calls.status.includes('claude/task-77'), '
|
|
1484
|
+
assert.ok(deps._calls.status.includes('claude/task-77'), 'the derived claude/<worktree_name> branch is tried first');
|
|
1485
1485
|
assert.ok(deps._calls.status.includes('task-77'), 'then the bare worktree branch');
|
|
1486
1486
|
assert.deepEqual(deps._calls.ship, [{ taskId: 77, builderId: 3 }]);
|
|
1487
1487
|
});
|