@bongos/core 1.18.19 → 1.19.3
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 +547 -197
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/bin/bongos.js +33 -7
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/0161-publish-on-merge.md +34 -0
- package/docs/adr/README.md +9 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +181 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +6 -2
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/migrations/core_205_versions_track_default.sql +28 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +99 -69
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/bump-version.js +76 -0
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +77 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +5 -5
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +96 -13
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_cli.mjs +91 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/bump_version.mjs +71 -0
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +147 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +1 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/patch_gate_ordering.mjs +102 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
|
@@ -171,5 +171,67 @@ test('laptop deploy mode → never server-handled, even with server push (false)
|
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
// ---- 3. task 1002606: the strand notice (task.strand-flagged → Discord) ----
|
|
175
|
+
// A strand previously had NO push channel — Discord fired only on task.shipped, so
|
|
176
|
+
// a builder learned of a dead land at their next /builder-start, up to a session
|
|
177
|
+
// later. These pin the poster the reconciler's task.strand-flagged listener fires.
|
|
178
|
+
|
|
179
|
+
test('1002606 chooseStrandTargets: non-security strand → #ship-feed; line does NOT lead with #<id>', () => {
|
|
180
|
+
const t = shipBroadcast.chooseStrandTargets({ securitySensitive: false, taskId: 88, reason: 'strand:pr_closed_unmerged' });
|
|
181
|
+
assert.equal(t.length, 1);
|
|
182
|
+
assert.equal(t[0].channel, 'ship_feed');
|
|
183
|
+
assert.match(t[0].line, /task 88/);
|
|
184
|
+
assert.match(t[0].line, /strand:pr_closed_unmerged/);
|
|
185
|
+
// A LEADING #<id> is the applause marker (discord-reaction.parseShipMarker) —
|
|
186
|
+
// a strand must never collect 👍-votes as if it were shipped work.
|
|
187
|
+
assert.ok(!/^\s*#\d/.test(t[0].line), 'strand line must not start with a #<id> ship marker');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('1002606 chooseStrandTargets: security strand → ONLY #security-ship-feed (reason can name an unpatched fix)', () => {
|
|
191
|
+
const t = shipBroadcast.chooseStrandTargets({ securitySensitive: true, taskId: 91, reason: 'strand:unresolvable_conflict:src/gds/auth.js' });
|
|
192
|
+
assert.equal(t.length, 1);
|
|
193
|
+
assert.equal(t[0].channel, 'security_ship_feed');
|
|
194
|
+
for (const { channel } of t) assert.ok(!PUBLIC_CHANNELS.has(channel));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await atest('1002606 postStrandNotice: posts to #ship-feed for a plain strand', async () => {
|
|
198
|
+
const { fn, calls } = fakePoster();
|
|
199
|
+
const res = await shipBroadcast.postStrandNotice(
|
|
200
|
+
{ id: 88, builder_id: 7, security_sensitive: false, reason: 'strand:pr_closed_unmerged' },
|
|
201
|
+
{ postWebhook: fn, log: quietLog }
|
|
202
|
+
);
|
|
203
|
+
assert.deepEqual(res.posted, ['ship_feed']);
|
|
204
|
+
assert.equal(calls.length, 1);
|
|
205
|
+
assert.match(calls[0].line, /task 88/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await atest('1002606 postStrandNotice: indeterminate security flag → FAIL CLOSED (no post)', async () => {
|
|
209
|
+
for (const sec of [null, undefined, 'true', 1]) {
|
|
210
|
+
const { fn, calls } = fakePoster();
|
|
211
|
+
const res = await shipBroadcast.postStrandNotice(
|
|
212
|
+
{ id: 9, security_sensitive: sec, reason: 'strand:x' },
|
|
213
|
+
{ postWebhook: fn, log: quietLog }
|
|
214
|
+
);
|
|
215
|
+
assert.equal(calls.length, 0, `flag=${String(sec)} must suppress`);
|
|
216
|
+
assert.equal(res.skipped, 'security_flag_indeterminate');
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await atest('1002606 postStrandNotice: best-effort — a throwing webhook never propagates; no task → no_task', async () => {
|
|
221
|
+
const boom = async () => { throw new Error('webhook down'); };
|
|
222
|
+
let threw = false;
|
|
223
|
+
let res;
|
|
224
|
+
try {
|
|
225
|
+
res = await shipBroadcast.postStrandNotice(
|
|
226
|
+
{ id: 10, security_sensitive: false, reason: 'strand:y' },
|
|
227
|
+
{ postWebhook: boom, log: quietLog }
|
|
228
|
+
);
|
|
229
|
+
} catch (_) { threw = true; }
|
|
230
|
+
assert.equal(threw, false, 'postStrandNotice must never throw');
|
|
231
|
+
assert.deepEqual(res.posted, [], 'a failed post is not recorded as posted');
|
|
232
|
+
const { fn } = fakePoster();
|
|
233
|
+
assert.equal((await shipBroadcast.postStrandNotice(null, { postWebhook: fn, log: quietLog })).skipped, 'no_task');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
console.log(`\nship broadcast server (task 1045 + 1002606) tests: ${passed} passed, ${failed} failed`);
|
|
175
237
|
process.exit(failed > 0 ? 1 : 0);
|
package/tests/ship_card.mjs
CHANGED
|
@@ -105,6 +105,72 @@ test('confirmed: info tone, auto-landing deploy, merge-mode override action', ()
|
|
|
105
105
|
assert.equal(m.action.cmd, '/merge-mode'); // kept as an optional manual override
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
// ---- task 1002541: the card must never claim a bailed merge is landing ----
|
|
109
|
+
//
|
|
110
|
+
// The bug: deployText came from the STATUS alone, so a 'confirmed' task printed
|
|
111
|
+
// "landing automatically" even when auto-merge had already given up — no PR, no
|
|
112
|
+
// merge, and (ADR 0120, pay on land) no credits. The failure announced itself as
|
|
113
|
+
// success, which is why these strands survived for days.
|
|
114
|
+
const bailed = {
|
|
115
|
+
...shippedInput,
|
|
116
|
+
postGradeStatus: 'confirmed',
|
|
117
|
+
land: { ok: false, pending: false, reason: 'the main checkout is on claude/foo, not main', nextStep: '/merge-mode' },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
test('confirmed + bailed land: says NOT landed, names the reason, warns', () => {
|
|
121
|
+
const m = card.buildCardModel(bailed);
|
|
122
|
+
assert.equal(m.stage, 'confirmed');
|
|
123
|
+
assert.equal(row(m, 'Deploy'), 'NOT landed — the main checkout is on claude/foo, not main');
|
|
124
|
+
assert.equal(m.cfg.tone, 'warning'); // not the reassuring 'info'
|
|
125
|
+
assert.equal(m.cfg.statusWord, 'Not landed');
|
|
126
|
+
assert.match(m.cfg.headline(1266), /NOT landed/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('confirmed + bailed land: the bail reaches the printed monospace card', () => {
|
|
130
|
+
// The monospace card is the ALWAYS-printed fallback (headless, cron, dev box).
|
|
131
|
+
// If the reason stops at the model, the builder still never sees it.
|
|
132
|
+
const out = card.renderMonospaceCard(card.buildCardModel(bailed));
|
|
133
|
+
assert.match(out, /NOT landed/);
|
|
134
|
+
assert.ok(!/landing automatically/.test(out), 'the lie must not survive anywhere on the card');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('confirmed + bailed land: the action is the bail-specific recovery command', () => {
|
|
138
|
+
const m = card.buildCardModel({
|
|
139
|
+
...bailed,
|
|
140
|
+
land: { ok: false, pending: false, reason: 'no commits ahead of main', nextStep: 'node scripts/gds/ship.js 7 --db-only' },
|
|
141
|
+
});
|
|
142
|
+
assert.equal(m.action.cmd, 'node scripts/gds/ship.js 7 --db-only'); // beats the generic /merge-mode
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('confirmed + PENDING land still reads "landing automatically"', () => {
|
|
146
|
+
// A poll timeout with auto-merge armed is success-PENDING (task 1049): CI or the
|
|
147
|
+
// server reconciler really does finish it. Crying failure here would be a NEW
|
|
148
|
+
// lie in the opposite direction, and would train builders to ignore the warning.
|
|
149
|
+
const m = card.buildCardModel({ ...bailed, land: { ok: false, pending: true, reason: null, nextStep: null } });
|
|
150
|
+
assert.equal(row(m, 'Deploy'), 'landing automatically');
|
|
151
|
+
assert.equal(m.cfg.tone, 'info');
|
|
152
|
+
assert.equal(m.cfg.statusWord, 'Confirmed');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('confirmed with NO land info is unchanged (back-compat)', () => {
|
|
156
|
+
// Older land-watch breadcrumbs replay stored cardInputs that have no `land` key,
|
|
157
|
+
// and other callers (resumeMerge, the claim card) may pass none. Absent ⇒ the
|
|
158
|
+
// pre-existing behaviour, byte for byte.
|
|
159
|
+
const m = card.buildCardModel({ ...shippedInput, postGradeStatus: 'confirmed', land: null });
|
|
160
|
+
assert.equal(row(m, 'Deploy'), 'landing automatically');
|
|
161
|
+
assert.equal(m.cfg.tone, 'info');
|
|
162
|
+
assert.equal(m.action.cmd, '/merge-mode');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('a stale bailed land never overrides a task that DID reach shipped', () => {
|
|
166
|
+
// land-watch replays the breadcrumb after the server lands it; the status is
|
|
167
|
+
// then 'shipped' and the old bail is history. The card must show live, not NOT-landed.
|
|
168
|
+
const m = card.buildCardModel({ ...bailed, postGradeStatus: 'shipped' });
|
|
169
|
+
assert.equal(m.stage, 'shipped');
|
|
170
|
+
assert.equal(row(m, 'Deploy'), 'live · 967fb4a');
|
|
171
|
+
assert.equal(m.cfg.tone, 'success');
|
|
172
|
+
});
|
|
173
|
+
|
|
108
174
|
test('failed: danger tone, no credits, issue count, regrade action', () => {
|
|
109
175
|
const m = card.buildCardModel({
|
|
110
176
|
...shippedInput, postGradeStatus: 'completed',
|
package/tests/ship_ci_deploy.mjs
CHANGED
|
@@ -44,7 +44,7 @@ function test(name, fn) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const { parseDeployMode, effectiveDeployMode, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded, shouldBailOnFetchFailure, classifyLandOutcome, strandNextStep, pickStaleGateReviewRun, landPollAfterAutoMerge, reconciledCardStatus } = ship;
|
|
47
|
+
const { parseDeployMode, effectiveDeployMode, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded, shouldBailOnFetchFailure, classifyLandOutcome, strandNextStep, pickStaleGateReviewRun, landPollAfterAutoMerge, reconciledCardStatus, landed, landPending, landBailed, strandCommand, landPendingLines } = ship;
|
|
48
48
|
|
|
49
49
|
// ---- 1. parseDeployMode precedence + safe default ----
|
|
50
50
|
|
|
@@ -266,6 +266,44 @@ test('reconciledCardStatus: status read failed (null) → keep the pre-merge sta
|
|
|
266
266
|
assert.equal(reconciledCardStatus(undefined, 'confirmed'), 'confirmed');
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
+
// ---- 3d. the land outcome: why this is a 3-way object, not a boolean ----
|
|
270
|
+
// task 1002541. The land chain used to answer true/false, so every bail site's
|
|
271
|
+
// precise reason was discarded and the card fell back to the STATUS — printing
|
|
272
|
+
// "landing automatically" over a merge that had already given up.
|
|
273
|
+
|
|
274
|
+
test('landBailed carries the reason + the recovery command', () => {
|
|
275
|
+
const o = landBailed('the main checkout is on claude/foo, not main', '/merge-mode');
|
|
276
|
+
assert.equal(o.ok, false);
|
|
277
|
+
assert.equal(o.pending, false);
|
|
278
|
+
assert.equal(o.reason, 'the main checkout is on claude/foo, not main');
|
|
279
|
+
assert.equal(o.nextStep, '/merge-mode');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('THE TRAP: every outcome is truthy, so consumers must test .ok', () => {
|
|
283
|
+
// mergeAndDeploy's guard used to be `if (!merged) return;`. With objects that
|
|
284
|
+
// check NEVER fires, so a bailed ship would fall through and POST /ship for a
|
|
285
|
+
// task that never landed. This is the assertion that catches a re-introduction.
|
|
286
|
+
assert.ok(landBailed('nope', '/merge-mode'), 'a bail object is truthy — !merged cannot detect it');
|
|
287
|
+
assert.ok(landPending(), 'a pending object is truthy too');
|
|
288
|
+
assert.equal(landBailed('nope', '/merge-mode').ok, false);
|
|
289
|
+
assert.equal(landPending().ok, false);
|
|
290
|
+
assert.equal(landed().ok, true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('pending is NOT a bail: no reason, so the card keeps "landing automatically"', () => {
|
|
294
|
+
// A poll timeout with auto-merge armed is success-pending (task 1049) — it does
|
|
295
|
+
// land on its own. The card gates its warning on `reason`, so this stays quiet.
|
|
296
|
+
const o = landPending();
|
|
297
|
+
assert.equal(o.pending, true);
|
|
298
|
+
assert.equal(o.reason, null);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('strandCommand: a box gets the resume, a laptop gets /merge-mode', () => {
|
|
302
|
+
// /merge-mode needs gh + a main checkout a dev box doesn't have (task 1052).
|
|
303
|
+
assert.equal(strandCommand(77, 'server'), 'node scripts/gds/ship.js 77');
|
|
304
|
+
assert.equal(strandCommand(77, 'local'), '/merge-mode');
|
|
305
|
+
});
|
|
306
|
+
|
|
269
307
|
test('reconciledCardStatus: already shipped stays shipped', () => {
|
|
270
308
|
assert.equal(reconciledCardStatus('shipped', 'shipped'), 'shipped');
|
|
271
309
|
});
|
|
@@ -506,6 +544,33 @@ test('effectiveDeployMode: tolerates a missing options object (defaults to no fa
|
|
|
506
544
|
assert.equal(effectiveDeployMode('laptop'), 'ci'); // canPushLocally undefined ⇒ falsy ⇒ fall back
|
|
507
545
|
});
|
|
508
546
|
|
|
547
|
+
// ---- task 1002606: the success-pending banner carries the strand-detection story ----
|
|
548
|
+
// Finding P-3: the banner ended at "This is NOT a failure", so when a land DID die
|
|
549
|
+
// (hand-written conflict, dead PR, red check) the builder's last signal said all was
|
|
550
|
+
// well. The banner must keep the reassurance AND say how a real failure reaches them.
|
|
551
|
+
|
|
552
|
+
test('1002606 landPendingLines: keeps the "NOT a failure" reassurance + the resume command', () => {
|
|
553
|
+
const lines = landPendingLines(940).join('\n');
|
|
554
|
+
assert.match(lines, /NOT a failure/);
|
|
555
|
+
assert.match(lines, /node scripts\/gds\/ship\.js 940/, 'the resume command names THIS task');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('1002606 landPendingLines: tells the builder how a strand WILL reach them (needs_rebase + /builder-start)', () => {
|
|
559
|
+
const lines = landPendingLines(940).join('\n');
|
|
560
|
+
assert.match(lines, /CANNOT land on its own/i, 'the failure story is present');
|
|
561
|
+
assert.match(lines, /needs_rebase/, 'names the flag the reconciler sets');
|
|
562
|
+
assert.match(lines, /\/builder-start/, 'names WHERE the warning appears');
|
|
563
|
+
assert.match(lines, /release/, 'names the release exit too');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('1002606 landPendingLines: merged variant says deploy-only', () => {
|
|
567
|
+
const merged = landPendingLines(1, { merged: true }).join('\n');
|
|
568
|
+
assert.match(merged, /and the PR has merged/);
|
|
569
|
+
assert.match(merged, /GitHub will deploy automatically/);
|
|
570
|
+
const unmerged = landPendingLines(1, {}).join('\n');
|
|
571
|
+
assert.match(unmerged, /GitHub will merge \+ deploy automatically/);
|
|
572
|
+
});
|
|
573
|
+
|
|
509
574
|
console.log('');
|
|
510
575
|
console.log(`ship_ci_deploy: ${passed} passed, ${failed} failed`);
|
|
511
576
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// tests/ship_smoke_gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002605 (choke-point map finding G-18): a FAILED production smoke run used
|
|
4
|
+
// to fall silently through into the claim resolve — the claim was consumed, the
|
|
5
|
+
// grader silently skipped, the task parked at 'completed', and ship.js exited 0
|
|
6
|
+
// printing "Done." The fix aborts BEFORE the resolve (claim intact) unless the
|
|
7
|
+
// builder passes --allow-smoke-fail, and both paths now say exactly what happened.
|
|
8
|
+
// This pins the loudness contract via the pure smokeGateMessages export, so a
|
|
9
|
+
// future edit can't quietly regress the gate back to silence.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/ship_smoke_gate.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const { smokeGateMessages } = require('../scripts/gds/ship.js');
|
|
18
|
+
|
|
19
|
+
let passed = 0;
|
|
20
|
+
let failed = 0;
|
|
21
|
+
function t(name, fn) {
|
|
22
|
+
try {
|
|
23
|
+
fn();
|
|
24
|
+
passed++;
|
|
25
|
+
console.log(` PASS ${name}`);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
failed++;
|
|
28
|
+
console.log(` FAIL ${name}`);
|
|
29
|
+
console.log(` ${err.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const m = smokeGateMessages(1234);
|
|
34
|
+
const abortText = m.abort.join('\n');
|
|
35
|
+
const parkedText = m.parked.join('\n');
|
|
36
|
+
|
|
37
|
+
t('abort: says the ship ABORTED because smoke FAILED', () => {
|
|
38
|
+
assert.match(abortText, /ABORTED/);
|
|
39
|
+
assert.match(abortText, /smoke test FAILED/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
t('abort: says the claim is still active and nothing changed', () => {
|
|
43
|
+
assert.match(abortText, /claim is still active/);
|
|
44
|
+
assert.match(abortText, /No claim resolved/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
t('abort: names the re-run and the deliberate --allow-smoke-fail continuation', () => {
|
|
48
|
+
assert.match(abortText, /ship\.js 1234(?!\S)/);
|
|
49
|
+
assert.match(abortText, /ship\.js 1234 --allow-smoke-fail/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
t('abort: explains the smoke suite hits live endpoints (the change is not deployed yet)', () => {
|
|
53
|
+
assert.match(abortText, /LIVE endpoints/);
|
|
54
|
+
assert.match(abortText, /not deployed yet/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
t('parked: names the deliberate stop, the exact status, and that nothing was paid', () => {
|
|
58
|
+
assert.match(parkedText, /--allow-smoke-fail/);
|
|
59
|
+
assert.match(parkedText, /Task #1234 is parked at 'completed'/);
|
|
60
|
+
assert.match(parkedText, /ungraded/);
|
|
61
|
+
assert.match(parkedText, /no credits/i);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
t('parked: names the resume path (--regrade)', () => {
|
|
65
|
+
assert.match(parkedText, /ship\.js 1234 --regrade/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
t('both message sets are non-empty arrays of strings (printable as-is)', () => {
|
|
69
|
+
for (const set of [m.abort, m.parked]) {
|
|
70
|
+
assert.ok(Array.isArray(set) && set.length > 0);
|
|
71
|
+
for (const line of set) assert.equal(typeof line, 'string');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
console.log(`\nship smoke gate (task 1002605): ${passed} passed, ${failed} failed`);
|
|
76
|
+
process.exit(failed ? 1 : 0);
|
package/tests/versions_route.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const VERSIONS_POST_SCHEMA = {
|
|
|
28
28
|
id: { required: true, type: 'string', maxLength: 64, minLength: 1 },
|
|
29
29
|
name: { required: true, type: 'string', maxLength: 200, minLength: 1 },
|
|
30
30
|
status: { type: 'string', maxLength: 32 },
|
|
31
|
+
track: { type: 'string', maxLength: 16 },
|
|
31
32
|
done_when: { type: 'string', maxLength: 8000 },
|
|
32
33
|
scope_doc_path: { type: 'string', maxLength: 256 },
|
|
33
34
|
criteria: { type: 'array', maxItems: 100 },
|
|
@@ -62,6 +63,20 @@ test('INIT: unknown extra fields pass through (forward-compat)', () => {
|
|
|
62
63
|
assert.equal(validate({ id: 'V1', name: 'x', future_field: 42 }, VERSIONS_POST_SCHEMA), null);
|
|
63
64
|
});
|
|
64
65
|
|
|
66
|
+
// versions.track is NOT NULL with no DB default (migration 003) — the create
|
|
67
|
+
// path must accept it so callers can choose, and default it so omitting it can
|
|
68
|
+
// never again 500 the route (the @bongos/core 1.18.18 `bongos init` blocker).
|
|
69
|
+
test('INIT: explicit track passes the schema guard (both tracks)', () => {
|
|
70
|
+
assert.equal(validate({ id: 'V1', name: 'x', track: 'product' }, VERSIONS_POST_SCHEMA), null);
|
|
71
|
+
assert.equal(validate({ id: 'GDS-V5', name: 'x', track: 'internal' }, VERSIONS_POST_SCHEMA), null);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('BAD: non-string track → structured 400 shape, not a DB 500', () => {
|
|
75
|
+
const r = validate({ id: 'V1', name: 'x', track: 42 }, VERSIONS_POST_SCHEMA);
|
|
76
|
+
assert.ok(r);
|
|
77
|
+
assert.ok(r.details.some((d) => d.field === 'track'));
|
|
78
|
+
});
|
|
79
|
+
|
|
65
80
|
// ---- BAD shapes must return a structured 400, never throw / 500 ----
|
|
66
81
|
test('BAD: missing id → missing', () => {
|
|
67
82
|
const r = validate({ name: 'x' }, VERSIONS_POST_SCHEMA);
|
|
@@ -23,14 +23,26 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
23
23
|
const REPO = path.resolve(__dirname, '..');
|
|
24
24
|
const CORE_PATH_SH = path.join(REPO, 'infra', 'core-path.sh');
|
|
25
25
|
|
|
26
|
+
// infra/ + the gate workflows under .github/ are HOST topology (publish-manifest.js
|
|
27
|
+
// PROTECTED_TOPOLOGY_EXCLUDE) — absent from a neutral core checkout (task 1002470).
|
|
28
|
+
// The real-tree checks only mean something where that tree exists; the synthetic
|
|
29
|
+
// counterfactual below always runs.
|
|
30
|
+
const HAS_GATE_WORKFLOWS = fs.existsSync(path.join(REPO, '.github', 'workflows', 'gate-review.yml'));
|
|
31
|
+
const HAS_CORE_PATH = fs.existsSync(CORE_PATH_SH);
|
|
32
|
+
|
|
26
33
|
let passed = 0, failed = 0;
|
|
27
34
|
function test(name, fn) {
|
|
28
35
|
try { fn(); console.log(` ok ${name}`); passed++; }
|
|
29
36
|
catch (err) { console.error(` FAIL ${name}`); console.error(' ', err.message); failed++; }
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
function hostTest(name, present, fn) {
|
|
40
|
+
if (!present) { console.log(` skip ${name} — the host topology it reads is absent in this checkout`); return; }
|
|
41
|
+
test(name, fn);
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
// ---- A. the workflow lint is green on the real tree ---------------------------------------------
|
|
33
|
-
|
|
45
|
+
hostTest('lint-workflow-exec: every core invocation is routed + publishable (no bare repo-relative form)', HAS_GATE_WORKFLOWS, () => {
|
|
34
46
|
const r = lintWorkflowExec({ instanceRoot: REPO, coreRoot: REPO });
|
|
35
47
|
assert.deepEqual(r.violations, [], `expected no violations, got: ${JSON.stringify(r.violations, null, 2)}`);
|
|
36
48
|
assert.ok(r.ok, 'lint should pass');
|
|
@@ -63,7 +75,7 @@ function runCorePath(rel, instanceRoot) {
|
|
|
63
75
|
}).trim();
|
|
64
76
|
}
|
|
65
77
|
|
|
66
|
-
|
|
78
|
+
hostTest('core-path.sh prints the monolith path (script at the instance root)', HAS_CORE_PATH, () => {
|
|
67
79
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-mono-'));
|
|
68
80
|
try {
|
|
69
81
|
fs.mkdirSync(path.join(tmp, 'scripts'), { recursive: true });
|
|
@@ -72,7 +84,7 @@ test('core-path.sh prints the monolith path (script at the instance root)', () =
|
|
|
72
84
|
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
73
85
|
});
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
hostTest('core-path.sh falls back to the consumer path (script only under node_modules/@bongos/core)', HAS_CORE_PATH, () => {
|
|
76
88
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-cons-'));
|
|
77
89
|
try {
|
|
78
90
|
const pkg = path.join(tmp, 'node_modules', '@bongos', 'core', 'scripts');
|
|
@@ -82,7 +94,7 @@ test('core-path.sh falls back to the consumer path (script only under node_modul
|
|
|
82
94
|
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
83
95
|
});
|
|
84
96
|
|
|
85
|
-
|
|
97
|
+
hostTest('core-path.sh exits non-zero (no stdout) when the script is in NO layout', HAS_CORE_PATH, () => {
|
|
86
98
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-none-'));
|
|
87
99
|
let threw = false;
|
|
88
100
|
try {
|
package/tests/worktree_guard.mjs
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { strict as assert } from 'node:assert';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
|
+
import { requireHostModule } from './helpers.mjs';
|
|
12
13
|
|
|
13
14
|
const require = createRequire(import.meta.url);
|
|
14
|
-
const { decideWorktreeGuard, isUnder } = require
|
|
15
|
+
const { decideWorktreeGuard, isUnder } = requireHostModule(require, '../.claude/hooks/worktree-guard.js');
|
|
15
16
|
|
|
16
17
|
let passed = 0;
|
|
17
18
|
let failed = 0;
|