@bongos/core 1.21.123 → 1.21.124

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.21.123",
6
- "core_contract": "1.21.123",
7
- "source_commit": "bfe95a4e390577d4579a3da16d81d5ada6bc8b9a",
5
+ "core_version": "1.21.124",
6
+ "core_contract": "1.21.124",
7
+ "source_commit": "695a6d5cad41a0d7a9e79cf58a7de3282b64daf3",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-14T19:30:01.919Z",
9
+ "built_at": "2026-10-14T19:49:05.130Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 605,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 3122,
14
+ "functional_verbatim": 3124,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3756,
20
- "tree_sha256": "c5becc4dfe0e822803424a46f1041b66adce70cbe65f85be941603de3db80cee",
19
+ "file_count": 3758,
20
+ "tree_sha256": "97c9a1a6dc5c913bdacb02a6bbef3bef853e5babbbd971cd4f097403aa754be9",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2922,7 +2922,7 @@
2922
2922
  {
2923
2923
  "path": "docs/module-api-changelog.md",
2924
2924
  "mode": "0000644",
2925
- "sha256": "f2b2bf656d2fa819a7bd0f10ceb966b5414c1c06f05f2e8ccaa9ed326a7227ed"
2925
+ "sha256": "1ead6d6c8697d53d3f324f3d5c1523f5c590ebcce268abcb2b29a367d6c34f17"
2926
2926
  },
