@bongos/core 1.19.876 → 1.19.878

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 (47) hide show
  1. package/.bongos-core.json +110 -40
  2. package/.gitattributes +6 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -0
  5. package/clients/bongos-client/index.cjs +4 -0
  6. package/clients/bongos-client/index.d.ts +7 -0
  7. package/clients/bongos-client/index.mjs +4 -0
  8. package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +12 -1
  9. package/docs/api/openapi.json +149 -3
  10. package/docs/api-reference.md +4 -2
  11. package/docs/copy-inventory.md +7 -6
  12. package/docs/copy-registry.json +16 -7
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/recipes/autobongos-windows-host.md +163 -0
  15. package/docs/recipes/instance-deploy.md +29 -3
  16. package/modules/autonomy/cadence.js +276 -0
  17. package/modules/autonomy/db.js +47 -0
  18. package/modules/autonomy/digest.js +344 -0
  19. package/modules/autonomy/migrations/autonomy_002_runner_heartbeat.sql +93 -0
  20. package/modules/autonomy/routes/autonomy.js +69 -0
  21. package/modules/autonomy/runner-health.js +97 -0
  22. package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
  23. package/modules/hall-ui/public/gate.html +4 -1
  24. package/modules/hall-ui/public/gate.js +51 -2
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/scripts/gds/autobongos-digest.js +123 -0
  28. package/scripts/gds/autobongos-run.js +375 -11
  29. package/scripts/gds/autobongos-service.cmd +38 -0
  30. package/scripts/gds/autobongos-service.ps1 +135 -0
  31. package/scripts/gds/deploy-main-audit-gate.sh +90 -0
  32. package/scripts/gds/main-audit.js +26 -4
  33. package/scripts/gds/provision-core-upgrade.js +23 -3
  34. package/scripts/gds/run-unit-tests.js +16 -1
  35. package/src/module-api.js +1 -1
  36. package/tests/autobongos_cadence.mjs +602 -0
  37. package/tests/autonomy_digest.mjs +302 -0
  38. package/tests/autonomy_runner_health.mjs +135 -0
  39. package/tests/bump_version.mjs +12 -1
  40. package/tests/cli_package.mjs +11 -1
  41. package/tests/core_upgrade_runner.mjs +43 -1
  42. package/tests/deploy_audit_gate.mjs +154 -0
  43. package/tests/deploy_divergence_line.mjs +47 -1
  44. package/tests/helpers.mjs +61 -0
  45. package/tests/main_audit_db_roster.mjs +97 -0
  46. package/tests/publish_watch.mjs +9 -0
  47. package/tests/setup_device_flow_fallback.mjs +2 -1
