@bongos/core 1.19.1032 → 1.19.1034

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.
Files changed (151) hide show
  1. package/.bongos-core.json +322 -117
  2. package/.claude/skills/builder-start/SKILL.md +1 -1
  3. package/.claude/skills/grade-audit/SKILL.md +1 -1
  4. package/.claude/skills/grade-recover/SKILL.md +2 -1
  5. package/.claude/skills/grader-health/SKILL.md +5 -1
  6. package/.claude/skills/merge-mode/SKILL.md +1 -1
  7. package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
  8. package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
  9. package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
  10. package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
  11. package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
  12. package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
  13. package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
  14. package/docs/adr/README.md +4 -4
  15. package/docs/api/openapi.json +1 -1
  16. package/docs/architecture.md +4 -3
  17. package/docs/canonical-permissions.md +2 -2
  18. package/docs/copy-inventory.md +34 -30
  19. package/docs/copy-registry.json +69 -33
  20. package/docs/file-map.md +4 -3
  21. package/docs/module-api-changelog.md +4 -0
  22. package/docs/recipes/autobongos-windows-host.md +3 -1
  23. package/docs/recipes/local-dev.md +45 -3
  24. package/docs/recipes/packaging-the-core.md +3 -1
  25. package/docs/recipes/redteam-patrol.md +2 -2
  26. package/docs/recipes/upgrading-the-core.md +1 -1
  27. package/modules/autonomy/cadence.js +52 -0
  28. package/modules/autonomy/gauge.js +57 -9
  29. package/modules/government/protected-surfaces.json +1 -0
  30. package/modules/grading/grade-engine.js +267 -0
  31. package/modules/grading/grader-subagent.js +4 -3
  32. package/modules/grading/grader-workers/index.js +67 -11
  33. package/modules/grading/grader-workers/worker-verdict.js +32 -2
  34. package/modules/grading/grader.js +109 -88
  35. package/modules/hall-ui/public/deploy.js +35 -0
  36. package/modules/hall-ui/public/project-settings.css +7 -0
  37. package/modules/hall-ui/records/builders-roster.md +10 -0
  38. package/modules/hall-ui/records/catalog.md +10 -0
  39. package/modules/hall-ui/records/collaboration.md +14 -0
  40. package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
  41. package/modules/hall-ui/records/oversight-family.md +19 -0
  42. package/modules/hall-ui/records/panel-family.md +19 -0
  43. package/modules/hall-ui/records/reading-rooms.md +12 -0
  44. package/modules/hall-ui/records/record-family.md +13 -0
  45. package/modules/hall-ui/records/reference-room.md +9 -0
  46. package/modules/hall-ui/records/roster-and-ledger.md +9 -0
  47. package/modules/hall-ui/records/sky.md +23 -0
  48. package/modules/lifecycle/dependency-advisory.js +2 -1
  49. package/modules/lifecycle/routes/tasks.js +19 -4
  50. package/modules/lifecycle/ship-preflight.js +91 -0
  51. package/modules/provisioning/control-plane-guard.js +14 -13
  52. package/modules/provisioning/core-upgrade.js +30 -17
  53. package/modules/provisioning/public-refusal.js +44 -0
  54. package/modules/provisioning/routes/disconnect.js +2 -1
  55. package/modules/provisioning/routes/env-manifest.js +5 -4
  56. package/modules/provisioning/routes/provisioning.js +16 -15
  57. package/modules/provisioning/routes/repo-private.js +2 -1
  58. package/package-lock.json +2 -2
  59. package/package.json +1 -1
  60. package/release-notes.json +7481 -0
  61. package/scripts/gds/address-park-sweep.sh +3 -1
  62. package/scripts/gds/autobongos-run.js +91 -3
  63. package/scripts/gds/backfill-cost.js +20 -9
  64. package/scripts/gds/baseline-staleness.js +1 -1
  65. package/scripts/gds/claim.js +2 -2
  66. package/scripts/gds/credential-presence.js +281 -0
  67. package/scripts/gds/db-backup-nightly.sh +9 -4
  68. package/scripts/gds/db-backup-offsite.sh +4 -2
  69. package/scripts/gds/deploy-main-audit-gate.sh +11 -4
  70. package/scripts/gds/dev-lib.js +212 -5
  71. package/scripts/gds/dev.js +102 -50
  72. package/scripts/gds/doctor.js +3 -2
  73. package/scripts/gds/grade-correlation-audit.js +280 -0
  74. package/scripts/gds/grade-replay.js +237 -0
  75. package/scripts/gds/hooks/pre-push.js +7 -3
  76. package/scripts/gds/main-audit.js +19 -10
  77. package/scripts/gds/memory-map.js +3 -1
  78. package/scripts/gds/onboarding-config.js +3 -2
  79. package/scripts/gds/package-core.js +72 -1
  80. package/scripts/gds/plain-cards.js +4 -0
  81. package/scripts/gds/preflight.js +12 -4
  82. package/scripts/gds/provision-core-upgrade.js +282 -80
  83. package/scripts/gds/provision-repo-auth.js +15 -0
  84. package/scripts/gds/provision.js +1 -1
  85. package/scripts/gds/publish-credential-check.js +3 -0
  86. package/scripts/gds/push-main.js +14 -3
  87. package/scripts/gds/push-path-brief.js +3 -2
  88. package/scripts/gds/redteam-patrol.js +4 -4
  89. package/scripts/gds/release-notes.js +271 -0
  90. package/scripts/gds/run-routine.js +18 -5
  91. package/scripts/gds/run-unit-tests.js +14 -3
  92. package/scripts/gds/seed-board-room-tasks.js +1 -1
  93. package/scripts/gds/session-cost.js +12 -3
  94. package/scripts/gds/ship-deploy-target.js +11 -6
  95. package/scripts/gds/ship-flow.js +5 -3
  96. package/scripts/gds/ship-grade-steps.js +33 -0
  97. package/scripts/gds/ship-land.js +241 -16
  98. package/scripts/gds/ship-merge.js +3 -2
  99. package/scripts/gds/ship-preflight-steps.js +86 -11
  100. package/scripts/gds/ship.js +1 -1
  101. package/scripts/gds/stray-unit-sweep.sh +3 -1
  102. package/scripts/gds/upgrade-migrate.js +67 -0
  103. package/scripts/gds/upgrade.js +9 -7
  104. package/scripts/hall-preview/README.md +20 -3
  105. package/scripts/hall-preview/live-identity.js +1 -1
  106. package/scripts/hall-preview/refresh-fixtures.js +71 -11
  107. package/scripts/hall-preview/scrub-identity.js +296 -0
  108. package/src/bongos/routes/security.js +3 -3
  109. package/src/build-info.js +4 -2
  110. package/src/module-api.js +1 -1
  111. package/tests/adr_0089_scope.mjs +70 -0
  112. package/tests/adr_0162_citation.mjs +184 -0
  113. package/tests/autobongos_cadence.mjs +74 -0
  114. package/tests/autobongos_loop.mjs +114 -0
  115. package/tests/autonomy_gauge.mjs +56 -0
  116. package/tests/consumer_layout_boot.mjs +363 -31
  117. package/tests/core_upgrade_door.mjs +40 -6
  118. package/tests/core_upgrade_runner.mjs +329 -5
  119. package/tests/credential_presence.mjs +153 -0
  120. package/tests/db_backup_nightly.mjs +21 -3
  121. package/tests/db_backup_offsite.mjs +81 -0
  122. package/tests/deploy_audit_gate.mjs +5 -2
  123. package/tests/deploy_page_projects.mjs +63 -2
  124. package/tests/dev_lib.mjs +2 -1
  125. package/tests/dev_windows.mjs +324 -0
  126. package/tests/env_knobs_neutral_prefix.mjs +203 -0
  127. package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
  128. package/tests/government_protected_surfaces.mjs +10 -0
  129. package/tests/grade_correlation_audit.mjs +159 -0
  130. package/tests/grade_engine.mjs +249 -0
  131. package/tests/grade_engine_replay.mjs +99 -0
  132. package/tests/grader_prompt_window_bound.mjs +132 -0
  133. package/tests/grader_unbacked_score.mjs +194 -0
  134. package/tests/hall_preview_scrub.mjs +214 -0
  135. package/tests/hall_records.mjs +85 -0
  136. package/tests/helpers.mjs +25 -0
  137. package/tests/instance_db_migrated_on_code_move.mjs +8 -1
  138. package/tests/integration_parked_ratchet.mjs +24 -1
  139. package/tests/plain_cards.mjs +1 -1
  140. package/tests/provisioning_public_refusal.mjs +195 -0
  141. package/tests/publish_status_branch.mjs +128 -0
  142. package/tests/redteam_patrol.mjs +1 -1
  143. package/tests/release_notes.mjs +327 -0
  144. package/tests/shell_stat_portability.mjs +49 -0
  145. package/tests/ship_land_conflict.mjs +181 -0
  146. package/tests/ship_land_red_pr.mjs +236 -0
  147. package/tests/ship_preflight.mjs +226 -3
  148. package/tests/start_env_guard.mjs +16 -0
  149. package/tests/sweep_app_account.mjs +15 -2
  150. package/tests/upgrade.mjs +20 -1
  151. package/tests/upgrade_migrate.mjs +195 -0