2927
2927
  {
2928
2928
  "path": "docs/modules-contract.md",
@@ -7242,7 +7242,7 @@
7242
7242
  {
7243
7243
  "path": "modules/lifecycle/github-push.js",
7244
7244
  "mode": "0000644",
7245
- "sha256": "65386fff8736c1b55c1f105c595758161e718352077603d1ba9aa7dd05d6d1d6"
7245
+ "sha256": "b15eb0c36f0acb41d11f52393ca9696de55b9326a483de02624b3a4d5e87250c"
7246
7246
  },
7247
7247
  {
7248
7248
  "path": "modules/lifecycle/goal-advisory.js",
@@ -7349,6 +7349,11 @@
7349
7349
  "mode": "0000644",
7350
7350
  "sha256": "69e9bf495dfdbc324e7f5664e09a5ad30e6eac896c12656b7d6ecb38d6be81f9"
7351
7351
  },
7352
+ {
7353
+ "path": "modules/lifecycle/local-land-proof.js",
7354
+ "mode": "0000644",
7355
+ "sha256": "7698b11de1978c29c1744032da5b28831a1f6823b905d60c8be8ac469916d46a"
7356
+ },
7352
7357
  {
7353
7358
  "path": "modules/lifecycle/matter-acts.js",
7354
7359
  "mode": "0000644",
@@ -7507,7 +7512,7 @@
7507
7512
  {
7508
7513
  "path": "modules/lifecycle/publish-reconciler.js",
7509
7514
  "mode": "0000644",
7510
- "sha256": "5d8bf02c3646f12debc411aa6291110c5bc4f1fe8917af9e17a4ecc5f6451e76"
7515
+ "sha256": "d03c36a2b3e50bb2334db66d177514542185a7277873891ee6d3a99c5ad10464"
7511
7516
  },
7512
7517
  {
7513
7518
  "path": "modules/lifecycle/removal-derivation.js",
@@ -10467,12 +10472,12 @@
10467
10472
  {
10468
10473
  "path": "package-lock.json",
10469
10474
  "mode": "0000644",
10470
- "sha256": "e92fbdef629a9283a773d94986531ae9d30412fe05dc3012dd918be8fb859030"
10475
+ "sha256": "31a3c967bca985d1dc5007279f9ed7e9352b0466128af6edd9debbf4e7b592bb"
10471
10476
  },
10472
10477
  {
10473
10478
  "path": "package.json",
10474
10479
  "mode": "0000644",
10475
- "sha256": "cf96b6c94e06d4306f0b3e3c0626c051729a0498bd3e6760012ce86350286d16"
10480
+ "sha256": "4714b7161307f36c4ae4322e72f3670f6d958b1ab3b19f728a3f05f880898771"
10476
10481
  },
10477
10482
  {
10478
10483
  "path": "public-docs/index.html",
@@ -10492,7 +10497,7 @@
10492
10497
  {
10493
10498
  "path": "release-notes.json",
10494
10499
  "mode": "0000644",
10495
- "sha256": "86c4edcb22bf8005ac7f4e479a9b4037d2bdad36618ed1e8f739a3a786952344"
10500
+ "sha256": "aeb72646e01f775fe09e570e0f5e8dccf9733fc59cb79d5a67e6315f2195d88c"
10496
10501
  },
10497
10502
  {
10498
10503
  "path": "scripts/cron/style_guide_audit.sh",
@@ -12812,7 +12817,7 @@
12812
12817
  {
12813
12818
  "path": "src/module-api.js",
12814
12819
  "mode": "0000644",
12815
- "sha256": "01bc6b541cf12105be05701170377582f537316fe3f70fdea2681bae07864bf7"
12820
+ "sha256": "792dad669f6c0d0b86433d206c837ca7ffcbf6a8b5b7c02bda8707c3dd3f7099"
12816
12821
  },
12817
12822
  {
12818
12823
  "path": "src/module-loader/catalog.js",
@@ -16109,6 +16114,11 @@
16109
16114
  "mode": "0000644",
16110
16115
  "sha256": "64f8bd94dc461ca9b4ef82fae929d49675231afa6a24d6d65ba2dbe8721efc89"
16111
16116
  },
16117
+ {
16118
+ "path": "tests/local_land_proof.mjs",
16119
+ "mode": "0000644",
16120
+ "sha256": "0a7825f1e35c7251c66ab986555b71728575d3dff8be95f8a017dd66190d186f"
16121
+ },
16112
16122
  {
16113
16123
  "path": "tests/local_preview.mjs",
16114
16124
  "mode": "0000644",
@@ -17342,7 +17352,7 @@
17342
17352
  {
17343
17353
  "path": "tests/publish_reconciler.mjs",
17344
17354
  "mode": "0000644",
17345
- "sha256": "fac182001ce8a53bafea2d59661d2879c2ccbac55600936d151d1fe86ca59b71"
17355
+ "sha256": "882dfd49bebd89bed3c2bb20d0bf78e5f7b4da45e525492e8742b43f9c4e19a9"
17346
17356
  },
17347
17357
  {
17348
17358
  "path": "tests/publish_status_branch.mjs",
@@ -3083,5 +3083,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3083
3083
  landed since 1.21.121 with no explicit bump. run 38620147259. (task 1002620)
3084
3084
  1.21.123 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3085
3085
  landed since 1.21.122 with no explicit bump. run 38624070870. (task 1002620)
3086
+ 1.21.124 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3087
+ landed since 1.21.123 with no explicit bump. run 38626749089. (task 1002620)
3086
3088
  ---------------------------------------------------------------------------
3087
3089
  ```
@@ -1194,7 +1194,8 @@ async function landProvenForTask({ taskId, publishedSha, status }, deps = {}) {
1194
1194
  async function isAncestorOfDefaultBranch({ sha, base = 'main' } = {}, deps = {}) {
1195
1195
  if (typeof sha !== 'string' || !SAFE_SHA_RE.test(sha)) return false;
1196
1196
  const token = await resolveToken(deps);
1197
- if (!token) throw err('PUSH_UNCONFIGURED');
1197
+ // task 1004633: no credential (a laptop-mode instance) → ask the instance's own checkout.
1198
+ if (!token) return (deps.localLandProof || require('./local-land-proof')).isOnOriginMain({ sha, base }, deps.local || {});
1198
1199
  const repoInfo = deps.repoInfo || loadRepoInfo();
1199
1200
  if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
1200
1201
  const { owner, repo } = repoInfo;
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ // local-land-proof — prove a land from the instance's OWN git checkout when the
4
+ // server holds no GitHub credential (task 1004633).
5
+ //
6
+ // WHY. Every land proof the lifecycle had asked GitHub: isAncestorOfDefaultBranch
7
+ // runs the compare API, and the publish reconciler returned early when
8
+ // isConfigured() was false. A laptop-mode instance (the owner's mercury project)
9
+ // merges a ship LOCALLY and pushes main itself — no PR, no server credential. So
10
+ // POST /tasks/:id/ship refused `ship_land_not_proven` (the compare threw
11
+ // PUSH_UNCONFIGURED, swallowed as "no proof"), the sweep never ran, and task 48
12
+ // sat at 'confirmed' with its merge commit on origin/main.
13
+ //
14
+ // THE PROOF is the same one the GitHub path makes, asked of a different witness:
15
+ // is the claim's recorded tip (claims.published_head_sha, written by attest-gate
16
+ // before its own credential check) an ancestor of origin/<base>? The witness is
17
+ // the remote-tracking ref after a fetch, never the local branch: a laptop ship
18
+ // merges into local main BEFORE it pushes, and a merge whose push failed must not
19
+ // count. A failed fetch (offline) falls back to the last fetched ref, which can
20
+ // only be behind the truth, so it can delay a ship but never invent one.
21
+ //
22
+ // Pure w.r.t. an injected `git` runner, so it unit-tests with no repository.
23
+
24
+ const { execFile } = require('node:child_process');
25
+ const { promisify } = require('node:util');
26
+
27
+ const execFileP = promisify(execFile);
28
+
29
+ // A real commit sha only: `merge-base` resolves refs, so "HEAD" or a branch name
30
+ // would answer about something other than the claim's tip.
31
+ const SAFE_SHA_RE = /^[0-9a-f]{7,64}$/;
32
+ const SAFE_BASE_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,100}$/;
33
+
34
+ function defaultGit(root) {
35
+ return (args) => execFileP('git', args, { cwd: root, timeout: 60_000, windowsHide: true });
36
+ }
37
+
38
+ function resolveRoot(deps) {
39
+ if (deps.root) return deps.root;
40
+ return require('../../src/module-api').resolveInstanceRoot();
41
+ }
42
+
43
+ async function fetchOrigin(git, base) {
44
+ try {
45
+ await git(['fetch', '--quiet', 'origin', base]);
46
+ } catch (_) {
47
+ /* offline or no credential: the last fetched origin ref still answers, only later */
48
+ }
49
+ }
50
+
51
+ // true = the tip is in origin/<base>; false = it is not (or git does not know the
52
+ // commit). Throws LOCAL_PROOF_UNAVAILABLE when there is no usable checkout (git
53
+ // missing, not a repository) so a caller can tell "not landed" from "cannot say".
54
+ async function isOnOriginMain({ sha, base = 'main' } = {}, deps = {}) {
55
+ if (typeof sha !== 'string' || !SAFE_SHA_RE.test(sha)) return false;
56
+ if (typeof base !== 'string' || !SAFE_BASE_RE.test(base) || base.includes('..')) return false;
57
+ const git = deps.git || defaultGit(resolveRoot(deps));
58
+ if (!deps.fetched) await fetchOrigin(git, base);
59
+ try {
60
+ await git(['merge-base', '--is-ancestor', sha, `refs/remotes/origin/${base}`]);
61
+ return true;
62
+ } catch (e) {
63
+ // 1 = not an ancestor; 128 = an unknown commit (never reached origin).
64
+ if (e && (e.code === 1 || e.code === 128)) return false;
65
+ const out = new Error('LOCAL_PROOF_UNAVAILABLE');
66
+ out.code = 'LOCAL_PROOF_UNAVAILABLE';
67
+ out.cause = e;
68
+ throw out;
69
+ }
70
+ }
71
+
72
+ // The reconciler's sweep for a host with no GitHub credential. Tip proof only: no
73
+ // PR, branch or deploy probe exists to consult, and a row with no recorded tip
74
+ // has nothing local to prove, so it waits (strand-watch reports it).
75
+ // One root and one fetch per sweep, not per row: origin/main is the same ref for
76
+ // every confirmed task the sweep reads.
77
+ async function sweepByLocalProof(rows, { db, emit, log = console.log, error = console.error, proof } = {}) {
78
+ if (!proof) {
79
+ let shared = null;
80
+ proof = async ({ sha }) => {
81
+ if (!shared) {
82
+ shared = { git: defaultGit(resolveRoot({})), fetched: true };
83
+ await fetchOrigin(shared.git, 'main');
84
+ }
85
+ return isOnOriginMain({ sha }, shared);
86
+ };
87
+ }
88
+ let checked = 0;
89
+ let reconciled = 0;
90
+ let errors = 0;
91
+ for (const r of rows || []) {
92
+ const tip = r && r.published_head_sha;
93
+ if (!tip || !SAFE_SHA_RE.test(String(tip))) continue;
94
+ checked++;
95
+ let landed = false;
96
+ try {
97
+ landed = await proof({ sha: String(tip) });
98
+ } catch (e) {
99
+ errors++;
100
+ error(`[publish-reconciler] task ${r.task_id}: local land proof unavailable: ${e && e.message}`);
101
+ continue;
102
+ }
103
+ if (!landed) continue;
104
+ const reason = `published tip ${String(tip).slice(0, 8)} in origin/main (local checkout; no GitHub credential)`;
105
+ try {
106
+ const shipped = await db.shipTask({
107
+ taskId: r.task_id,
108
+ builderId: r.builder_id,
109
+ landAssurance: { kind: 'proof', reason },
110
+ });
111
+ reconciled++;
112
+ log(`[publish-reconciler] task ${r.task_id} (${reason}) landed → shipped`);
113
+ try { await emit('task.shipped', shipped); } catch (e) {
114
+ error(`[publish-reconciler] ship broadcast for task ${r.task_id} (non-blocking): ${e && e.message}`);
115
+ }
116
+ } catch (e) {
117
+ if (!e || e.code !== 'TASK_NOT_CONFIRMED') {
118
+ errors++;
119
+ error(`[publish-reconciler] shipTask ${r.task_id} failed: ${e && e.message}`);
120
+ }
121
+ }
122
+ }
123
+ return { checked, reconciled, errors };
124
+ }
125
+
126
+ module.exports = { isOnOriginMain, sweepByLocalProof, SAFE_SHA_RE };
@@ -76,6 +76,7 @@
76
76
  const githubPush = require('./github-push');
77
77
  const seams = require('../../src/module-api');
78
78
  const conflictResolve = require('./conflict-resolve');
79
+ const localLandProof = require('./local-land-proof'); // task 1004633
79
80
  // The claim gate's grace for a strand its builder may be unable to clear — imported
80
81
  // rather than restated so the sweep's escalation and the gate agree on one number
81
82
  // (task 1003294).
@@ -195,10 +196,22 @@ async function runSweep(deps = {}) {
195
196
  if (!warn.unconfigured) {
196
197
  warn.unconfigured = true;
197
198
  errFn(
198
- `[publish-reconciler] WARN ${describeTarget({ configured: false, repo: resolveTarget(deps) })} — confirmed→shipped can NEVER land until a credential is set (GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY — installation id is auto-discovered — or GITHUB_PUSH_TOKEN). Subsequent sweeps stay quiet until this changes.`
199
+ `[publish-reconciler] WARN ${describeTarget({ configured: false, repo: resolveTarget(deps) })} — no GitHub credential (GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY — installation id is auto-discovered — or GITHUB_PUSH_TOKEN): confirmed→shipped lands only on a recorded tip this instance's own checkout finds in origin/main. Subsequent sweeps stay quiet until this changes.`
199
200
  );
200
201
  }
201
- return { skipped: 'unconfigured', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 0, strandsFlagged: 0 };
202
+ // task 1004633: a laptop-mode instance merges and pushes main itself, with no
203
+ // server credential. Returning here stranded its merged tasks at 'confirmed'
204
+ // forever (mercury task 48), so prove the recorded tip from the local checkout.
205
+ const local = deps.localLandProof || localLandProof;
206
+ let rows = [];
207
+ try {
208
+ rows = await db.listConfirmedTasksForReconcile({ limit: deps.limit || SWEEP_LIMIT });
209
+ } catch (e) {
210
+ errFn(`[publish-reconciler] could not list confirmed tasks: ${e && e.message}`);
211
+ return { skipped: 'unconfigured', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 1, strandsFlagged: 0 };
212
+ }
213
+ const swept = await local.sweepByLocalProof(rows, { db, emit, log: logFn, error: errFn });
214
+ return { skipped: 'unconfigured', checked: swept.checked, reconciled: swept.reconciled, merged: 0, resolvedConflicts: 0, errors: swept.errors, strandsFlagged: 0 };
202
215
  }
203
216
  warn.unconfigured = false; // re-arm: a later credential LOSS must warn again
204
217
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.123",
3
+ "version": "1.21.124",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.123",
9
+ "version": "1.21.124",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.123",
3
+ "version": "1.21.124",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9720,5 +9720,11 @@
9720
9720
  "id": "1004631",
9721
9721
  "text": "Found why the AI refused to ship on Windows projects: the core update failed on Windows, so projects never got the permission that lets the AI run a ship. That is fixed. New projects also tell the AI to ship on its own in Auto"
9722
9722
  }
9723
+ ],
9724
+ "1.21.124": [
9725
+ {
9726
+ "id": "1004633",
9727
+ "text": "Found why a merged task on your own project stayed at 'confirmed': with no GitHub key set up on the project's server, Bongos could not prove the merge happened, and its background checker switched itself off. It now confirms t"
9728
+ }
9723
9729
  ]
9724
9730
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.123'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.124'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,173 @@
1
+ // tests/local_land_proof.mjs
2
+ //
3
+ // task 1004633 — a merged task on an instance with no GitHub credential becomes
4
+ // 'shipped'. The owner's mercury project merged task 48 locally and pushed main;
5
+ // with no server credential the ship route refused (the compare API threw
6
+ // PUSH_UNCONFIGURED) and the reconciler returned early, so the task sat at
7
+ // 'confirmed' with its merge on origin/main. These pin the three halves of the fix:
8
+ // 1. isOnOriginMain proves a tip against a REAL repository's origin ref, and a
9
+ // merge that never reached origin does not count;
10
+ // 2. landProvenForTask falls back to it when there is no token;
11
+ // 3. the unconfigured reconciler sweep ships a proven row, and only that row.
12
+ //
13
+ // Run: node tests/local_land_proof.mjs
14
+
15
+ import { strict as assert } from 'node:assert';
16
+ import { createRequire } from 'node:module';
17
+ import { execFileSync } from 'node:child_process';
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const local = require('../modules/lifecycle/local-land-proof.js');
24
+ const gh = require('../modules/lifecycle/github-push.js');
25
+ const reconciler = require('../modules/lifecycle/publish-reconciler.js');
26
+
27
+ let passed = 0;
28
+ let failed = 0;
29
+ async function test(name, fn) {
30
+ try {
31
+ await fn();
32
+ passed++;
33
+ console.log(` ok ${name}`);
34
+ } catch (err) {
35
+ failed++;
36
+ console.log(` FAIL ${name}\n ${err && err.stack}`);
37
+ }
38
+ }
39
+
40
+ const git = (cwd, ...args) => execFileSync('git', args, { cwd, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
41
+
42
+ // A bare origin plus a clone, the shape of a laptop-mode instance checkout.
43
+ function mkRepo() {
44
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'local-land-proof-'));
45
+ const origin = path.join(dir, 'origin.git');
46
+ const work = path.join(dir, 'work');
47
+ git(dir, 'init', '--quiet', '--bare', '-b', 'main', origin);
48
+ git(dir, 'clone', '--quiet', origin, work);
49
+ git(work, 'config', 'user.email', 't@example.com');
50
+ git(work, 'config', 'user.name', 't');
51
+ git(work, 'checkout', '--quiet', '-b', 'main');
52
+ fs.writeFileSync(path.join(work, 'a.txt'), 'a\n');
53
+ git(work, 'add', 'a.txt');
54
+ git(work, 'commit', '--quiet', '-m', 'base');
55
+ git(work, 'push', '--quiet', 'origin', 'main');
56
+ return { dir, work };
57
+ }
58
+
59
+ function commit(work, file) {
60
+ fs.writeFileSync(path.join(work, file), `${file}\n`);
61
+ git(work, 'add', file);
62
+ git(work, 'commit', '--quiet', '-m', file);
63
+ return git(work, 'rev-parse', 'HEAD');
64
+ }
65
+
66
+ await test('a tip merged and pushed to origin/main is proven', async () => {
67
+ const { dir, work } = mkRepo();
68
+ try {
69
+ git(work, 'checkout', '--quiet', '-b', 'task-48');
70
+ const tip = commit(work, 'b.txt');
71
+ git(work, 'checkout', '--quiet', 'main');
72
+ git(work, 'merge', '--quiet', '--no-ff', '-m', 'Merge task-48: (no value summary)', 'task-48');
73
+ git(work, 'push', '--quiet', 'origin', 'main');
74
+ assert.equal(await local.isOnOriginMain({ sha: tip }, { root: work }), true);
75
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
76
+ });
77
+
78
+ await test('a merge into local main that never reached origin is NOT proven', async () => {
79
+ const { dir, work } = mkRepo();
80
+ try {
81
+ const tip = commit(work, 'c.txt'); // on local main, unpushed
82
+ assert.equal(await local.isOnOriginMain({ sha: tip }, { root: work }), false);
83
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
84
+ });
85
+
86
+ await test('an unknown commit or a ref-shaped value is not proven', async () => {
87
+ const { dir, work } = mkRepo();
88
+ try {
89
+ assert.equal(await local.isOnOriginMain({ sha: 'c'.repeat(40) }, { root: work }), false);
90
+ assert.equal(await local.isOnOriginMain({ sha: 'HEAD' }, { root: work }), false, 'a ref must never resolve');
91
+ assert.equal(await local.isOnOriginMain({ sha: 'a'.repeat(40), base: '../x' }, { root: work }), false);
92
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
93
+ });
94
+
95
+ await test('no usable checkout throws LOCAL_PROOF_UNAVAILABLE, never a silent false', async () => {
96
+ const broken = async () => { const e = new Error('spawn git ENOENT'); e.code = 'ENOENT'; throw e; };
97
+ await assert.rejects(
98
+ local.isOnOriginMain({ sha: 'a'.repeat(40) }, { git: broken }),
99
+ (e) => e.code === 'LOCAL_PROOF_UNAVAILABLE'
100
+ );
101
+ });
102
+
103
+ await test('a caller that already fetched (the sweep) is not made to fetch again per row', async () => {
104
+ const calls = [];
105
+ const fake = async (args) => { calls.push(args[0]); };
106
+ await local.isOnOriginMain({ sha: 'a'.repeat(40) }, { git: fake });
107
+ await local.isOnOriginMain({ sha: 'b'.repeat(40) }, { git: fake, fetched: true });
108
+ await local.isOnOriginMain({ sha: 'c'.repeat(40) }, { git: fake, fetched: true });
109
+ assert.deepEqual(calls, ['fetch', 'merge-base', 'merge-base', 'merge-base']);
110
+ });
111
+
112
+ await test('landProvenForTask with no credential proves the tip from the local checkout', async () => {
113
+ const saved = { t: process.env.GITHUB_PUSH_TOKEN, a: process.env.GITHUB_APP_ID, k: process.env.GITHUB_APP_PRIVATE_KEY };
114
+ delete process.env.GITHUB_PUSH_TOKEN; delete process.env.GITHUB_APP_ID; delete process.env.GITHUB_APP_PRIVATE_KEY;
115
+ try {
116
+ const asked = [];
117
+ const localLandProof = { isOnOriginMain: async ({ sha }) => { asked.push(sha); return sha === 'a'.repeat(40); } };
118
+ const yes = await gh.landProvenForTask({ taskId: 48, publishedSha: 'a'.repeat(40), status: null }, { localLandProof });
119
+ assert.equal(yes.proven, true);
120
+ const no = await gh.landProvenForTask({ taskId: 49, publishedSha: 'b'.repeat(40), status: null }, { localLandProof });
121
+ assert.equal(no.proven, false);
122
+ assert.deepEqual(asked, ['a'.repeat(40), 'b'.repeat(40)]);
123
+ } finally {
124
+ for (const [k, v] of [['GITHUB_PUSH_TOKEN', saved.t], ['GITHUB_APP_ID', saved.a], ['GITHUB_APP_PRIVATE_KEY', saved.k]]) {
125
+ if (v === undefined) delete process.env[k]; else process.env[k] = v;
126
+ }
127
+ }
128
+ });
129
+
130
+ await test('the unconfigured sweep ships a proven tip — and only that row', async () => {
131
+ const PROVEN = 'a'.repeat(40);
132
+ const shipped = [];
133
+ const events = [];
134
+ const rows = [
135
+ { task_id: 48, builder_id: 7, worktree_name: 'task-48', published_head_sha: PROVEN },
136
+ { task_id: 49, builder_id: 7, worktree_name: 'task-49', published_head_sha: 'b'.repeat(40) },
137
+ { task_id: 50, builder_id: 7, worktree_name: 'task-50', published_head_sha: null },
138
+ ];
139
+ const r = await reconciler.runSweep({
140
+ db: {
141
+ listConfirmedTasksForReconcile: async () => rows,
142
+ shipTask: async (a) => { shipped.push(a); return { id: a.taskId }; },
143
+ },
144
+ githubPush: { isConfigured: () => false },
145
+ localLandProof: {
146
+ sweepByLocalProof: (rs, o) => local.sweepByLocalProof(rs, { ...o, proof: async ({ sha }) => sha === PROVEN }),
147
+ },
148
+ emit: async (name, t) => { events.push([name, t.id]); },
149
+ log: () => {},
150
+ error: () => {},
151
+ });
152
+ assert.equal(r.skipped, 'unconfigured');
153
+ assert.equal(r.reconciled, 1);
154
+ assert.equal(r.checked, 2, 'the row with no tip has nothing local to prove');
155
+ assert.deepEqual(shipped.map((s) => s.taskId), [48]);
156
+ assert.equal(shipped[0].landAssurance.kind, 'proof');
157
+ assert.deepEqual(events, [['task.shipped', 48]]);
158
+ });
159
+
160
+ await test('a sweep racing another flip is not an error (TASK_NOT_CONFIRMED)', async () => {
161
+ const r = await local.sweepByLocalProof(
162
+ [{ task_id: 48, builder_id: 7, published_head_sha: 'a'.repeat(40) }],
163
+ {
164
+ db: { shipTask: async () => { const e = new Error('x'); e.code = 'TASK_NOT_CONFIRMED'; throw e; } },
165
+ emit: async () => {}, log: () => {}, error: () => {},
166
+ proof: async () => true,
167
+ }
168
+ );
169
+ assert.deepEqual(r, { checked: 1, reconciled: 0, errors: 0 });
170
+ });
171
+
172
+ console.log(`\n${passed} passed, ${failed} failed`);
173
+ if (failed) process.exit(1);
@@ -205,7 +205,9 @@ function mkDeps({
205
205
 
206
206
  // ---------------------------------------------------------------------------
207
207
 
208
- test('no push credential → no-op (skipped: unconfigured), never touches the DB', async () => {
208
+ // task 1004633: an unconfigured sweep now reads the confirmed rows (to prove a recorded tip
209
+ // from the local checkout), but a row with no tip still ships nothing and GitHub is never asked.
210
+ test('no push credential → skipped: unconfigured; a row with no tip ships nothing, GitHub never called', async () => {
209
211
  const deps = mkDeps({ configured: false, rows: [{ task_id: 1, builder_id: 9, worktree_name: 'wt' }] });
210
212
  const r = await reconciler.runSweep(deps);
211
213
  assert.equal(r.skipped, 'unconfigured');