@@ -0,0 +1,154 @@
1
+ // tests/deploy_audit_gate.mjs
2
+ //
3
+ // The deployed marker advances ONLY on success (task 1001426, SEC #863 / ADR 0043).
4
+ //
5
+ // The gate audits what a deploy pulled, so it can only run AFTER the working tree has moved
6
+ // to the new commit. That ordering is what the original row calls the marker-bypass: refuse
7
+ // the deploy, exit, and leave the tree sitting on the refused commit, and the NEXT deploy
8
+ // computes its "previously deployed" SHA from the poison. That delta is then never audited
9
+ // again — and it is already checked out. The gate closes it by putting the tree back.
10
+ //
11
+ // There is no marker FILE on a standalone instance; the marker is the checked-out HEAD.
12
+ // These cases pin that, against a stub auditor so the assertions are about the gate's own
13
+ // control flow and never about rank resolution (covered in tests/main_audit_db_roster.mjs).
14
+ //
15
+ // Run: node --test tests/deploy_audit_gate.mjs
16
+
17
+ import assert from 'node:assert/strict';
18
+ import { test } from 'node:test';
19
+ import { execFileSync, spawnSync } from 'node:child_process';
20
+ import fs from 'node:fs';
21
+ import os from 'node:os';
22
+ import path from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const GATE = path.join(ROOT, 'scripts', 'gds', 'deploy-main-audit-gate.sh');
27
+
28
+ // A throwaway repo with two commits, checked out at the second — the state a deploy is in
29
+ // when the gate runs: PREV is what was deployed, HEAD is what was just pulled.
30
+ function makeRepo({ auditor = 'exit 0' } = {}) {
31
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-gate-'));
32
+ const git = (...args) => execFileSync('git', args, { cwd: dir, encoding: 'utf8' });
33
+ git('init', '-q', '-b', 'main');
34
+ git('config', 'user.email', 'test@example.com');
35
+ git('config', 'user.name', 'Test');
36
+ fs.writeFileSync(path.join(dir, 'a.txt'), 'one\n');
37
+ git('add', '-A'); git('commit', '-q', '-m', 'one');
38
+ const prev = git('rev-parse', 'HEAD').trim();
39
+ fs.writeFileSync(path.join(dir, 'a.txt'), 'two\n');
40
+ git('add', '-A'); git('commit', '-q', '-m', 'two (the pulled commit)');
41
+ const head = git('rev-parse', 'HEAD').trim();
42
+
43
+ if (auditor !== null) {
44
+ const gdsDir = path.join(dir, 'node_modules', '@bongos', 'core', 'scripts', 'gds');
45
+ fs.mkdirSync(gdsDir, { recursive: true });
46
+ fs.writeFileSync(path.join(gdsDir, 'main-audit.js'), `process.${auditor.replace('exit ', 'exit(')});\n`);
47
+ }
48
+ return { dir, prev, head, git };
49
+ }
50
+
51
+ function runGate(repo, env = {}) {
52
+ const r = spawnSync('sh', [GATE], {
53
+ cwd: repo.dir, encoding: 'utf8',
54
+ env: { ...process.env, ...env },
55
+ });
56
+ return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
57
+ }
58
+
59
+ const headOf = (repo) => repo.git('rev-parse', 'HEAD').trim();
60
+
61
+ test('a CLEAN audit leaves the deploy on the new commit and exits 0', () => {
62
+ const repo = makeRepo({ auditor: 'exit 0' });
63
+ try {
64
+ const r = runGate(repo, { OTB_DEPLOY_PREV_SHA: repo.prev });
65
+ assert.equal(r.code, 0, r.out);
66
+ assert.equal(headOf(repo), repo.head, 'a passing gate must not move the tree');
67
+ assert.match(r.out, /exact deploy delta/);
68
+ } finally { fs.rmSync(repo.dir, { recursive: true, force: true }); }
69
+ });
70
+
71
+ test('THE BUG: a REFUSED audit puts the checkout back, so the next deploy re-audits the same delta', () => {
72
+ const repo = makeRepo({ auditor: 'exit 1' });
73
+ try {
74
+ const r = runGate(repo, { OTB_DEPLOY_PREV_SHA: repo.prev });
75
+ assert.equal(r.code, 1, 'a refusal must abort the deploy');
76
+ assert.equal(
77
+ headOf(repo), repo.prev,
78
+ 'the tree must be restored to the previously-deployed SHA. Left advanced, the next '
79
+ + "deploy's OTB_DEPLOY_PREV_SHA is the POISONED commit and its delta is never re-audited",
80
+ );
81
+ assert.match(r.out, /DEPLOY ABORTED/);
82
+ assert.match(r.out, /advances only on success/);
83
+ } finally { fs.rmSync(repo.dir, { recursive: true, force: true }); }
84
+ });
85
+
86
+ test('the range handed to the auditor is the exact deploy delta, not a window', () => {
87
+ // Pinned because the whole row is named for an audit range that could be slipped under.
88
+ const repo = makeRepo({ auditor: null });
89
+ try {
90
+ const gdsDir = path.join(repo.dir, 'node_modules', '@bongos', 'core', 'scripts', 'gds');
91
+ fs.mkdirSync(gdsDir, { recursive: true });
92
+ fs.writeFileSync(path.join(gdsDir, 'main-audit.js'),
93
+ 'require("fs").writeFileSync("range.txt", process.env.OTB_AUDIT_RANGE || "UNSET");\n');
94
+ const r = runGate(repo, { OTB_DEPLOY_PREV_SHA: repo.prev });
95
+ assert.equal(r.code, 0, r.out);
96
+ assert.equal(
97
+ fs.readFileSync(path.join(repo.dir, 'range.txt'), 'utf8'), `${repo.prev}..HEAD`,
98
+ 'the auditor must receive <previously-deployed>..HEAD',
99
+ );
100
+ } finally { fs.rmSync(repo.dir, { recursive: true, force: true }); }
101
+ });
102
+
103
+ test('with no previous SHA there is nothing to restore to, and the gate says so', () => {
104
+ // The degraded mode: still audits (wide window), still refuses, but cannot undo the move.
105
+ // It must not pretend otherwise — a silent non-restore is the bug wearing a disguise.
106
+ const repo = makeRepo({ auditor: 'exit 1' });
107
+ try {
108
+ const r = runGate(repo, {});
109
+ assert.equal(r.code, 1);
110
+ assert.equal(headOf(repo), repo.head, 'nothing to restore to');
111
+ assert.match(r.out, /OTB_DEPLOY_PREV_SHA not set/);
112
+ assert.match(r.out, /cannot put the tree back/);
113
+ } finally { fs.rmSync(repo.dir, { recursive: true, force: true }); }
114
+ });
115
+
116
+ test('OTB_GATE_NO_RESTORE=1 leaves the tree to a caller that restores it itself', () => {
117
+ const repo = makeRepo({ auditor: 'exit 1' });
118
+ try {
119
+ const r = runGate(repo, { OTB_DEPLOY_PREV_SHA: repo.prev, OTB_GATE_NO_RESTORE: '1' });
120
+ assert.equal(r.code, 1);
121
+ assert.equal(headOf(repo), repo.head, 'the opt-out must actually opt out');
122
+ } finally { fs.rmSync(repo.dir, { recursive: true, force: true }); }
123
+ });
124
+
125
+ test('a missing auditor FAILS CLOSED — it never waves the deploy through', () => {
126
+ // The failure mode that would quietly disarm every instance: upgrade the core, lose the
127
+ // script, and a gate that treated "not found" as "nothing to report" would pass forever.
128
+ const repo = makeRepo({ auditor: null });
129
+ try {
130
+ const r = runGate(repo, { OTB_DEPLOY_PREV_SHA: repo.prev });
131
+ assert.equal(r.code, 1, 'no auditor must abort the deploy, not permit it');
132
+ assert.match(r.out, /main-audit\.js not found/);
133
+ } finally { fs.rmSync(repo.dir, { recursive: true, force: true }); }
134
+ });
135
+
136
+ test('OTB_AUDIT_STRICT=1 is passed through, and is OFF by default', () => {
137
+ const repo = makeRepo({ auditor: null });
138
+ try {
139
+ const gdsDir = path.join(repo.dir, 'node_modules', '@bongos', 'core', 'scripts', 'gds');
140
+ fs.mkdirSync(gdsDir, { recursive: true });
141
+ fs.writeFileSync(path.join(gdsDir, 'main-audit.js'),
142
+ 'require("fs").writeFileSync("argv.txt", process.argv.slice(2).join(" "));\n');
143
+
144
+ runGate(repo, { OTB_DEPLOY_PREV_SHA: repo.prev });
145
+ assert.equal(
146
+ fs.readFileSync(path.join(repo.dir, 'argv.txt'), 'utf8').includes('--strict'), false,
147
+ 'strict must default OFF: a lane committing under display names resolves to nothing, '
148
+ + 'so strict would block every deploy forever',
149
+ );
150
+
151
+ runGate(repo, { OTB_DEPLOY_PREV_SHA: repo.prev, OTB_AUDIT_STRICT: '1' });
152
+ assert.match(fs.readFileSync(path.join(repo.dir, 'argv.txt'), 'utf8'), /--strict/);
153
+ } finally { fs.rmSync(repo.dir, { recursive: true, force: true }); }
154
+ });
@@ -23,11 +23,13 @@ import { spawnSync } from 'node:child_process';
23
23
  import { createRequire } from 'node:module';