@@ -31,7 +31,8 @@ import { createRequire } from 'node:module';
31
31
  import fs from 'node:fs';
32
32
  import path from 'node:path';
33
33
  // An ESM named import, not R.parseSchemaPending: knip credits a CJS export only when a test imports it this way.
34
- import { parseSchemaPending } from '../scripts/gds/provision-core-upgrade.js';
34
+ import { parseSchemaPending, repoNote, REPO_NOTES } from '../scripts/gds/provision-core-upgrade.js';
35
+ import { parseRepoNote } from '../modules/provisioning/core-upgrade.js';
35
36
 
36
37
  const require = createRequire(import.meta.url);
37
38
  const R = require('../scripts/gds/provision-core-upgrade.js');
@@ -196,9 +197,9 @@ await t('and an ordinary slug still runs', async () => {
196
197
  });
197
198
 
198
199
  await t('the guard error carries a status and a sentence, so a route CAN answer with it', async () => {
199
- // No route maps it today — reaching it means a route forgot its own check, which
200
- // deserves a 500 and a stack. The fields exist so that stays a choice rather than a
201
- // rewrite, and so the wording is settled once.
200
+ // Every route answers it through failFrom now (task 1004126): the owner reads this
201
+ // sentence as a 409, and the route still logs the stack. The wording is settled here,
202
+ // once; tests/provisioning_public_refusal.mjs pins that it arrives.
202
203
  const selfRow = { query: async () => ({ rows: [{ hosting_shape: 'control-plane' }] }) };
203
204
  await assert.rejects(() => G.assertIntentAllowed(selfRow, 1, 'teardown'), (err) => {
204
205
  assert.equal(err.code, 'control_plane_protected');
@@ -886,15 +887,26 @@ await t('a paragraph-long summary is bounded — it reaches a DOM', () => {
886
887
  // A box that really does hold a core checkout. `repos` maps a directory to what git
887
888
  // there answers: `core` (a real core checkout), `notCore` (a git repo of something else),
888
889
  // `shallow`, `owned` (git refuses on ownership) or absent (not a git repo).
890
+ //
891
+ // The release notes the core PACKAGE carries (task 1004218) are asked first, and are
892
+ // stubbed here: none unless a test hands some in, so the checkout tests below exercise the
893
+ // fallback — and no test in this file can ever reach the real registry.
889
894
  const changeDeps = ({
890
895
  shas = { '1.19.900': 'a'.repeat(40), '1.19.949': 'b'.repeat(40) },
891
896
  log = LOG, logOk = true, repos = { '/srv/core': 'core' }, env = {}, privileged = false,
897
+ installedNotes = null, packagedNotes = null,
892
898
  } = {}) => {
893
899
  const ran = [];
900
+ const downloaded = [];
894
901
  return {
895
- ran, env, privileged,
902
+ ran, downloaded, env, privileged,
896
903
  selfInstanceRoot: () => '/srv/core',
897
904
  appUser: () => 'appacct',
905
+ readInstalledNotes: () => (installedNotes ? { notes: installedNotes } : { missing: 'this core carries no release notes' }),
906
+ readPackagedNotes: (v) => {
907
+ downloaded.push(v);
908
+ return packagedNotes ? { notes: packagedNotes } : { missing: `the core ${v} package carries no release notes` };
909
+ },
898
910
  exec: (cmd) => {
899
911
  ran.push(cmd);
900
912
  const dir = (/git -C (\S+) /.exec(cmd) || [])[1];
@@ -1051,6 +1063,83 @@ await t('a long range is capped, and says how many it did not list', () => {
1051
1063
  assert.equal(r.more, 5, 'the remainder is counted, not silently dropped');
1052
1064
  });
1053
1065
 
1066
+ // ---- THE NOTES THE PACKAGE CARRIES, asked first (task 1004218) --------------------
1067
+
1068
+ // What the packer writes into release-notes.json: one key per release, what each brought in.
1069
+ const NOTES = {
1070
+ '1.19.900': [{ text: 'already live before the move' }],
1071
+ '1.19.901': [{ id: '1004001', text: 'the first thing the move brings' }],
1072
+ '1.19.920': [],
1073
+ '1.19.949': [{ id: '1004003', text: 'the target itself' }, { text: 'a hand-landed ship in the target' }],
1074
+ '1.19.950': [{ text: 'after the target, so not part of this move' }],
1075
+ };
1076
+
1077
+ await t('THE PACKAGE’S NOTES ANSWER FIRST — on a box with no checkout, and with no git run at all', () => {
1078
+ // A tenant box, which is every box but the one that builds the core: no core history.
1079
+ const d = changeDeps({ packagedNotes: NOTES, repos: {} });
1080
+ const r = R.changesBetween('1.19.900', '1.19.949', d);
1081
+ assert.equal(r.error, undefined);
1082
+ assert.deepEqual(r.entries.map((e) => e.text),
1083
+ ['the target itself', 'a hand-landed ship in the target', 'the first thing the move brings'], 'newest release first');
1084
+ assert.equal(r.source, 'release-notes');
1085
+ assert.deepEqual(d.ran, [], 'not one git command');
1086
+ assert.deepEqual(d.downloaded, ['1.19.949'], "the TARGET's own package is the one read");
1087
+ });
1088
+
1089
+ await t('the range is (from, to]: what the box already runs is not listed, what the target carries is', () => {
1090
+ const texts = R.changesBetween('1.19.900', '1.19.949', changeDeps({ packagedNotes: NOTES })).entries.map((e) => e.text);
1091
+ assert.ok(!texts.includes('already live before the move'), '`from` is exclusive');
1092
+ assert.ok(texts.includes('the target itself'), '`to` is inclusive');
1093
+ assert.ok(!texts.includes('after the target, so not part of this move'));
1094
+ });
1095
+
1096
+ await t('the INSTALLED core’s notes answer alone when they already reach the target — nothing is downloaded', () => {
1097
+ const d = changeDeps({ installedNotes: NOTES });
1098
+ assert.equal(R.changesBetween('1.19.900', '1.19.949', d).entries.length, 3);
1099
+ assert.deepEqual(d.downloaded, [], 'a tenant catching up to where the platform already is costs no download');
1100
+ });
1101
+
1102
+ await t('installed notes that stop SHORT of the target send it to the target’s package', () => {
1103
+ const d = changeDeps({ installedNotes: { '1.19.900': [], '1.19.901': [{ text: 'x' }] }, packagedNotes: NOTES });
1104
+ assert.equal(R.changesBetween('1.19.900', '1.19.949', d).entries.length, 3);
1105
+ assert.deepEqual(d.downloaded, ['1.19.949']);
1106
+ });
1107
+
1108
+ await t('notes that BEGIN after the box’s version are not used — the list would look whole and be short', () => {
1109
+ // A bounded file drops its oldest releases. Answering a range that reaches back past
1110
+ // where it begins would silently omit what it no longer holds.
1111
+ const r = R.changesBetween('1.19.850', '1.19.949', changeDeps({ packagedNotes: NOTES, repos: {} }));
1112
+ assert.equal(r.entries, undefined, 'no list at all');
1113
+ assert.match(r.error, /the release notes in the core 1\.19\.949 package begin at core 1\.19\.900, after 1\.19\.850/);
1114
+ });
1115
+
1116
+ await t('NEITHER SOURCE CAN ANSWER: the error carries both reasons, and is never an empty list', () => {
1117
+ // Every target published before the notes were packaged, on every tenant box.
1118
+ const r = R.changesBetween('1.19.900', '1.19.949', changeDeps({ repos: {} }));
1119
+ assert.equal(r.entries, undefined);
1120
+ assert.match(r.error, /the core 1\.19\.949 package carries no release notes/, "the notes' reason");
1121
+ assert.match(r.error, /no core checkout is reachable/, "and the checkout's own precise sentence (task 1004212)");
1122
+ });
1123
+
1124
+ await t('no notes in the package, but a checkout on the box: the checkout still answers', () => {
1125
+ const r = R.changesBetween('1.19.900', '1.19.949', changeDeps());
1126
+ assert.equal(r.entries.length, 2);
1127
+ assert.equal(r.source, 'checkout');
1128
+ });
1129
+
1130
+ await t('a download that fails is named, and the checkout is still tried', () => {
1131
+ const d = changeDeps({ repos: {} });
1132
+ d.readPackagedNotes = () => ({ error: 'could not download the core 1.19.949 package to read its release notes (npm error code E404)' });
1133
+ assert.match(R.changesBetween('1.19.900', '1.19.949', d).error, /could not download the core 1\.19\.949 package.*; no core checkout is reachable/);
1134
+ });
1135
+
1136
+ await t('a notes answer is capped exactly like a checkout answer', () => {
1137
+ const many = { '1.19.900': [], '1.19.949': Array.from({ length: R.CHANGE_CAP + 3 }, (_, i) => ({ text: `summary ${i}` })) };
1138
+ const r = R.changesBetween('1.19.900', '1.19.949', changeDeps({ packagedNotes: many }));
1139
+ assert.equal(r.entries.length, R.CHANGE_CAP);
1140
+ assert.equal(r.more, 3);
1141
+ });
1142
+
1054
1143
  await t('the dry run records the list beside the verdict, keyed to where the box IS', async () => {
1055
1144
  // Both answers are about ONE move, so they are stored together — and `from` is stored
1056
1145
  // with them because the list is only true of that starting point.
@@ -1071,5 +1160,240 @@ await t('the dry run records the list beside the verdict, keyed to where the box
1071
1160
  assert.equal(stored.changes.entries[0].text, 'a thing landed');
1072
1161
  });
1073
1162
 
1163
+ console.log('\nthe pin reaches the project\'s GitHub repo — pushed from the HUB (task 1004065):');
1164
+
1165
+ // THE BUG. A hosted project's upgrade runs connected to the PROJECT's database, which
1166
+ // has no provisioning table (ADR 0281), so upgrade.js could never find the owner's
1167
+ // token, fell back to `git push origin` over a read-only deploy key (ADR 0176), was
1168
+ // refused, and the intent still resolved `done`. hermeslines-marketing's box ran
1169
+ // 1.19.994 while its GitHub repo still pinned 1.19.881. These cases drive the whole
1170
+ // move so the push is proven to happen, from where the token actually lives, AFTER a
1171
+ // confirmed upgrade, and to be reported when it cannot.
1172
+
1173
+ const OWNER_TOKEN = 'ghu_OWNER_TOKEN_never_in_a_command';
1174
+
1175
+ function pinDeps({ served = '1.19.762', token = OWNER_TOKEN, branch = 'main', pushResult = null, originResult = null, upgradeThrows = false } = {}) {
1176
+ const d = makeDeps({ served });
1177
+ const ran = [];
1178
+ const writes = [];
1179
+ const tokenAsks = [];
1180
+ d.ran = [];
1181
+ d.ranWith = ran;
1182
+ d.exec = (cmd, opts) => {
1183
+ ran.push({ cmd, cwd: opts && opts.cwd, env: opts && opts.env });
1184
+ d.ran.push(cmd);
1185
+ if (/upgrade\.js/.test(cmd) && upgradeThrows) throw new Error('health check failed after restart — rolled back');
1186
+ if (/\/version/.test(cmd)) return { ok: true, stdout: JSON.stringify({ coreVersion: served }) };
1187
+ if (/symbolic-ref/.test(cmd)) {
1188
+ return branch ? { ok: true, stdout: `${branch}\n` } : { ok: true, softFailed: true, stdout: '', stderr: 'fatal: ref HEAD is not a symbolic ref' };
1189
+ }
1190
+ if (/push https:\/\/x-access-token@github\.com\//.test(cmd)) return pushResult || { ok: true, stdout: '' };
1191
+ if (/ push origin /.test(cmd)) return originResult || { ok: true, stdout: '' };
1192
+ return { ok: true, stdout: '' };
1193
+ };
1194
+ d.controlWriteFile = (p, body, o) => { writes.push({ p, body, mode: o && o.mode }); return { ok: true }; };
1195
+ d.getGithubToken = async (builderId) => { tokenAsks.push(builderId); return token ? { token } : null; };
1196
+ d._writes = writes;
1197
+ d._tokenAsks = tokenAsks;
1198
+ return d;
1199
+ }
1200
+
1201
+ const hosted = (over = {}) => inst({
1202
+ hosting_shape: 'cloud-host', slug: 'acme-site', port: 3011, db_name: 'acme-site',
1203
+ target_ref: 'acme/site', owner_builder_id: 5, ...over,
1204
+ });
1205
+ const tokenPush = (d) => d.ranWith.find((c) => /push https:\/\/x-access-token@github\.com\//.test(c.cmd));
1206
+ const originPush = (d) => d.ranWith.find((c) => / push origin /.test(c.cmd));
1207
+ const refusedBy = (stderr) => ({ ok: true, softFailed: true, stdout: '', stderr });
1208
+
1209
+ await t('a hosted project\'s upgrade COMMITS the pin on the box and leaves the push to the hub', async () => {
1210
+ const d = pinDeps();
1211
+ await R.coreUpgradeInstance(hosted(), d, intent());
1212
+ const cmd = upgradeCall(d);
1213
+ assert.ok(cmd.includes('--commit-pin'), 'the pin is still committed — an uncommitted pin is the silent-revert bug');
1214
+ assert.ok(cmd.includes('--pin-no-push'), 'and upgrade.js no longer attempts a push it has no credential for');
1215
+ });
1216
+
1217
+ await t('the platform itself keeps pushing its own pin — its key CAN write its own repo', async () => {
1218
+ const d = makeDeps({ served: '1.19.762' });
1219
+ await R.coreUpgradeInstance(inst(), d, intent());
1220
+ assert.doesNotMatch(upgradeCall(d), /--pin-no-push/, 'unchanged for the control plane');
1221
+ });
1222
+
1223
+ await t('the preview carries no push flag at all — it never gets as far as a pin', () => {
1224
+ const peek = R.upgradeInvocation(hosted(), '1.19.762', { dryRun: true });
1225
+ assert.doesNotMatch(peek.cmd, /--pin-no-push|--commit-pin/);
1226
+ });
1227
+
1228
+ await t('THE HUB PUSHES the committed pin to the project\'s repo with the owner\'s token read from the HUB db', async () => {
1229
+ const d = pinDeps();
1230
+ const r = await R.coreUpgradeInstance(hosted(), d, intent());
1231
+ assert.equal(r.ok, true);
1232
+ assert.deepEqual(d._tokenAsks, [5], 'the OWNER\'s token, looked up by the owner id on the provisioning row');
1233
+ const push = tokenPush(d);
1234
+ assert.ok(push, 'the hub pushed');
1235
+ assert.ok(push.cmd.includes('https://x-access-token@github.com/acme/site.git HEAD:main'), `to the row's target_ref, main — got ${push.cmd}`);
1236
+ assert.ok(push.cmd.includes(`-C ${RP.standaloneRoot(hosted())} `), 'from the project\'s own checkout');
1237
+ assert.equal(r.repo.pushed, true);
1238
+ assert.equal(r.repo.via, 'owner-token');
1239
+ assert.ok(!r.doneNote, 'a clean push leaves nothing to report');
1240
+ });
1241
+
1242
+ await t('the push happens AFTER the confirmed upgrade, never before it', async () => {
1243
+ const d = pinDeps();
1244
+ await R.coreUpgradeInstance(hosted(), d, intent());
1245
+ const at = (re) => d.ranWith.findIndex((c) => re.test(c.cmd));
1246
+ assert.ok(at(/upgrade\.js/) >= 0 && at(/upgrade\.js/) < at(/x-access-token@github\.com/), 'the pin must exist before it can be pushed');
1247
+ });
1248
+
1249
+ await t('the TOKEN never appears in a command line, and its credential file is removed', async () => {
1250
+ const d = pinDeps();
1251
+ await R.coreUpgradeInstance(hosted(), d, intent());
1252
+ for (const c of d.ranWith) assert.ok(!c.cmd.includes(OWNER_TOKEN), `argv reaches ps and the journal — leaked in: ${c.cmd}`);
1253
+ assert.equal(d._writes.length, 1, 'one credential file');
1254
+ const cred = d._writes[0];
1255
+ assert.ok(cred.body.includes(OWNER_TOKEN), 'the token rides a file');
1256
+ assert.equal(cred.mode, 0o600, 'readable by the runner alone');
1257
+ assert.ok(tokenPush(d).cmd.includes(`store --file=${cred.p}`), 'git reads it through the store helper');
1258
+ assert.ok(d.ranWith.some((c) => c.cmd === `rm -f ${cred.p}`), 'and it is deleted afterwards');
1259
+ });
1260
+
1261
+ await t('the push is HARDENED: helper list reset, no hooks, no prompt', async () => {
1262
+ // Helpers are cumulative — a global one would answer first with some other account.
1263
+ // A hook in the project's checkout would otherwise run as the RUNNER on this push.
1264
+ const d = pinDeps();
1265
+ await R.coreUpgradeInstance(hosted(), d, intent());
1266
+ const push = tokenPush(d);
1267
+ assert.match(push.cmd, /-c credential\.helper= -c credential\.helper='store --file=/, 'the empty entry resets the list first');
1268
+ assert.match(push.cmd, /-c core\.hooksPath=\/dev\/null /, 'no hook runs');
1269
+ assert.equal(push.env && push.env.GIT_TERMINAL_PROMPT, '0', 'a refused credential fails instead of waiting at a prompt');
1270
+ assert.doesNotMatch(push.cmd, /--force|\+HEAD/, 'never forced');
1271
+ });
1272
+
1273
+ await t('the credential file is removed even when the push FAILS', async () => {
1274
+ const d = pinDeps({ pushResult: refusedBy('remote: Write access to repository not granted.\nfatal: unable to access: The requested URL returned error: 403') });
1275
+ await R.coreUpgradeInstance(hosted(), d, intent());
1276
+ assert.ok(d.ranWith.some((c) => c.cmd === `rm -f ${d._writes[0].p}`));
1277
+ });
1278
+
1279
+ await t('NO TOKEN STORED: the box\'s own remote is tried, and a refusal resolves done WITH a note', async () => {
1280
+ const d = pinDeps({ token: null, originResult: refusedBy('ERROR: The key you are authenticating with has been marked as read only.\nfatal: Could not read from remote repository.') });
1281
+ const r = await R.coreUpgradeInstance(hosted(), d, intent());
1282
+ assert.equal(r.ok, true, 'the upgrade DID land and the site runs it — that is not a failure');
1283
+ assert.ok(originPush(d), 'the remote was tried, in case it can write');
1284
+ assert.equal(d._writes.length, 0, 'no credential file without a credential');
1285
+ assert.equal(r.repo.pushed, false);
1286
+ assert.equal(r.repo.code, 'no_access');
1287
+ assert.match(r.doneNote, /^repo_not_updated:no_access: /, 'the note the web tier reads');
1288
+ assert.match(r.doneNote, /an hour/, 'and it says WHY there was no access, in words');
1289
+ });
1290
+
1291
+ await t('NO TOKEN, but the box\'s remote CAN write: pushed that way, nothing to report', async () => {
1292
+ const d = pinDeps({ token: null });
1293
+ const r = await R.coreUpgradeInstance(hosted(), d, intent());
1294
+ assert.equal(r.repo.pushed, true);
1295
+ assert.equal(r.repo.via, 'origin');
1296
+ assert.ok(!r.doneNote);
1297
+ assert.match(originPush(d).cmd, /-c core\.hooksPath=\/dev\/null /, 'hardened the same way');
1298
+ });
1299
+
1300
+ await t('GitHub REFUSES the owner\'s token: named as a refusal, not as "no token stored"', async () => {
1301
+ const d = pinDeps({ pushResult: refusedBy('remote: Write access to repository not granted.\nfatal: unable to access: The requested URL returned error: 403') });
1302
+ const r = await R.coreUpgradeInstance(hosted(), d, intent());
1303
+ assert.equal(r.repo.code, 'refused');
1304
+ assert.match(r.doneNote, /^repo_not_updated:refused: /);
1305
+ assert.match(r.doneNote, /Write access to repository not granted/, 'GitHub\'s own words ride along');
1306
+ assert.equal(originPush(d), undefined, 'a read-only remote cannot succeed where the token was refused');
1307
+ });
1308
+
1309
+ await t('GitHub HAS COMMITS THE BOX DOES NOT: reported, and nothing is forced over them', async () => {
1310
+ const d = pinDeps({ pushResult: refusedBy(' ! [rejected] HEAD -> main (fetch first)\nerror: failed to push some refs to \'https://github.com/acme/site.git\'') });
1311
+ const r = await R.coreUpgradeInstance(hosted(), d, intent());
1312
+ assert.equal(r.repo.code, 'behind');
1313
+ assert.match(r.doneNote, /^repo_not_updated:behind: /);
1314
+ assert.ok(!d.ranWith.some((c) => /--force|\+HEAD|reset --hard|rebase/.test(c.cmd)), 'the box never rewrites anyone\'s history');
1315
+ });
1316
+
1317
+ await t('a push that fails some OTHER way is reported with git\'s line, not guessed at', async () => {
1318
+ const d = pinDeps({ pushResult: refusedBy('fatal: unable to access \'https://github.com/acme/site.git/\': Could not resolve host: github.com') });
1319
+ const r = await R.coreUpgradeInstance(hosted(), d, intent());
1320
+ assert.equal(r.repo.code, 'failed');
1321
+ assert.match(r.doneNote, /Could not resolve host/);
1322
+ });
1323
+
1324
+ await t('NO REPOSITORY RECORDED: reported, and no git is run after the upgrade', async () => {
1325
+ const d = pinDeps();
1326
+ const r = await R.coreUpgradeInstance(hosted({ target_ref: null }), d, intent());
1327
+ assert.equal(r.repo.code, 'no_repo');
1328
+ assert.match(r.doneNote, /^repo_not_updated:no_repo: /);
1329
+ assert.equal(d._tokenAsks.length, 0, 'no token is read for a push that cannot happen');
1330
+ assert.ok(!d.ranWith.some((c) => /\bgit\b/.test(c.cmd)), 'nothing touched the checkout');
1331
+ });
1332
+
1333
+ await t('a checkout not on a branch is pushed NOWHERE — HEAD:main from a detached head would be a guess', async () => {
1334
+ const d = pinDeps({ branch: null });
1335
+ const r = await R.coreUpgradeInstance(hosted(), d, intent());
1336
+ assert.equal(r.repo.code, 'no_branch');
1337
+ assert.equal(tokenPush(d), undefined);
1338
+ assert.equal(d._tokenAsks.length, 0, 'and no token is read for it');
1339
+ });
1340
+
1341
+ await t('a branch name that is not a plain ref never reaches the shell', async () => {
1342
+ const d = pinDeps({ branch: 'main;touch /tmp/pwn' });
1343
+ const r = await R.coreUpgradeInstance(hosted(), d, intent());
1344
+ assert.equal(r.repo.code, 'no_branch');
1345
+ assert.ok(!d.ranWith.some((c) => /pwn/.test(c.cmd)), 'the name was refused, not interpolated');
1346
+ });
1347
+
1348
+ await t('a FAILED upgrade pushes nothing — only a confirmed pin is worth saving', async () => {
1349
+ const d = pinDeps({ upgradeThrows: true });
1350
+ const r = await R.coreUpgradeInstance(hosted(), d, intent());
1351
+ assert.equal(r.ok, false);
1352
+ assert.equal(tokenPush(d), undefined);
1353
+ assert.equal(d._tokenAsks.length, 0);
1354
+ });
1355
+
1356
+ await t('the event log says GitHub was not updated, so the history is not a clean success either', async () => {
1357
+ const events = [];
1358
+ const d = pinDeps({ token: null, originResult: refusedBy('ERROR: The key you are authenticating with has been marked as read only.') });
1359
+ d.provisioning = { ...d.provisioning, recordEvent: async (_db, e) => { events.push(e); } };
1360
+ await R.coreUpgradeInstance(hosted(), d, intent());
1361
+ assert.equal(events.length, 1);
1362
+ assert.match(events[0].detail, /GitHub was not updated/);
1363
+ });
1364
+
1365
+ await t('every note the runner writes is one the web tier reads back, code and sentence intact', () => {
1366
+ for (const code of Object.keys(REPO_NOTES)) {
1367
+ const note = repoNote(code, '(git: something)');
1368
+ const w = parseRepoNote(note);
1369
+ assert.ok(w, `${code} parses`);
1370
+ assert.equal(w.code, code);
1371
+ assert.ok(w.text.startsWith(REPO_NOTES[code]), `${code}: the sentence survives`);
1372
+ assert.ok(note.length <= 600, `${code}: fits the page's bound`);
1373
+ }
1374
+ });
1375
+
1376
+ await t('the DRAIN LOOP resolves a done-with-a-note move WITH its note, still as done', async () => {
1377
+ // Driven through the real dispatch: the note is only worth anything if it reaches the
1378
+ // intent row the deploy page reads.
1379
+ const P2 = require('../scripts/gds/provision.js');
1380
+ let claims = 0; const resolved = []; const statusWrites = [];
1381
+ const d = pinDeps({ token: null, originResult: refusedBy('ERROR: The key you are authenticating with has been marked as read only.') });
1382
+ d.provisioning = {
1383
+ claimNextIntent: async () => (claims++ === 0 ? { id: 12, instance_id: 7, action: 'core-upgrade', attempts: 1, target_version: '1.19.762', requested_by: 'api:archon:42' } : null),
1384
+ getInstanceById: async () => hosted(),
1385
+ resolveIntent: async (_db, id, state, note) => { resolved.push({ id, state, note }); },
1386
+ setInstanceStatus: async (_db, _id, s, patch) => { statusWrites.push({ s, patch }); },
1387
+ recordEvent: async () => {}, clearErrorNote: async () => {},
1388
+ claimNextOAuthManifest: async () => null, resolveOAuthManifest: async () => {},
1389
+ };
1390
+ const r = await P2.cmdRunIntents(d);
1391
+ assert.equal(r.errors, 0, JSON.stringify(r));
1392
+ assert.equal(resolved.length, 1);
1393
+ assert.equal(resolved[0].state, 'done', 'the upgrade landed — never an error');
1394
+ assert.match(String(resolved[0].note), /^repo_not_updated:no_access: /, 'and the note travels to the row');
1395
+ assert.equal(statusWrites.length, 0, 'a healthy instance is not marked anything');
1396
+ });
1397
+
1074
1398
  console.log(`\ncore_upgrade_runner: ${passed} passed, ${failed} failed`);
1075
1399
  if (failed) process.exit(1);
@@ -0,0 +1,153 @@
1
+ // tests/credential_presence.mjs — the host credential sweep reports names and presence,
2
+ // never a value, and the inventory it sweeps is the one docs/credentials.md documents
3
+ // (task 1002880).
4
+ //
5
+ // Every value below is invented for the test and is chosen to be easy to spot: the
6
+ // "never a value" cases scan the whole report for every value AND every 4-character
7
+ // fragment of one, because a leak of a prefix ("ghp_…") is still a leak.
8
+ //
9
+ // Run: node --test tests/credential_presence.mjs
10
+
11
+ import { test } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { spawnSync } from 'node:child_process';
14
+ import { createRequire } from 'node:module';
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const SCRIPT = path.join(ROOT, 'scripts', 'gds', 'credential-presence.js');
23
+ const { CREDENTIALS, parseEnvFile, sweep, format, readFiles } = require('../scripts/gds/credential-presence.js');
24
+ const { envPrefix } = require('../src/instance-config.js');
25
+ const P = envPrefix();
26
+
27
+ // Random-looking on purpose: a value that shares four characters with a variable name
28
+ // ("CLIE" in CLIENT_ID) would make the fragment scan below flag the name, not a leak.
29
+ const SECRETS = {
30
+ client: 'q7Zx9vLp2Rk8', pat: 'w3Ty6uHb1Nm4', box: 'e5Rc8oJd0Qs2', key: 'f9Gh2kAa7Vb3',
31
+ };
32
+ const WEB_ENV = [
33
+ '# /etc/example/web.env — test fixture',
34
+ `${P}_GITHUB_CLIENT_ID=Iv1clientIDqq9`,
35
+ `${P}_GITHUB_CLIENT_SECRET="${SECRETS.client}"`,
36
+ `GITHUB_PUSH_TOKEN=${SECRETS.pat}`,
37
+ `${P}_GITHUB_OWNER=example-owner`,
38
+ `${P}_GITHUB_REPO=example-repo`,
39
+ `BUILDER_SECRET_KEY=${SECRETS.key}`,
40
+ 'DISCORD_BOT_TOKEN=',
41
+ 'PORT=4100',
42
+ ].join('\n');
43
+
44
+ function fragmentsOf(value) {
45
+ const out = new Set([value]);
46
+ for (let i = 0; i + 4 <= value.length; i++) out.add(value.slice(i, i + 4));
47
+ return [...out];
48
+ }
49
+ function assertNoValueIn(text) {
50
+ for (const v of Object.values(SECRETS)) {
51
+ for (const frag of fragmentsOf(v)) assert.ok(!text.includes(frag), `the report leaked a fragment of a value: "${frag}"`);
52
+ }
53
+ }
54
+
55
+ test('parseEnvFile reads a systemd EnvironmentFile: comments, blanks, quotes, empties', () => {
56
+ const env = parseEnvFile('# comment\n\nA=1\nB="two words"\nC=\'three\'\nD=\n E = spaced \nnot a line\n');
57
+ assert.deepEqual(env, { A: '1', B: 'two words', C: 'three', D: '', E: 'spaced' });
58
+ });
59
+
60
+ test('a configured host reads as present, row by row, with the NAME that satisfied each', () => {
61
+ const r = sweep({ files: { 'web.env': { env: parseEnvFile(WEB_ENV) }, 'box.env': { env: { DO_API_TOKEN: SECRETS.box } }, 'spaces.env': { env: {} } } });
62
+ const row = (id) => r.rows.find((x) => x.id === id);
63
+ assert.equal(row('github-sign-in').state, 'set');
64
+ assert.deepEqual(row('github-sign-in').satisfiedBy, [`${P}_GITHUB_CLIENT_ID`, `${P}_GITHUB_CLIENT_SECRET`]);
65
+ assert.equal(row('publish-pat').state, 'set');
66
+ assert.equal(row('publish-app').state, 'missing', 'the App is absent...');
67
+ assert.equal(r.groups.publish.satisfied, true, '...but the publish group is satisfied by the PAT');
68
+ assert.equal(row('discord-bot').state, 'empty', 'a name present with no value is called out, not treated as set');
69
+ assert.equal(row('digitalocean').state, 'set');
70
+ assert.equal(row('cloudflare').state, 'missing');
71
+ assert.deepEqual(r.requiredMissing, [], 'every required row, or its alternative, is present');
72
+ assert.equal(r.ok, true);
73
+ });
74
+
75
+ test('THE ONE RULE: no value, and no fragment of one, ever reaches the report or --json', () => {
76
+ const r = sweep({ files: { 'web.env': { env: parseEnvFile(WEB_ENV) }, 'box.env': { env: { DO_API_TOKEN: SECRETS.box } }, 'spaces.env': { env: {} } } });
77
+ assertNoValueIn(format(r, { etc: '/etc/example' }));
78
+ assertNoValueIn(JSON.stringify(r));
79
+ });
80
+
81
+ test('a missing required credential fails the sweep and is named', () => {
82
+ const env = parseEnvFile(WEB_ENV);
83
+ delete env.GITHUB_PUSH_TOKEN;
84
+ const r = sweep({ files: { 'web.env': { env }, 'box.env': { env: {} }, 'spaces.env': { env: {} } } });
85
+ assert.equal(r.groups.publish.satisfied, false, 'neither the App nor the PAT is set');
86
+ assert.ok(r.requiredMissing.includes('publish-app') && r.requiredMissing.includes('publish-pat'));
87
+ assert.equal(r.ok, false);
88
+ assert.match(format(r), /REQUIRED and missing: .*publish-app/);
89
+ });
90
+
91
+ test('the App needs BOTH halves: an id without a key is not a publish credential', () => {
92
+ const r = sweep({ files: { 'web.env': { env: { GITHUB_APP_ID: '123' } }, 'box.env': { env: {} }, 'spaces.env': { env: {} } } });
93
+ const app = r.rows.find((x) => x.id === 'publish-app');
94
+ assert.equal(app.state, 'missing');
95
+ assert.deepEqual(app.missing, ['GITHUB_APP_PRIVATE_KEY']);
96
+ });
97
+
98
+ test('a prefixed name also answers to its legacy spelling, the way the server reads it', () => {
99
+ const env = { CLOUDBONGOS_GITHUB_CLIENT_ID: 'x-id', CLOUDBONGOS_GITHUB_CLIENT_SECRET: 'x-secret' };
100
+ const r = sweep({ files: { 'web.env': { env }, 'box.env': { env: {} }, 'spaces.env': { env: {} } } });
101
+ const row = r.rows.find((x) => x.id === 'github-sign-in');
102
+ if (P === 'CLOUDBONGOS') return; // the legacy spelling IS canonical on such an instance
103
+ assert.equal(row.state, 'set');
104
+ assert.deepEqual(row.satisfiedBy, ['CLOUDBONGOS_GITHUB_CLIENT_ID', 'CLOUDBONGOS_GITHUB_CLIENT_SECRET']);
105
+ });
106
+
107
+ test('the secret-box key counts when it lives in the key file instead of the env', () => {
108
+ const r = sweep({ files: { 'web.env': { env: {} }, 'box.env': { env: {} }, 'spaces.env': { env: {} } }, keyFileExists: true });
109
+ assert.equal(r.rows.find((x) => x.id === 'secret-box-key').state, 'set');
110
+ });
111
+
112
+ test('an unreadable web.env makes the answer UNKNOWN (exit 2), never a clean pass', () => {
113
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cred-etc-'));
114
+ try {
115
+ const files = readFiles(dir);
116
+ assert.ok(files['web.env'].error, 'no web.env here');
117
+ assert.deepEqual(files['spaces.env'], { env: {} }, 'an absent spaces.env only means offsite backup was never set up');
118
+ const r = spawnSync(process.execPath, [SCRIPT, '--etc', dir], { cwd: ROOT, encoding: 'utf8' });
119
+ assert.equal(r.status, 2, r.stdout + r.stderr);
120
+ assert.match(r.stdout, /could not read/);
121
+ } finally {
122
+ fs.rmSync(dir, { recursive: true, force: true });
123
+ }
124
+ });
125
+
126
+ test('the CLI, run against a real env dir, reports presence and prints no value', () => {
127
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cred-etc-'));
128
+ try {
129
+ fs.writeFileSync(path.join(dir, 'web.env'), WEB_ENV);
130
+ fs.writeFileSync(path.join(dir, 'box.env'), `DO_API_TOKEN=${SECRETS.box}\nCLOUDFLARE_API_TOKEN=\n`);
131
+ const r = spawnSync(process.execPath, [SCRIPT, '--etc', dir], { cwd: ROOT, encoding: 'utf8' });
132
+ assert.equal(r.status, 0, r.stdout + r.stderr);
133
+ assert.match(r.stdout, /✓ github-sign-in/);
134
+ assert.match(r.stdout, /! cloudflare .*set but EMPTY: CLOUDFLARE_API_TOKEN/);
135
+ assertNoValueIn(r.stdout + r.stderr);
136
+ const j = spawnSync(process.execPath, [SCRIPT, '--etc', dir, '--json'], { cwd: ROOT, encoding: 'utf8' });
137
+ assert.equal(JSON.parse(j.stdout).ok, true);
138
+ assertNoValueIn(j.stdout + j.stderr);
139
+ } finally {
140
+ fs.rmSync(dir, { recursive: true, force: true });
141
+ }
142
+ });
143
+
144
+ test('docs/credentials.md documents every variable the sweep checks', () => {
145
+ const doc = fs.readFileSync(path.join(ROOT, 'docs', 'credentials.md'), 'utf8');
146
+ for (const row of CREDENTIALS) {
147
+ for (const name of row.names) {
148
+ const shown = row.prefixed ? `<PREFIX>_${name}` : name;
149
+ assert.ok(doc.includes('`' + shown + '`'), `docs/credentials.md must name \`${shown}\` (row ${row.id})`);
150
+ }
151
+ }
152
+ assert.match(doc, /never a value|No value of any credential/i, 'the page states the one rule');
153
+ });
@@ -18,6 +18,7 @@ import fs from 'node:fs';
18
18
  import os from 'node:os';
19
19
  import path from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
+ import { macShapedTools } from './helpers.mjs';
21
22
 
22
23
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
24
  const SCRIPT = path.join(ROOT, 'scripts', 'gds', 'db-backup-nightly.sh');
@@ -29,13 +30,15 @@ const ENV = 'gds_backup_'.toUpperCase();
29
30
  // A fake pg_dump: a plain-format dump of `rows` rows, closed the way pg_dump closes one
30
31
  // (including the \unrestrict line patched servers append) unless it is `cut` short, and
31
32
  // exiting `exitCode`.
32
- // `seed` pre-places dumps in the shared dir as [filename, ageInDays].
33
- function runBackup({ rows = 40, cut = false, exitCode = 0, env = {}, seed = [] } = {}) {
33
+ // `seed` pre-places dumps in the shared dir as [filename, ageInDays]; `mac` puts
34
+ // BSD/macOS-shaped stat and wc first on PATH too.
35
+ function runBackup({ rows = 40, cut = false, exitCode = 0, env = {}, seed = [], mac = false } = {}) {
34
36
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'db-backup-'));
35
37
  const bin = path.join(tmp, 'bin');
36
38
  const dir = path.join(tmp, 'backups');
37
39
  fs.mkdirSync(bin);
38
40
  fs.mkdirSync(dir);
41
+ if (mac) macShapedTools(bin);
39
42
  for (const [name, ageDays] of seed) {
40
43
  const f = path.join(dir, name);
41
44
  fs.writeFileSync(f, '');
@@ -50,7 +53,7 @@ function runBackup({ rows = 40, cut = false, exitCode = 0, env = {}, seed = [] }
50
53
  encoding: 'utf8',
51
54
  env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, [`${ENV}DB`]: 'smallproj', [`${ENV}DIR`]: dir, ...env },
52
55
  });
53
- return { status: r.status, stdout: r.stdout, stderr: r.stderr, dumps: fs.readdirSync(dir).filter((f) => f.endsWith('.sql.gz')) };
56
+ return { status: r.status, stdout: r.stdout, stderr: r.stderr, dir, dumps: fs.readdirSync(dir).filter((f) => f.endsWith('.sql.gz')) };
54
57
  }
55
58
 
56
59
  test('a complete dump of a small project is kept, however small', { skip }, () => {
@@ -93,6 +96,21 @@ test('the prune and the count reach only this database\'s own dumps, never a lon
93
96
  assert.match(r.stdout, /ok · 1 dump\(s\) retained/, 'the count is this database\'s dumps only');
94
97
  });
95
98
 
99
+ test('it runs the same on macOS, where stat refuses -c and wc pads its numbers (task 1004170)', { skip }, () => {
100
+ // Every case above used to die on a Mac with "stat: illegal option -- c" while CI's
101
+ // Linux passed them, so the red never showed in Actions. This runs the Mac's tools here.
102
+ const attach = fs.mkdtempSync(path.join(os.tmpdir(), 'db-backup-attach-'));
103
+ for (const f of ['a.png', 'b.png']) fs.writeFileSync(path.join(attach, f), 'x');
104
+ const r = runBackup({ mac: true, seed: [['smallproj-2026-01-01T000000Z.sql.gz', 40]], env: { BUG_ATTACHMENTS_DIR: attach } });
105
+ assert.equal(r.status, 0, r.stderr);
106
+ assert.equal(r.dumps.length, 1, 'the expired dump is pruned and the new one kept');
107
+ const size = fs.statSync(path.join(r.dir, r.dumps[0])).size;
108
+ assert.match(r.stdout, new RegExp(`wrote .*\\.sql\\.gz \\(${size} bytes\\)`), 'the byte count is the real size, unpadded');
109
+ assert.match(r.stdout, /\(2 attachment\(s\)\)/);
110
+ assert.match(r.stdout, /pruned 1 dump\(s\)/);
111
+ assert.match(r.stdout, /ok · 1 dump\(s\) retained/);
112
+ });
113
+
96
114
  test('no fixed size floor is baked in for every instance', () => {
97
115
  const src = fs.readFileSync(SCRIPT, 'utf8');
98
116
  assert.doesNotMatch(src, new RegExp(`${ENV}MIN_BYTES:-[1-9]`), 'a default floor fails every database smaller than it');