24
24
  import fs from 'node:fs';
25
25
  import path from 'node:path';
26
+ import { childEnv, API_BASE_ENV_PREFIXES } from './helpers.mjs';
26
27
  import { fileURLToPath } from 'node:url';
27
28
 
28
29
  const require = createRequire(import.meta.url);
29
30
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
31
  const { line } = require('../scripts/gds/deploy-divergence.js');
32
+ const ic = require('../src/instance-config.js');
31
33
  const SCRIPT = path.join(ROOT, 'scripts/gds/deploy-divergence.js');
32
34
 
33
35
  // Run `--line` exactly as SessionStart does. Returns {status, stdout} instead of
@@ -36,7 +38,7 @@ function runLine(env = {}) {
36
38
  const res = spawnSync(
37
39
  process.execPath,
38
40
  [SCRIPT, '--line', '--timeout-ms', '2500'],
39
- { cwd: ROOT, encoding: 'utf8', timeout: 30000, env: { ...process.env, ...env } },
41
+ { cwd: ROOT, encoding: 'utf8', timeout: 30000, env: childEnv(env) },
40
42
  );
41
43
  return { status: res.status, stdout: String(res.stdout || '').trim(), stderr: String(res.stderr || '') };
42
44
  }
@@ -101,6 +103,50 @@ test('--line exits 0 and stays silent when the instance is unreachable', () => {
101
103
  assert.equal(r.stdout, '', 'an unreachable instance must print NOTHING to stdout');
102
104
  });
103
105
 
106
+ test('childEnv strips EVERY spelling of the api base, so an offline stub cannot be shadowed', () => {
107
+ // THE BUG THIS PINS (task 1004049). The case above sets only LEGACY spellings.
108
+ // resolveApiBase() reads the canonical `<PREFIX>_API_BASE` FIRST, so on any box
109
+ // that exports it — every Bongos session box — the legacy stub was silently
110
+ // shadowed and `--line` went and read the LIVE instance. The assertion then
111
+ // compared production's real divergence banner against '' and failed. It passed in
112
+ // CI, where the canonical var is unset, which is why it survived: the test was only
113
+ // ever in control on the one machine nobody was debugging on.
114
+ //
115
+ // This is asserted against childEnv directly rather than by spawning --line at a
116
+ // real host, and the reason is the subject of this very task: an end-to-end version
117
+ // would pass or fail according to whether production happened to be mid-release at
118
+ // the time, which is another ambient dependency. The mechanism is what must hold,
119
+ // so the mechanism is what is pinned.
120
+ // The stub prefix is TAKEN FROM the imported list rather than typed, for the same
121
+ // single-source reason childEnv imports it — and because the repo's rename ratchet
122
+ // (goal 1000073 C5) counts a typed legacy prefix as a new literal.
123
+ const STUB = ic.LEGACY_ENV_PREFIXES.at(-1);
124
+ const STUB_VAR = `${STUB}_API_BASE`;
125
+ const DEAD = 'http://127.0.0.1:9';
126
+
127
+ // Every OTHER spelling points at a host that would answer — the shape of a real
128
+ // session box — so a survivor is the difference between offline and not.
129
+ const box = { PATH: process.env.PATH };
130
+ for (const prefix of API_BASE_ENV_PREFIXES) box[`${prefix}_API_BASE`] = 'https://example.invalid';
131
+
132
+ const out = childEnv({ [STUB_VAR]: DEAD }, { base: box });
133
+
134
+ for (const prefix of API_BASE_ENV_PREFIXES.filter((p) => p !== STUB)) {
135
+ assert.equal(out[`${prefix}_API_BASE`], undefined,
136
+ `${prefix}_API_BASE survived — it would shadow the stub the test set`);
137
+ }
138
+ assert.equal(out[STUB_VAR], DEAD, 'the override the test set must survive');
139
+ assert.equal(out.PATH, process.env.PATH, 'unrelated environment must be passed through untouched');
140
+
141
+ // The list is derived from instance-config, not copied here: a prefix added or
142
+ // sunset there must not need an edit in the test suite to keep working.
143
+ assert.ok(API_BASE_ENV_PREFIXES.includes(ic.envPrefix() || ic.FALLBACK_ENV_PREFIX),
144
+ 'the canonical prefix must be among those stripped');
145
+ for (const legacy of ic.LEGACY_ENV_PREFIXES) {
146
+ assert.ok(API_BASE_ENV_PREFIXES.includes(legacy), `legacy prefix ${legacy} must be stripped too`);
147
+ }
148
+ });
149
+
104
150
  test('--line exits 0 against the REAL instance (the 127 regression)', { timeout: 60000 }, () => {
105
151
  // The success path is the one that broke: a live socket to tear down is exactly
106
152
  // what process.exit() raced. Skips only if the network is unavailable, so it
package/tests/helpers.mjs CHANGED
@@ -70,6 +70,8 @@ export function makeSqlAwareClient(handler) {
70
70
  import fs from 'node:fs';
71
71
  import path from 'node:path';
72
72
  import { fileURLToPath } from 'node:url';
73
+ import { createRequire } from 'node:module';
74
+ import { execFileSync } from 'node:child_process';
73
75
 
74
76
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
75
77
 
@@ -434,3 +436,62 @@ export function sqlColumnLines(sql) {
434
436
  // whose NAME contained a scanned word would otherwise read as a column.
435
437
  .filter((l) => l && !/^(CREATE TABLE|CONSTRAINT|CHECK|\))/.test(l));
436
438
  }
439
+
440
+ // childEnv — build the environment for a spawned child so the API base the test
441
+ // SETS is the API base the child USES (task 1004049).
442
+ //
443
+ // The trap this closes: `src/instance-config.js resolveApiBase()` reads the
444
+ // CANONICAL `<PREFIX>_API_BASE` first and only then the legacy spellings. A test
445
+ // that passes `{ ...process.env, <LEGACY>_API_BASE: stub }` is therefore only in
446
+ // control on a box where the canonical var is UNSET. On a box where it is set —
447
+ // which is every Bongos session box, the tooling exports it — the canonical value
448
+ // wins, the test's stub is silently shadowed, and the child talks to the REAL
449
+ // instance.
450
+ //
451
+ // That is not hypothetical. It is how tests/deploy_divergence_line.mjs came to
452
+ // assert "stays silent when the instance is unreachable" while printing a live
453
+ // reading of the production host; how tests/setup_device_flow_fallback.mjs came to
454
+ // verify a token against the real /me instead of its local stub; and why
455
+ // tests/cli_package.mjs ran every verb against production and timed out after
456
+ // minutes instead of failing fast against a closed port (3.7s once fixed). All
457
+ // three passed in CI, where the canonical var is unset, and failed everywhere else.
458
+ //
459
+ // The prefix list is IMPORTED, never copied: instance-config.js is the single
460
+ // source that decides which spellings resolve, so a prefix added or sunset there
461
+ // must not need a matching edit here — a stale copy would reintroduce exactly the
462
+ // silent-shadowing bug this helper exists to prevent.
463
+ const helperRequire = createRequire(import.meta.url);
464
+ const ic = helperRequire(path.join(ROOT, 'src', 'instance-config.js'));
465
+ export const API_BASE_ENV_PREFIXES = [ic.envPrefix(), ic.FALLBACK_ENV_PREFIX, ...ic.LEGACY_ENV_PREFIXES]
466
+ .filter(Boolean)
467
+ .filter((p, i, a) => a.indexOf(p) === i);
468
+
469
+ export function childEnv(overrides = {}, { base = process.env } = {}) {
470
+ const env = { ...base };
471
+ for (const prefix of API_BASE_ENV_PREFIXES) delete env[`${prefix}_API_BASE`];
472
+ return { ...env, ...overrides };
473
+ }
474
+
475
+ // shallowCloneReason — why a history-reading test cannot run HERE, or null (task 1004049).
476
+ //
477
+ // A cloud session clones shallow, so a test that counts release commits or walks a
478
+ // date window sees a truncated log and asserts against a number that was never going
479
+ // to be right. That is not a defect in the code under test and must not read as one.
480
+ //
481
+ // It must not read as a PASS either. Task 1003993 (same goal) is open precisely
482
+ // because sanitize_rebuild.mjs counted two SKIPPED cases as passed and reported
483
+ // 760/760 while its only real coverage never ran. So the contract here is: return a
484
+ // REASON, and let the caller hand it to node:test's t.skip(), which reports the case
485
+ // under `# skipped` where a reader can see it — never a silent early return.
486
+ export function shallowCloneReason(root = ROOT) {
487
+ let shallow = false;
488
+ try {
489
+ shallow = execFileSync('git', ['rev-parse', '--is-shallow-repository'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
490
+ .trim() === 'true';
491
+ } catch {
492
+ return null; // not a git checkout at all: let the test fail honestly rather than skip
493
+ }
494
+ if (!shallow) return null;
495
+ return 'shallow clone — full history is unavailable here, so this case cannot be evaluated '
496
+ + '(git fetch --unshallow to run it)';
497
+ }
@@ -0,0 +1,97 @@
1
+ // tests/main_audit_db_roster.mjs
2
+ //
3
+ // main-audit.js `--db` must be able to REACH the roster (task 1001426).
4
+ //
5
+ // `--db` is the mode the deploy gate uses: the droplet has Postgres but no Bongos session,
6
+ // so it resolves builder ranks straight from the database. It called
7
+ // `require('../../src/bongos/db.js').getBuildersRoster(...)`. That function was carved out
8
+ // of the core db.js into the lifecycle module (goal 1000010's per-domain split) and this
9
+ // caller was not moved with it, so every `--db` run died with
10
+ //
11
+ // main-audit: --db roster read failed (db.getBuildersRoster is not a function)
12
+ //
13
+ // and returned an EMPTY rank map. An empty map is not a neutral outcome for this tool: every
14
+ // author then resolves to null, no blocker can fire, and the audit reports success — the
15
+ // vacuous green this file's own comments warn about for the API path. Non-strict said
16
+ // "clean" for the one reason it exists to catch; strict said BLOCKER on every commit.
17
+ //
18
+ // Two assertions, because the bug needed both to happen: the address must be live, and a
19
+ // failure to read it must not be laundered into an empty map.
20
+ //
21
+ // Run: node --test tests/main_audit_db_roster.mjs
22
+
23
+ import assert from 'node:assert/strict';
24
+ import { test } from 'node:test';
25
+ import path from 'node:path';
26
+ import { createRequire } from 'node:module';
27
+
28
+ const require = createRequire(import.meta.url);
29
+ const audit = require('../scripts/gds/main-audit.js');
30
+
31
+ test('the --db roster address is LIVE: that module really exports getBuildersRoster', () => {
32
+ // Resolved from main-audit.js's OWN directory, exactly as the lazy require() inside it
33
+ // does — so this fails for the same reason the gate would, and never for a different one.
34
+ const auditDir = path.dirname(require.resolve('../scripts/gds/main-audit.js'));
35
+ const mod = require(path.resolve(auditDir, audit.ROSTER_DB_MODULE));
36
+ assert.equal(
37
+ typeof mod.getBuildersRoster, 'function',
38
+ `${audit.ROSTER_DB_MODULE} must export getBuildersRoster — main-audit.js --db calls it, `
39
+ + 'and a moved export reads exactly like "no database" at the deploy gate',
40
+ );
41
+ });
42
+
43
+ test('the OLD address is genuinely dead — this is not a cosmetic rename', () => {
44
+ // Kept so the next reader can see the carve actually happened, rather than wondering
45
+ // whether the require was simply a typo that never worked.
46
+ const coreDb = require('../src/bongos/db.js');
47
+ assert.equal(
48
+ typeof coreDb.getBuildersRoster, 'undefined',
49
+ 'src/bongos/db.js no longer owns the roster; if it does again, collapse the two paths '
50
+ + 'rather than leaving main-audit.js pointing at whichever one happens to answer',
51
+ );
52
+ });
53
+
54
+ test('rowsToMap keys ONLY on github_login — a display name must never resolve a rank', () => {
55
+ // #863-followup finding #6. display_name is builder-controlled, so keying it let one
56
+ // builder alias another login's rank. This is also WHY a deploy lane's own commits stay
57
+ // unresolved on a standalone instance: they are authored under a human display name,
58
+ // not a github_login.
59
+ const map = audit.rowsToMap([
60
+ { github_login: 'somebuilder', display_name: 'Someone Else', rank: 'xenos' },
61
+ { display_name: 'Firstname Lastname', rank: 'archon' },
62
+ ]);
63
+ assert.equal(map.somebuilder, 'xenos');
64
+ assert.equal(map['firstname lastname'], undefined, 'a display name must not carry a rank');
65
+ assert.equal(Object.keys(map).length, 1);
66
+ });
67
+
68
+ test('an unreadable --db roster does NOT resolve to an empty map in silence', async () => {
69
+ // The vacuous green, asserted directly. fetchRankMap({useDb:true}) is forced to fail by
70
+ // pointing it at a database that cannot exist, and must then FALL THROUGH to the
71
+ // API/public roster rather than returning {}. Offline (no network in the unit lane) the
72
+ // fallbacks also fail and the map is legitimately empty — so what is pinned here is that
73
+ // the failure was ANNOUNCED on stderr, which is what a gate operator needs in order to
74
+ // know the audit did not actually run.
75
+ const errs = [];
76
+ const realError = console.error;
77
+ console.error = (...a) => errs.push(a.join(' '));
78
+ const realPg = process.env.PGDATABASE;
79
+ process.env.PGDATABASE = 'cloudbongos_no_such_db_for_tests';
80
+ try {
81
+ const map = await audit.fetchRankMap({ useDb: true });
82
+ assert.equal(typeof map, 'object', 'still returns a map rather than throwing at the gate');
83
+ } finally {
84
+ console.error = realError;
85
+ if (realPg === undefined) delete process.env.PGDATABASE; else process.env.PGDATABASE = realPg;
86
+ }
87
+ const said = errs.join('\n');
88
+ assert.match(
89
+ said, /--db roster read (failed|returned NO builders)/,
90
+ 'a --db roster that cannot be read must say so on stderr; silence here is what made '
91
+ + 'the dead require survive — the gate exits 0 and looks like a pass',
92
+ );
93
+ assert.match(
94
+ said, /falling through/,
95
+ 'and it must say it is falling through, so nobody reads the empty map as "roster is empty"',
96
+ );
97
+ });
@@ -10,9 +10,11 @@ import assert from 'node:assert/strict';
10
10
  import path from 'node:path';
11
11
  import { execFileSync } from 'node:child_process';
12
12
  import { createRequire } from 'node:module';
13
+ import { shallowCloneReason } from './helpers.mjs';
13
14
 
14
15
  const require = createRequire(import.meta.url);
15
16
  const ROOT = path.resolve(import.meta.dirname, '..');
17
+
16
18
  const pw = require(path.join(ROOT, 'scripts', 'gds', 'publish-watch.js'));
17
19
 
18
20
  const REL = (v, iso, sha = 'abcdef1234567890') =>
@@ -80,6 +82,13 @@ test('a read that returned nothing must not read as "nothing missing"', () => {
80
82
  });
81
83
 
82
84
  test('LIVE: replays the real 2026-09-13 publish outage from git + the registry', (t) => {
85
+ // Needs real history: a shallow clone's origin/main does not reach back to the
86
+ // incident window, so the log below comes back empty and the count reads 0 (task
87
+ // 1004049). Skipped with the reason stated, for the same reason the unreachable
88
+ // registry below is skipped rather than failed — and never as a silent pass.
89
+ const shallow = shallowCloneReason(ROOT);
90
+ if (shallow) { t.skip(shallow); return; }
91
+
83
92
  // The eight carrier commits exist on main...
84
93
  // Bounded: this runs on every ordinary suite run, so it reads a window around
85
94
  // the incident rather than the whole history.
@@ -25,6 +25,7 @@
25
25
  // Run: node tests/setup_device_flow_fallback.mjs
26
26
 
27
27
  import { strict as assert } from 'node:assert';
28
+ import { childEnv } from './helpers.mjs';
28
29
  import { createRequire } from 'node:module';
29
30
  import { spawn } from 'node:child_process';
30
31
  import http from 'node:http';
@@ -144,7 +145,7 @@ fs.writeFileSync(driverPath, [
144
145
  function redeemInChild(token, home) {
145
146
  return new Promise((resolve) => {
146
147
  const child = spawn(process.execPath, [driverPath, token], {
147
- env: { ...process.env, HOME: home, USERPROFILE: home, GDS_API_BASE: BASE, OTB_API_BASE: BASE },
148
+ env: childEnv({ HOME: home, USERPROFILE: home, GDS_API_BASE: BASE, OTB_API_BASE: BASE }),
148
149
  });
149
150
  let stdout = '', stderr = '';
150
151
  child.stdout.on('data', (c) => { stdout += c; });