@bongos/core 1.19.1029 → 1.19.1031

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.1029",
6
- "core_contract": "1.19.1029",
7
- "source_commit": "9061b1b634a11e0d3737336b0d59fe110d2a7a17",
5
+ "core_version": "1.19.1031",
6
+ "core_contract": "1.19.1031",
7
+ "source_commit": "04f5126869533ad9e3a29fa072dd288ba6e63f3b",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-25T21:07:08.900Z",
9
+ "built_at": "2026-09-25T21:32:54.328Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 529,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 3014,
20
- "tree_sha256": "1305b1deb2512746e9b12451812ed65ff0e0364e416b1cbdc3ffacb9d12b8851",
20
+ "tree_sha256": "05efd2e30efe440d0892c97889cdea81127c2544ef93366c2b054750750fceb3",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2757,7 +2757,7 @@
2757
2757
  {
2758
2758
  "path": "docs/module-api-changelog.md",
2759
2759
  "mode": "0000644",
2760
- "sha256": "bc6a32426c475f26a5257ae634f5cf8c9c0863c53edd9b8981c411323b2f21e4"
2760
+ "sha256": "6f37eb2ce46310219bfc4f0c4820e67f6afda57cfe76cb2a0e8cf75d3bd6577d"
2761
2761
  },
2762
2762
  {
2763
2763
  "path": "docs/modules-contract.md",
@@ -6452,7 +6452,7 @@
6452
6452
  {
6453
6453
  "path": "modules/platform-identity/activity-snapshots.js",
6454
6454
  "mode": "0000644",
6455
- "sha256": "6cf9dce12fa2689b29a149ccb56204fb494ea7b8c4cc59a3421f471162dbb529"
6455
+ "sha256": "c483a9ef792ad0ac3600dc1eb227323c6f851e7cd33189cc418a4e0ef3443ed6"
6456
6456
  },
6457
6457
  {
6458
6458
  "path": "modules/platform-identity/applicant-profile.js",
@@ -6652,7 +6652,7 @@
6652
6652
  {
6653
6653
  "path": "modules/platform-identity/platform-identity.js",
6654
6654
  "mode": "0000644",
6655
- "sha256": "16cae6287be02d85baa6ea359d80ba963df59a1d5f787f4234079b66b97812a6"
6655
+ "sha256": "0f60239ec8ae79e1cb87870d7454f6b03de84170d557a06fae93cc556757004a"
6656
6656
  },
6657
6657
  {
6658
6658
  "path": "modules/platform-identity/pollers/code-sweep.js",
@@ -6767,12 +6767,12 @@
6767
6767
  {
6768
6768
  "path": "modules/platform-identity/tests/activity-snapshots.mjs",
6769
6769
  "mode": "0000644",
6770
- "sha256": "19682ecba44275080f00d203b4f708ea4f244f6107d3dce73960f97529c3874b"
6770
+ "sha256": "47e4970496e303b66007c3c8cee9ecf1b1a10fe8805d28c8ef44d545ed7f1b4f"
6771
6771
  },
6772
6772
  {
6773
6773
  "path": "modules/platform-identity/tests/platform-identity.mjs",
6774
6774
  "mode": "0000644",
6775
- "sha256": "7db35ebebd3101fb2cb9d6a7d7e3d99c2aae176456a93d5b109f5f3ba72420e6"
6775
+ "sha256": "6bbddd8d1b85f159ba1bade8df61552979e85e50dace2e1c9128de4b9ee8beb9"
6776
6776
  },
6777
6777
  {
6778
6778
  "path": "modules/platform-identity/ui/.gitkeep",
@@ -8362,12 +8362,12 @@
8362
8362
  {
8363
8363
  "path": "package-lock.json",
8364
8364
  "mode": "0000644",
8365
- "sha256": "8608f78a458012a57ffc19e813402909f783c77bcc50f3d45780e4f8b2e3f759"
8365
+ "sha256": "eb24a6596bec145cc1e850a0c17f5f9a5a05bf4b5762785c063ac9711788417e"
8366
8366
  },
8367
8367
  {
8368
8368
  "path": "package.json",
8369
8369
  "mode": "0000644",
8370
- "sha256": "32e6fc0b4953d6f15b41649ea9df77918f37ac8a8ed7168ce17d9040d04bf5c6"
8370
+ "sha256": "84bad55379e5b9c61129a8d4cae7b074f22ba99984543dd1ac990688ac73efcb"
8371
8371
  },
8372
8372
  {
8373
8373
  "path": "public-docs/index.html",
@@ -8542,7 +8542,7 @@
8542
8542
  {
8543
8543
  "path": "scripts/gds/autobongos-run.js",
8544
8544
  "mode": "0000644",
8545
- "sha256": "24f0be946550a89f717278a418ebb68330fad3c7cdd45d3b79e85315564c7425"
8545
+ "sha256": "9fe8be8bc38039f700b6aab8b52508c162252513cc6b5b87aacb7dc48648937a"
8546
8546
  },
8547
8547
  {
8548
8548
  "path": "scripts/gds/autobongos-service.cmd",
@@ -8552,7 +8552,7 @@
8552
8552
  {
8553
8553
  "path": "scripts/gds/autobongos-service.ps1",
8554
8554
  "mode": "0000644",
8555
- "sha256": "3f3e44969809b3c22c9cf338af68695af6e835b861ac056d38408a4be1d202a7"
8555
+ "sha256": "ed95f836655535827c14a32caf197fee7c238e15d8e8bf7a9e987b78ab266887"
8556
8556
  },
8557
8557
  {
8558
8558
  "path": "scripts/gds/autobongos-verify.js",
@@ -10412,7 +10412,7 @@
10412
10412
  {
10413
10413
  "path": "src/module-api.js",
10414
10414
  "mode": "0000644",
10415
- "sha256": "9763f23d6375a13a9ce55193be5689f74c04cd9170079735bca1956524f007c7"
10415
+ "sha256": "320ce97feaea53ab38c025dbba20001ef89643b0519151e010c7456e0dd727eb"
10416
10416
  },
10417
10417
  {
10418
10418
  "path": "src/module-loader/catalog.js",
@@ -10517,7 +10517,7 @@
10517
10517
  {
10518
10518
  "path": "tests/activity_snapshots_db.mjs",
10519
10519
  "mode": "0000644",
10520
- "sha256": "5dcd2f23e6549938decf39306807dac6405f95e3245bd9fbb59a4e482a2c694d"
10520
+ "sha256": "219569f3160eb22a89e92a217b854a194afba6ebc7032dc07eff2534efc4390a"
10521
10521
  },
10522
10522
  {
10523
10523
  "path": "tests/admission_ceiling_gate.mjs",
@@ -10847,7 +10847,7 @@
10847
10847
  {
10848
10848
  "path": "tests/autobongos_loop.mjs",
10849
10849
  "mode": "0000644",
10850
- "sha256": "2f9c2b4e115b2c7411748cfe5179344bcc0e45c855c68102821966ab63ccbc21"
10850
+ "sha256": "8ba5a6b492ae5afabb13f9e44d2f82cb873a094a659425554f0d105bd979aff4"
10851
10851
  },
10852
10852
  {
10853
10853
  "path": "tests/autobongos_verify.mjs",
@@ -12562,7 +12562,7 @@
12562
12562
  {
12563
12563
  "path": "tests/hub_recent_ships.mjs",
12564
12564
  "mode": "0000644",
12565
- "sha256": "0d8f07c64f4d17c8c894b2d6395281882efd4e186c5714fe9aab61e2808d4314"
12565
+ "sha256": "ad183edeb3b24bdd70de80aad370c5411d32257907baa36c65b70a8143d8ec53"
12566
12566
  },
12567
12567
  {
12568
12568
  "path": "tests/idea_brightness.mjs",
@@ -14597,7 +14597,7 @@
14597
14597
  {
14598
14598
  "path": "tests/sso_checkin_identity_spoof.mjs",
14599
14599
  "mode": "0000644",
14600
- "sha256": "27bd2a87e001eec2bc58e13cfa58e53f88c73ebe7504b944c718182247d98ca7"
14600
+ "sha256": "628db25b4f0c5dc157e4aa9bc9d136efd474e88769641061dfbeca6d3e013cc2"
14601
14601
  },
14602
14602
  {
14603
14603
  "path": "tests/sso_checkin_legacy_field_gone.mjs",
@@ -2537,5 +2537,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2537
2537
  landed since 1.19.1027 with no explicit bump. run 36188894725. (task 1002620)
2538
2538
  1.19.1029 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2539
2539
  landed since 1.19.1028 with no explicit bump. run 36189693139. (task 1002620)
2540
+ 1.19.1030 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2541
+ landed since 1.19.1029 with no explicit bump. run 36190779629. (task 1002620)
2542
+ 1.19.1031 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2543
+ landed since 1.19.1030 with no explicit bump. run 36192093201. (task 1002620)
2540
2544
  ---------------------------------------------------------------------------
2541
2545
  ```
@@ -11,37 +11,53 @@
11
11
  // Compaction (daily granularity after 90 days) is a later, volume-triggered task
12
12
  // — nothing here prunes. A disconnected/re-registered project's snapshots are
13
13
  // likewise not purged here — platform_identity_builder_project_activity (the
14
- // table this mirrors) has the identical gap today; the future leaderboard reader
15
- // is where consent (rollupVisibleSql) is enforced, per that table's own precedent.
14
+ // table this mirrors) has the identical gap today; the leaderboard reader
15
+ // (community-leaderboard.js) is where consent (rollupVisibleSql) is enforced.
16
16
  'use strict';
17
17
 
18
- const { pool: defaultPool } = require('../../src/module-api');
18
+ const api = require('../../src/module-api');
19
19
 
20
- // Append one snapshot row iff the totals differ from the last one on record for
21
- // this (client, builder) pair — ONE statement, not a read then a write: the CTE
22
- // and the conditional INSERT run in the same round trip, closing the two-round-
23
- // trip check-then-insert race the earlier design had. A DB-level race remains
24
- // under READ COMMITTED — two truly concurrent statements for the same pair can
25
- // each see no conflicting row and both insert — but the worst case is a harmless
26
- // duplicate snapshot with IDENTICAL totals, which reads the same either way in
27
- // the windowed-delta arithmetic. A first-ever report (no prior row) always
28
- // inserts — NOT EXISTS is true against an empty CTE regardless of the
29
- // comparison. `id DESC` breaks a tie on an identical `reported_at` so "the last
30
- // snapshot" is always one specific row.
31
- async function recordActivitySnapshot({ clientId, githubId, credits = 0, tasksShipped = 0, karma = 0 }, { pool = defaultPool } = {}) {
32
- const { rows } = await pool.query(
33
- `WITH last AS (
34
- SELECT credits, tasks_shipped, karma FROM platform_identity_activity_snapshots
35
- WHERE client_id = $1 AND github_id = $2
36
- ORDER BY reported_at DESC, id DESC LIMIT 1
37
- )
38
- INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at)
39
- SELECT $1, $2, $3, $4, $5, now()
40
- WHERE NOT EXISTS (SELECT 1 FROM last WHERE credits = $3 AND tasks_shipped = $4 AND karma = $5)
41
- RETURNING id, reported_at`,
42
- [clientId, githubId, credits, tasksShipped, karma],
43
- );
44
- return rows[0] || null;
20
+ // Snapshot the LIVE totals of one (project, builder) iff they differ from the
21
+ // last snapshot. Must run on the transaction that just upserted the live row:
22
+ // that upsert's row lock queues racing reports, so this sees every earlier
23
+ // snapshot and a live row no one else can change (task 1004244).
24
+ // Best-effort: a fault rolls back to the savepoint, is logged, and returns null,
25
+ // so the upsert still commits. If even that rollback fails the transaction is
26
+ // gone, and the error propagates rather than let COMMIT silently drop the upsert.
27
+ // reported_at is the write's own moment (clock_timestamp(); now() would be the
28
+ // transaction start, before any wait on the lock), never earlier than the last
29
+ // row's, so the index order (reported_at, id) always ends at the last write.
30
+ async function recordActivitySnapshot({ clientId, githubId }, { client }) {
31
+ await client.query('SAVEPOINT activity_snapshot');
32
+ try {
33
+ const { rows } = await client.query(
34
+ `WITH live AS (
35
+ SELECT credits, tasks_shipped, karma FROM platform_identity_builder_project_activity
36
+ WHERE client_id = $1 AND github_id = $2
37
+ ), last AS (
38
+ SELECT credits, tasks_shipped, karma, reported_at FROM platform_identity_activity_snapshots
39
+ WHERE client_id = $1 AND github_id = $2
40
+ ORDER BY reported_at DESC, id DESC LIMIT 1
41
+ )
42
+ INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at)
43
+ SELECT $1, $2, live.credits, live.tasks_shipped, live.karma,
44
+ GREATEST(clock_timestamp(), (SELECT reported_at FROM last))
45
+ FROM live
46
+ WHERE NOT EXISTS (
47
+ SELECT 1 FROM last
48
+ WHERE last.credits = live.credits
49
+ AND last.tasks_shipped = live.tasks_shipped
50
+ AND last.karma = live.karma)
51
+ RETURNING id, reported_at`,
52
+ [clientId, githubId],
53
+ );
54
+ await client.query('RELEASE SAVEPOINT activity_snapshot');
55
+ return rows[0] || null;
56
+ } catch (err) {
57
+ await client.query('ROLLBACK TO SAVEPOINT activity_snapshot');
58
+ api.logger('platform-identity').error({ err }, 'activity snapshot write failed (non-fatal)');
59
+ return null;
60
+ }
45
61
  }
46
62
 
47
63
  module.exports = { recordActivitySnapshot };
@@ -666,31 +666,26 @@ async function upsertActivity({ clientId, githubId, credits = 0, tasksShipped =
666
666
  .filter((d) => typeof d === 'string' && d.length > 0 && d.length <= MAX_DISCIPLINE_LENGTH)
667
667
  .slice(0, MAX_ROLLUP_DISCIPLINES);
668
668
  const ships = sanitizeRecentShips(recentShips, clientOrigin);
669
- const { rows } = await pool.query(
670
- `INSERT INTO platform_identity_builder_project_activity
671
- (client_id, github_id, credits, tasks_shipped, karma, rank_held, disciplines, recent_ships, last_active, updated_at)
672
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, now(), now())
673
- ON CONFLICT (client_id, github_id) DO UPDATE SET
674
- credits = EXCLUDED.credits, tasks_shipped = EXCLUDED.tasks_shipped, karma = EXCLUDED.karma,
675
- rank_held = EXCLUDED.rank_held, disciplines = EXCLUDED.disciplines,
676
- recent_ships = EXCLUDED.recent_ships,
677
- last_active = now(), updated_at = now()
678
- RETURNING id, client_id, github_id`,
679
- // jsonb is bound as a JSON STRING: node-postgres serializes a JS array as a
680
- // Postgres ARRAY literal, which the jsonb column would reject.
681
- [clientId, githubId, credits | 0, tasksShipped | 0, karma | 0, safeRank, disc, JSON.stringify(ships)],
682
- );
683
- // Windowed data plane (task 1002977): one snapshot per report whose totals
684
- // changed, skipped on a no-op re-report — see activity-snapshots.js.
685
- // Best-effort, the application-echo.js precedent: descriptive analytics, never
686
- // authority, so a fault here (e.g. migration 022 not yet applied on this
687
- // instance) must never fail the rollup report that already committed above.
688
- try {
689
- await recordActivitySnapshot({ clientId, githubId, credits: credits | 0, tasksShipped: tasksShipped | 0, karma: karma | 0 }, { pool });
690
- } catch (err) {
691
- api.logger('platform-identity').error(`activity snapshot write failed (non-fatal): ${err && err.message}`);
692
- }
693
- return rows[0];
669
+ // One transaction with the snapshot: the upsert's row lock queues racing reports (task 1004244).
670
+ return api.withTx(async (client) => {
671
+ const { rows } = await client.query(
672
+ `INSERT INTO platform_identity_builder_project_activity
673
+ (client_id, github_id, credits, tasks_shipped, karma, rank_held, disciplines, recent_ships, last_active, updated_at)
674
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, now(), now())
675
+ ON CONFLICT (client_id, github_id) DO UPDATE SET
676
+ credits = EXCLUDED.credits, tasks_shipped = EXCLUDED.tasks_shipped, karma = EXCLUDED.karma,
677
+ rank_held = EXCLUDED.rank_held, disciplines = EXCLUDED.disciplines,
678
+ recent_ships = EXCLUDED.recent_ships,
679
+ last_active = now(), updated_at = now()
680
+ RETURNING id, client_id, github_id`,
681
+ // jsonb is bound as a JSON STRING: node-postgres serializes a JS array as a
682
+ // Postgres ARRAY literal, which the jsonb column would reject.
683
+ [clientId, githubId, credits | 0, tasksShipped | 0, karma | 0, safeRank, disc, JSON.stringify(ships)],
684
+ );
685
+ // Windowed data plane (task 1002977) — best-effort, see activity-snapshots.js.
686
+ await recordActivitySnapshot({ clientId, githubId }, { client });
687
+ return rows[0];
688
+ }, { pool });
694
689
  }
695
690
 
696
691
  // The scouting rollup: one row PER BUILDER, aggregated across every project they
@@ -1,11 +1,14 @@
1
1
  // modules/platform-identity/tests/activity-snapshots.mjs
2
2
  //
3
3
  // DB-free unit suite for the windowed data plane's append-only snapshot history
4
- // (community spec D2, task 1002977). Recording-fake-pool style, matching
5
- // tests/platform-identity.mjs: proves the SQL SHAPE (one statement, correct
6
- // table/columns, bound params, the tiebreak) — a fake pool cannot execute the
7
- // `WHERE NOT EXISTS` itself, so the skip-on-unchanged BEHAVIOUR is proved
8
- // against real Postgres in tests/activity_snapshots_db.mjs instead.
4
+ // (community spec D2, task 1002977; the live-row read, task 1004244).
5
+ // Recording-fake-client style, matching tests/platform-identity.mjs: proves the
6
+ // SHAPE — a SAVEPOINT on the caller's transaction, ONE INSERT that reads the
7
+ // totals from the live row (never from the caller) guarded by the last snapshot,
8
+ // RELEASE; on a fault, ROLLBACK TO SAVEPOINT, log the Error, return null. A fake
9
+ // cannot execute `WHERE NOT EXISTS` or a lock, so WHICH reports insert and how
10
+ // racing reports settle are proved against real Postgres in
11
+ // tests/activity_snapshots_db.mjs.
9
12
  //
10
13
  // Run standalone: node modules/platform-identity/tests/activity-snapshots.mjs
11
14
 
@@ -13,6 +16,14 @@ import { strict as assert } from 'node:assert';
13
16
  import { createRequire } from 'node:module';
14
17
 
15
18
  const require = createRequire(import.meta.url);
19
+
20
+ // Pin the doorway's logger so the test can read what a fault logs.
21
+ const logged = [];
22
+ const moduleApiPath = require.resolve('../../../src/module-api.js');
23
+ require.cache[moduleApiPath] = {
24
+ id: moduleApiPath, filename: moduleApiPath, loaded: true,
25
+ exports: { logger: () => ({ error: (obj, msg) => logged.push({ obj, msg }) }) },
26
+ };
16
27
  const { recordActivitySnapshot } = require('../activity-snapshots.js');
17
28
 
18
29
  let passed = 0, failed = 0;
@@ -21,47 +32,75 @@ async function ta(name, fn) {
21
32
  catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
22
33
  }
23
34
 
24
- // A recording fake pool: returns queued responses in order and records every call.
25
- function mockPool(responses = []) {
35
+ // A recording fake client: answers the INSERT with `insertRows`, or fails it;
36
+ // `failRollback` also fails the savepoint rollback (a dead connection).
37
+ function mockClient({ insertRows = [], failInsert = null, failRollback = null } = {}) {
26
38
  const calls = [];
27
- let i = 0;
28
39
  return {
29
40
  calls,
30
41
  query: async (text, params) => {
31
42
  calls.push({ text: String(text), params: params || [] });
32
- const r = responses[i++];
33
- return r || { rows: [], rowCount: 0 };
43
+ if (/INSERT INTO platform_identity_activity_snapshots/.test(text)) {
44
+ if (failInsert) throw failInsert;
45
+ return { rows: insertRows };
46
+ }
47
+ if (/^ROLLBACK TO SAVEPOINT/.test(text) && failRollback) throw failRollback;
48
+ return { rows: [] };
34
49
  },
35
50
  };
36
51
  }
37
52
 
38
- console.log('recordActivitySnapshot (community spec D2, task 1002977):');
53
+ const texts = (client) => client.calls.map((c) => c.text.replace(/\s+/g, ' ').trim());
54
+
55
+ console.log('recordActivitySnapshot (community spec D2, tasks 1002977 + 1004244):');
39
56
 
40
- await ta('one statement: a CTE for the last snapshot, guarding a conditional INSERT', async () => {
41
- const pool = mockPool([{ rows: [{ id: '1', reported_at: 'T1' }] }]);
42
- const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
43
- assert.equal(pool.calls.length, 1, 'one round trip, not a read then a write');
44
- const c = pool.calls[0];
45
- assert.match(c.text, /WITH last AS/);
46
- assert.match(c.text, /FROM platform_identity_activity_snapshots/);
47
- assert.match(c.text, /ORDER BY reported_at DESC, id DESC LIMIT 1/, 'a deterministic tiebreak on an identical reported_at');
48
- assert.match(c.text, /INSERT INTO platform_identity_activity_snapshots/);
49
- assert.match(c.text, /WHERE NOT EXISTS \(SELECT 1 FROM last WHERE credits = \$3 AND tasks_shipped = \$4 AND karma = \$5\)/);
50
- assert.deepEqual(c.params, ['c1', 42, 100, 5, 3]);
57
+ await ta('on the caller\'s transaction: SAVEPOINT, one conditional INSERT, RELEASE', async () => {
58
+ const client = mockClient({ insertRows: [{ id: '1', reported_at: 'T1' }] });
59
+ const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { client });
60
+ const t = texts(client);
61
+ assert.equal(t.length, 3);
62
+ assert.equal(t[0], 'SAVEPOINT activity_snapshot');
63
+ assert.match(t[1], /^WITH live AS/);
64
+ assert.equal(t[2], 'RELEASE SAVEPOINT activity_snapshot');
51
65
  assert.deepEqual(out, { id: '1', reported_at: 'T1' });
52
66
  });
53
67
 
54
- await ta('a skipped report (the fake pool answers no rows, as a real WHERE NOT EXISTS would on a match) returns null', async () => {
55
- const pool = mockPool([{ rows: [] }]);
56
- const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
57
- assert.equal(pool.calls.length, 1);
68
+ await ta('the INSERT reads the totals from the LIVE row, never from the caller', async () => {
69
+ const client = mockClient();
70
+ await recordActivitySnapshot({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { client });
71
+ assert.deepEqual(client.calls[1].params, ['c1', 42], 'only the pair is bound — a captured total has nowhere to go');
72
+ const sql = texts(client)[1];
73
+ assert.match(sql, /WITH live AS \( SELECT credits, tasks_shipped, karma FROM platform_identity_builder_project_activity WHERE client_id = \$1 AND github_id = \$2 \)/);
74
+ assert.match(sql, /SELECT \$1, \$2, live\.credits, live\.tasks_shipped, live\.karma, GREATEST\(clock_timestamp\(\), \(SELECT reported_at FROM last\)\) FROM live/,
75
+ 'stamped at the write, and never earlier than the last row');
76
+ assert.ok(!/\$3/.test(sql), 'no third parameter exists to carry a caller value');
77
+ });
78
+
79
+ await ta('skipped when the live totals equal the last snapshot, found in migration 022\'s index order', async () => {
80
+ const client = mockClient();
81
+ const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { client });
82
+ const sql = texts(client)[1];
83
+ assert.match(sql, /last AS \( SELECT credits, tasks_shipped, karma, reported_at FROM platform_identity_activity_snapshots WHERE client_id = \$1 AND github_id = \$2 ORDER BY reported_at DESC, id DESC LIMIT 1 \)/,
84
+ '(client_id, github_id, reported_at DESC, id DESC) serves this as an index-order LIMIT 1');
85
+ assert.match(sql, /WHERE NOT EXISTS \( SELECT 1 FROM last WHERE last\.credits = live\.credits AND last\.tasks_shipped = live\.tasks_shipped AND last\.karma = live\.karma\)/);
86
+ assert.equal(out, null, 'no row returned → null');
87
+ });
88
+
89
+ await ta('a fault rolls back to the savepoint, logs the Error itself, and returns null — the caller\'s upsert still commits', async () => {
90
+ logged.length = 0;
91
+ const fault = Object.assign(new Error('relation "platform_identity_activity_snapshots" does not exist'), { code: '42P01' });
92
+ const client = mockClient({ failInsert: fault });
93
+ const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { client });
58
94
  assert.equal(out, null);
95
+ const t = texts(client);
96
+ assert.equal(t[t.length - 1], 'ROLLBACK TO SAVEPOINT activity_snapshot');
97
+ assert.equal(logged.length, 1);
98
+ assert.equal(logged[0].obj.err, fault, 'the Error object itself, so its stack and SQLSTATE reach the log');
59
99
  });
60
100
 
61
- await ta('missing totals default to zero, like upsertActivity\'s own defaults', async () => {
62
- const pool = mockPool([{ rows: [{ id: '3', reported_at: 'T3' }] }]);
63
- await recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { pool });
64
- assert.deepEqual(pool.calls[0].params, ['c1', 42, 0, 0, 0]);
101
+ await ta('if the savepoint rollback itself fails, the error propagates — COMMIT must not silently drop the upsert', async () => {
102
+ const client = mockClient({ failInsert: new Error('boom'), failRollback: new Error('connection terminated') });
103
+ await assert.rejects(() => recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { client }), /connection terminated/);
65
104
  });
66
105
 
67
106
  console.log(`\n${passed} passed, ${failed} failed`);
@@ -558,11 +558,26 @@ await ta('getAccountByLogin resolves case-insensitively, newest first', async ()
558
558
 
559
559
  console.log('\ncross-project activity (§4 scouting data plane):');
560
560
 
561
+ // upsertActivity runs in a transaction, so a positional answer queue would be
562
+ // spent by BEGIN: answer by statement shape instead, and find the upsert by text.
563
+ const activityPool = ({ failSnapshot = false } = {}) => {
564
+ const calls = [];
565
+ const query = async (text, params) => {
566
+ calls.push({ text: String(text), params: params || [] });
567
+ if (failSnapshot && /INSERT INTO platform_identity_activity_snapshots/.test(text)) {
568
+ throw new Error('relation "platform_identity_activity_snapshots" does not exist');
569
+ }
570
+ if (/INSERT INTO platform_identity_builder_project_activity/.test(text)) return { rows: [{ id: '1' }] };
571
+ return { rows: [] };
572
+ };
573
+ return { calls, query, connect: async () => ({ query, release: () => {} }) };
574
+ };
575
+ const upsertCall = (pool) => pool.calls.find((c) => /INSERT INTO platform_identity_builder_project_activity/.test(c.text));
576
+
561
577
  await ta('upsertActivity upserts per (client_id, github_id) with the talent snapshot', async () => {
562
- const pool = mockPool([{ rows: [{ id: '1' }] }]);
578
+ const pool = activityPool();
563
579
  await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3, rank: 'metic', disciplines: ['engineer'] }, { pool });
564
- const c = pool.calls[0];
565
- assert.match(c.text, /INSERT INTO platform_identity_builder_project_activity/);
580
+ const c = upsertCall(pool);
566
581
  assert.match(c.text, /ON CONFLICT \(client_id, github_id\) DO UPDATE/);
567
582
  // recent_ships (task 1002803) rides the same row as a jsonb STRING param —
568
583
  // an empty list when the sender reported none, never NULL. Its own cap,
@@ -571,7 +586,7 @@ await ta('upsertActivity upserts per (client_id, github_id) with the talent snap
571
586
  });
572
587
 
573
588
  await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-ladder rank → null, disciplines validated + capped', async () => {
574
- const pool = mockPool([{ rows: [{ id: '1' }] }]);
589
+ const pool = activityPool();
575
590
  await P.upsertActivity({
576
591
  clientId: 'c1', githubId: 42,
577
592
  rank: 'Supreme Overlord', // not one of the four core rank keys → stored null
@@ -581,7 +596,7 @@ await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-la
581
596
  'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', // valid tail — capped to 8 total
582
597
  ],
583
598
  }, { pool });
584
- const c = pool.calls[0];
599
+ const c = upsertCall(pool);
585
600
  assert.equal(c.params[5], null, 'an invented rank label is never stored');
586
601
  const disc = c.params[6];
587
602
  assert.ok(disc.length <= 8, `disciplines capped at 8 (got ${disc.length})`);
@@ -591,30 +606,35 @@ await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-la
591
606
  });
592
607
 
593
608
  await ta('upsertActivity keeps a legitimate core rank key', async () => {
594
- const pool = mockPool([{ rows: [{ id: '1' }] }]);
609
+ const pool = activityPool();
595
610
  await P.upsertActivity({ clientId: 'c1', githubId: 42, rank: 'archon', disciplines: [] }, { pool });
596
- assert.equal(pool.calls[0].params[5], 'archon');
597
- });
598
-
599
- await ta('upsertActivity also appends to the snapshot history (task 1002977, community spec D2) — deleting that call would leave this red', async () => {
600
- const pool = mockPool([{ rows: [{ id: '1' }] }, { rows: [{ id: 's1', reported_at: 'T' }] }]);
601
- await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
602
- assert.equal(pool.calls.length, 2, 'the rollup upsert, then the snapshot statement');
603
- assert.match(pool.calls[1].text, /INSERT INTO platform_identity_activity_snapshots/);
604
- assert.deepEqual(pool.calls[1].params, ['c1', 42, 100, 5, 3]);
611
+ assert.equal(upsertCall(pool).params[5], 'archon');
605
612
  });
606
613
 
607
- await ta('upsertActivity survives a snapshot-write failure (best-effort — the application-echo.js precedent): the rollup report still succeeds', async () => {
608
- const pool = {
609
- calls: [],
610
- query: async (text, params) => {
611
- pool.calls.push({ text: String(text), params });
612
- if (/INSERT INTO platform_identity_activity_snapshots/.test(text)) throw new Error('relation "platform_identity_activity_snapshots" does not exist');
613
- return { rows: [{ id: '1' }] };
614
- },
615
- };
614
+ await ta('upsertActivity writes the upsert and its snapshot in ONE transaction, the snapshot reading the live row (tasks 1002977 + 1004244)', async () => {
615
+ const pool = activityPool();
616
+ const out = await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
617
+ const t = pool.calls.map((c) => c.text.replace(/\s+/g, ' ').trim().split(' ').slice(0, 4).join(' '));
618
+ assert.deepEqual(t, [
619
+ 'BEGIN',
620
+ 'INSERT INTO platform_identity_builder_project_activity (client_id,',
621
+ 'SAVEPOINT activity_snapshot',
622
+ 'WITH live AS (',
623
+ 'RELEASE SAVEPOINT activity_snapshot',
624
+ 'COMMIT',
625
+ ], 'the upsert holds the row lock until its snapshot is written; deleting the snapshot call leaves this red');
626
+ const snap = pool.calls.find((c) => /INSERT INTO platform_identity_activity_snapshots/.test(c.text));
627
+ assert.deepEqual(snap.params, ['c1', 42], 'the report\'s own totals are never handed to the snapshot');
628
+ assert.equal(out.id, '1');
629
+ });
630
+
631
+ await ta('upsertActivity survives a snapshot-write failure (best-effort — the application-echo.js precedent): the savepoint rolls back alone and the upsert commits', async () => {
632
+ const pool = activityPool({ failSnapshot: true });
616
633
  const out = await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
617
634
  assert.equal(out.id, '1', 'the rollup upsert result still returns despite the snapshot write throwing');
635
+ const t = pool.calls.map((c) => c.text.trim());
636
+ assert.ok(t.includes('ROLLBACK TO SAVEPOINT activity_snapshot'));
637
+ assert.equal(t[t.length - 1], 'COMMIT', 'the transaction still commits the upsert');
618
638
  });
619
639
 
620
640
  await ta('getMembership reads one (client, builder) row — the rollup membership requirement', async () => {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1029",
3
+ "version": "1.19.1031",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1029",
9
+ "version": "1.19.1031",
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.19.1029",
3
+ "version": "1.19.1031",
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",
@@ -162,6 +162,20 @@ const WORKER_ENV_ALLOW = [
162
162
  'BONGOS_API_BASE', 'CLOUDBONGOS_API_BASE',
163
163
  'OTB_CLAUDE_BIN', 'CLAUDE_BIN',
164
164
  'XDG_CONFIG_HOME',
165
+ // THE WORKER'S OWN CREDENTIAL, and it has to be here (task 1003907, found by
166
+ // running it). On a desktop, the claude CLI authenticates from the credential
167
+ // store under USERPROFILE/HOME, which are already allowed - so this list looked
168
+ // complete. On a HEADLESS host there is no interactive login to have written
169
+ // one, and setup.js's own preflight says to set CLAUDE_CODE_OAUTH_TOKEN there
170
+ // instead. Stripping it meant every worker on the machine this was built for
171
+ // started unauthenticated, exited in ~2s, and the run recorded only "worker
172
+ // output was not JSON".
173
+ //
174
+ // This is not the leak the list exists to prevent. The worker IS a Claude
175
+ // session; its own auth is the one credential it cannot do its job without.
176
+ // Everything the comment above refuses - DB URLs, git and npm tokens - stays
177
+ // refused.
178
+ 'CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY',
165
179
  ];
166
180
 
167
181
  function workerEnv(source = process.env, extra = {}) {
@@ -175,12 +189,46 @@ function workerEnv(source = process.env, extra = {}) {
175
189
 
176
190
  const MAX_WORKER_OUTPUT = 8 * 1024 * 1024; // a misbehaving worker must not grow memory until the timeout
177
191
 
192
+ // resolveWorkerBin — WHICH `claude`, and whether it needs a shell.
193
+ //
194
+ // THE BUG THIS EXISTS FOR (task 1003907, found by running it on the real Windows
195
+ // host). On Windows `claude` on PATH is a .cmd shim, and Node has refused to
196
+ // spawn .cmd/.bat without a shell since CVE-2024-27980 — so this used to pass
197
+ // `shell: process.platform === 'win32'`. But `shell: true` does not pass argv
198
+ // through: it CONCATENATES it into one command line that cmd.exe re-parses,
199
+ // which destroys every embedded quote. `--json-schema` is a JSON blob made
200
+ // almost entirely of quotes, so claude received a mangled argument and exited in
201
+ // ~2s with `--json-schema is not valid JSON`. Every worker. Every run. Forever —
202
+ // and the cadence classes that as `quiet`, so it never trips the failure ladder.
203
+ //
204
+ // The npm package ships a REAL claude.exe beside the shim, so prefer it: a true
205
+ // executable spawns with shell:false and exact argv, and nothing re-parses.
206
+ // Falling back to the old behaviour is deliberate — a host with a differently
207
+ // laid-out install should still run, and a corrupted schema surfaces as a failed
208
+ // worker rather than as a refusal to start at all.
209
+ function resolveWorkerBin(env = process.env, platform = process.platform) {
210
+ const override = env.OTB_CLAUDE_BIN || env.CLAUDE_BIN;
211
+ if (override) return { bin: override, shell: platform === 'win32' && /\.(cmd|bat)$/i.test(override) };
212
+ if (platform !== 'win32') return { bin: 'claude', shell: false };
213
+ for (const dir of String(env.PATH || env.Path || '').split(path.delimiter)) {
214
+ if (!dir) continue;
215
+ const candidates = [
216
+ path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
217
+ path.join(dir, 'claude.exe'),
218
+ ];
219
+ for (const c of candidates) {
220
+ try { if (fs.existsSync(c)) return { bin: c, shell: false }; } catch (_) {}
221
+ }
222
+ }
223
+ return { bin: 'claude', shell: true };
224
+ }
225
+
178
226
  // spawnWorker — one headless session, bounded by WALL CLOCK, in the task's own
179
227
  // working tree.
180
228
  async function spawnWorker({ task, model, timeoutMs, cwd, deps = {} }) {
181
229
  const spawnFn = deps.spawn || spawn;
182
230
  const sessionId = deps.sessionId || crypto.randomUUID(); // pinned, so the session can be resumed
183
- const bin = process.env.OTB_CLAUDE_BIN || process.env.CLAUDE_BIN || 'claude';
231
+ const { bin, shell: workerShell } = deps.resolveBin ? deps.resolveBin() : resolveWorkerBin();
184
232
  const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
185
233
  const args = buildWorkerArgs({ sessionId, model, isRoot });
186
234
  const prompt = buildWorkerPrompt({ task });
@@ -194,7 +242,7 @@ async function spawnWorker({ task, model, timeoutMs, cwd, deps = {} }) {
194
242
  // `claude -p` with an open inherited stdin waits seconds for input that
195
243
  // never comes, on every single spawn.
196
244
  stdio: ['pipe', 'pipe', 'pipe'],
197
- shell: process.platform === 'win32', // `claude` is a .cmd shim on Windows
245
+ shell: workerShell,
198
246
  windowsHide: true,
199
247
  env: workerEnv(),
200
248
  });
@@ -749,7 +797,7 @@ if (require.main === module) {
749
797
  }
750
798
 
751
799
  module.exports = {
752
- readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW,
800
+ readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW, resolveWorkerBin,
753
801
  worktreeName, readFence, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX,
754
802
  heartbeatPath, writeHeartbeat, readHeartbeat, pidAlive, anotherRunnerIsAlive, HEARTBEAT_STALE_MS,
755
803
  isRunnerTree, RUNNER_TREE_RE, postHeartbeat, RUNNER_STARTED_AT,
@@ -89,7 +89,12 @@ switch ($Action) {
89
89
  Assert-Prereqs
90
90
  New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
91
91
 
92
- $action = New-ScheduledTaskAction -Execute $Wrapper -WorkingDirectory $RepoRoot
92
+ # NOT $action: PowerShell variable names are CASE-INSENSITIVE, so $action IS the
93
+ # $Action parameter above, and its [ValidateSet]/[string] attributes stay attached
94
+ # to the variable. Assigning a task-action object to it throws "the value
95
+ # MSFT_TaskExecAction is not a valid value for the Action variable" and install
96
+ # fails every time, on every machine. Same trap as the `run` shadow in task 1003908.
97
+ $taskAction = New-ScheduledTaskAction -Execute $Wrapper -WorkingDirectory $RepoRoot
93
98
  # AtStartup covers the cold reboot the done-when actually names. AtLogOn is
94
99
  # NOT used: it would wait for someone to sign in, which is the one thing that
95
100
  # must not be required.
@@ -107,7 +112,7 @@ switch ($Action) {
107
112
  # MultipleInstances IgnoreNew = the scheduler will not start a second copy
108
113
  # over a live one; the heartbeat file is the second guard, inside the runner.
109
114
 
110
- Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
115
+ Register-ScheduledTask -TaskName $TaskName -Action $taskAction -Trigger $trigger `
111
116
  -Principal $principal -Settings $settings -Force | Out-Null
112
117
 
113
118
  Write-Host "installed '$TaskName' -- starts at boot, restarts on crash, logs to $LogFile"
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.1029'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1031'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -1,11 +1,12 @@
1
- // tests/activity_snapshots_db.mjs — task 1002977 (community spec D2; real DB).
1
+ // tests/activity_snapshots_db.mjs — tasks 1002977 + 1004244 (community spec D2; real DB).
2
2
  //
3
3
  // Proves the windowed data plane's append-only history against real SQL. The
4
- // skip-on-unchanged rule lives entirely inside one INSERT ... WHERE NOT EXISTS
5
- // statement (activity-snapshots.js), so a fake pool can only prove the query
6
- // SHAPE (modules/platform-identity/tests/activity-snapshots.mjs); only a real
7
- // Postgres can prove WHICH reports it actually inserts, and that upsertActivity
8
- // (the existing POST /sso/activity/rollup path) really calls it end to end.
4
+ // skip-on-unchanged rule lives inside one INSERT ... WHERE NOT EXISTS, and racing
5
+ // reports are serialised by the upsert's own row lock, so a fake pool can only
6
+ // prove the SHAPE (modules/platform-identity/tests/activity-snapshots.mjs); only
7
+ // a real Postgres can prove WHICH reports insert, that upsertActivity (the
8
+ // POST /sso/activity/rollup path) writes the upsert and its snapshot together,
9
+ // and that the newest snapshot always agrees with the live row.
9
10
  //
10
11
  // Real-DB integration test: self-skips without a reachable Postgres; in the
11
12
  // INTEGRATION set (scripts/gds/run-unit-tests.js) so the default DB-free lane
@@ -37,58 +38,126 @@ try {
37
38
  }
38
39
 
39
40
  // High-numbered fixture ids so a shared DB can never collide with real data.
40
- const BUILDER = 90002977, OTHER_BUILDER = 90002978;
41
- const IDS = [BUILDER, OTHER_BUILDER];
41
+ const BUILDER = 90002977, OTHER_BUILDER = 90002978, RACE_BUILDER = 90002979, FLIGHT_BUILDER = 90002980;
42
+ const IDS = [BUILDER, OTHER_BUILDER, RACE_BUILDER, FLIGHT_BUILDER];
42
43
  const CLIENT = 't1002977-client';
43
44
  const q = (sql, params) => pool.query(sql, params);
45
+ const report = (githubId, credits) =>
46
+ pi.upsertActivity({ clientId: CLIENT, githubId, credits, tasksShipped: 1, karma: 0 }, { pool });
44
47
 
45
48
  async function cleanup() {
46
49
  await q(`DELETE FROM platform_identity_activity_snapshots WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
47
50
  await q(`DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
48
51
  }
52
+ // The writer runs on its caller's transaction, right after the live row is set.
53
+ async function setLiveAndSnapshot(githubId, credits) {
54
+ const client = await pool.connect();
55
+ try {
56
+ await client.query('BEGIN');
57
+ await client.query(
58
+ `INSERT INTO platform_identity_builder_project_activity (client_id, github_id, credits, tasks_shipped, karma)
59
+ VALUES ($1, $2, $3, 5, 3)
60
+ ON CONFLICT (client_id, github_id) DO UPDATE SET credits = EXCLUDED.credits`,
61
+ [CLIENT, githubId, credits],
62
+ );
63
+ const row = await recordActivitySnapshot({ clientId: CLIENT, githubId }, { client });
64
+ await client.query('COMMIT');
65
+ return row;
66
+ } finally {
67
+ client.release();
68
+ }
69
+ }
70
+ async function history(githubId) {
71
+ const { rows } = await q(
72
+ `SELECT credits FROM platform_identity_activity_snapshots
73
+ WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at, id`,
74
+ [CLIENT, githubId],
75
+ );
76
+ return rows.map((r) => r.credits);
77
+ }
78
+ // The newest snapshot (in the index order the writer and the leaderboard use) against the live row.
79
+ async function newestVsLive(githubId) {
80
+ const { rows } = await q(
81
+ `SELECT (SELECT credits FROM platform_identity_activity_snapshots
82
+ WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at DESC, id DESC LIMIT 1) AS newest,
83
+ (SELECT credits FROM platform_identity_builder_project_activity
84
+ WHERE client_id = $1 AND github_id = $2) AS live`,
85
+ [CLIENT, githubId],
86
+ );
87
+ return [rows[0].newest, rows[0].live];
88
+ }
49
89
 
50
90
  let pass = 0;
51
91
  const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
92
+ const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg); pass++; console.log(` ok - ${msg}`); };
52
93
 
53
94
  try {
54
95
  await q(migrationSql('platform_identity_005_builder_activity.sql'));
55
96
  // upsertActivity's INSERT always names recent_ships (012_recent_ships adds the
56
- // column); without it here, step 2 below fails 42703 on a fresh database.
97
+ // column); without it here, the upsertActivity steps fail 42703 on a fresh database.
57
98
  await q(migrationSql('platform_identity_012_recent_ships.sql'));
58
99
  await q(migrationSql('platform_identity_022_activity_snapshots.sql'));
59
100
  await cleanup();
60
101
 
61
- // 1 — recordActivitySnapshot directly: the changed-totals skip rule.
62
- const first = await recordActivitySnapshot({ clientId: CLIENT, githubId: BUILDER, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
63
- ok(first !== null, 'a first-ever report inserts — nothing to be unchanged from');
64
-
65
- const unchanged = await recordActivitySnapshot({ clientId: CLIENT, githubId: BUILDER, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
66
- ok(unchanged === null, 'an identical re-report inserts nothing');
67
-
68
- const changed = await recordActivitySnapshot({ clientId: CLIENT, githubId: BUILDER, credits: 150, tasksShipped: 5, karma: 3 }, { pool });
69
- ok(changed !== null, 'a changed report (credits differ) inserts a new row');
70
-
71
- const { rows: history } = await q(
72
- `SELECT credits FROM platform_identity_activity_snapshots
73
- WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at, id`,
102
+ // 1 — the writer snapshots the LIVE row, and only when it changed.
103
+ ok(await setLiveAndSnapshot(BUILDER, 100) !== null, 'a first-ever report inserts — nothing to be unchanged from');
104
+ ok(await setLiveAndSnapshot(BUILDER, 100) === null, 'an unchanged live row inserts nothing');
105
+ ok(await setLiveAndSnapshot(BUILDER, 150) !== null, 'a changed live row (credits differ) inserts a new row');
106
+ same(await history(BUILDER), [100, 150], 'exactly two rows, in write order — the unchanged report left no trace');
107
+ // A clock that stepped back: the last row carries a later stamp than "now".
108
+ await q(
109
+ `UPDATE platform_identity_activity_snapshots SET reported_at = clock_timestamp() + interval '1 hour'
110
+ WHERE client_id = $1 AND github_id = $2 AND credits = 150`,
74
111
  [CLIENT, BUILDER],
75
112
  );
76
- ok(history.length === 2, 'exactly two rows exist — the unchanged report left no trace');
77
- assert.deepEqual(history.map((r) => r.credits), [100, 150]);
78
- pass++; console.log(' ok - the history is in report order');
113
+ const later = await setLiveAndSnapshot(BUILDER, 175);
114
+ same((await newestVsLive(BUILDER))[0], 175, 'a write after the clock stepped back is still the newest in index order');
115
+ ok(later !== null, 'and it was written, stamped no earlier than the row before it');
79
116
 
80
- // 2 — upsertActivity (the real POST /sso/activity/rollup path) really calls it.
81
- await pi.upsertActivity({ clientId: CLIENT, githubId: OTHER_BUILDER, credits: 10, tasksShipped: 1, karma: 0 }, { pool });
82
- await pi.upsertActivity({ clientId: CLIENT, githubId: OTHER_BUILDER, credits: 10, tasksShipped: 1, karma: 0 }, { pool });
83
- await pi.upsertActivity({ clientId: CLIENT, githubId: OTHER_BUILDER, credits: 20, tasksShipped: 1, karma: 0 }, { pool });
84
- const { rows: viaRollup } = await q(
85
- `SELECT credits FROM platform_identity_activity_snapshots
86
- WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at, id`,
87
- [CLIENT, OTHER_BUILDER],
88
- );
89
- ok(viaRollup.length === 2, 'upsertActivity appends a snapshot per changed report — two calls, one repeat, one row skipped');
90
- assert.deepEqual(viaRollup.map((r) => r.credits), [10, 20]);
91
- pass++; console.log(' ok - deleting the call inside upsertActivity would leave this red');
117
+ // 2 — a report whose snapshot is still pending holds the row: its upsert is
118
+ // not visible yet, and a second report queues behind it. The prior state is the
119
+ // hard case the live-row read alone left open — the last snapshot (110) already
120
+ // equals the fresh report, while a stale report (100) is mid-flight. A table
121
+ // lock parks the stale report at its snapshot INSERT, deterministically.
122
+ await report(FLIGHT_BUILDER, 110);
123
+ const parker = await pool.connect();
124
+ let stale, fresh;
125
+ const settled = { stale: false, fresh: false };
126
+ try {
127
+ await parker.query('BEGIN');
128
+ await parker.query('LOCK TABLE platform_identity_activity_snapshots IN SHARE ROW EXCLUSIVE MODE');
129
+ stale = report(FLIGHT_BUILDER, 100).then(() => { settled.stale = true; });
130
+ await new Promise((r) => setTimeout(r, 300));
131
+ same((await newestVsLive(FLIGHT_BUILDER))[1], 110,
132
+ "the stale report's upsert is invisible while its snapshot is pending — the two commit together");
133
+ fresh = report(FLIGHT_BUILDER, 110).then(() => { settled.fresh = true; });
134
+ await new Promise((r) => setTimeout(r, 300));
135
+ ok(!settled.stale && !settled.fresh, 'and the fresh report queues behind it on the row lock');
136
+ await parker.query('COMMIT');
137
+ } finally {
138
+ parker.release();
139
+ }
140
+ await Promise.all([stale, fresh]);
141
+ same(await newestVsLive(FLIGHT_BUILDER), [110, 110],
142
+ 'once both land, the newest snapshot agrees with the live row (the stale value is recorded, then superseded)');
143
+ same(await history(FLIGHT_BUILDER), [110, 100, 110], 'every change the live row went through is in the history');
144
+
145
+ // 3 — racing reports, end to end: however they interleave, once they settle
146
+ // the newest snapshot equals the live row.
147
+ for (let round = 0; round < 15; round++) {
148
+ const credits = [0, 1, 2, 3, 4].map((i) => 1000 + round * 10 + ((i * 7 + round) % 5));
149
+ await Promise.all(credits.map((c) => report(RACE_BUILDER, c)));
150
+ const [newest, live] = await newestVsLive(RACE_BUILDER);
151
+ assert.equal(newest, live, `round ${round}: newest snapshot ${newest} disagrees with the live row ${live}`);
152
+ }
153
+ pass++; console.log(' ok - 15 rounds of five racing reports: the newest snapshot always equals the live row');
154
+
155
+ // 4 — upsertActivity (the real POST /sso/activity/rollup path) really snapshots.
156
+ await report(OTHER_BUILDER, 10);
157
+ await report(OTHER_BUILDER, 10);
158
+ await report(OTHER_BUILDER, 20);
159
+ same(await history(OTHER_BUILDER), [10, 20],
160
+ 'upsertActivity appends a snapshot per changed report — deleting the call inside it would leave this red');
92
161
 
93
162
  console.log(`\nactivity_snapshots_db.mjs: ${pass} passed`);
94
163
  } finally {
@@ -563,3 +563,94 @@ test('the module-level exec helper is reachable from inside iteration()', async
563
563
  assert.equal(picked, true, 'iteration must reach the picker without touching an un-injected binding');
564
564
  assert.equal(row.event, 'nothing_claimable');
565
565
  });
566
+
567
+ // --- resolveWorkerBin: the Windows spawn that corrupted every worker ---------
568
+ //
569
+ // Found by task 1003907 running the thing rather than testing it. `shell: true`
570
+ // does not pass argv through on Windows: it CONCATENATES it into one command
571
+ // line that cmd.exe re-parses, destroying the quotes in --json-schema. claude
572
+ // then exited in ~2s with "--json-schema is not valid JSON" on EVERY worker, and
573
+ // cadence.js classes that outcome as `quiet`, which neither trips nor forgives
574
+ // the failure count — so a totally broken runner never escalated and looked
575
+ // healthy in the hall. The invariant: on win32, never ask for a shell when a
576
+ // real executable is available.
577
+ //
578
+ // Paths here use forward slashes deliberately. The resolver only string-matches
579
+ // the extension on an override, and backslash-heavy literals are their own
580
+ // source of bugs in a test that exists because of quoting.
581
+
582
+ test('resolveWorkerBin: a .cmd override still gets its shell', () => {
583
+ const got = runner.resolveWorkerBin({ CLAUDE_BIN: 'C:/x/claude.cmd' }, 'win32');
584
+ assert.equal(got.bin, 'C:/x/claude.cmd');
585
+ assert.equal(got.shell, true, 'Node cannot spawn a .cmd without a shell (CVE-2024-27980)');
586
+ });
587
+
588
+ test('resolveWorkerBin: a real .exe override is never re-parsed by cmd.exe', () => {
589
+ const got = runner.resolveWorkerBin({ OTB_CLAUDE_BIN: 'C:/x/claude.exe' }, 'win32');
590
+ assert.equal(got.shell, false);
591
+ });
592
+
593
+ test('resolveWorkerBin: win32 with nothing on PATH falls back rather than refusing', () => {
594
+ const got = runner.resolveWorkerBin({ PATH: 'C:/nowhere-at-all' }, 'win32');
595
+ assert.deepEqual(got, { bin: 'claude', shell: true },
596
+ 'a differently-laid-out install must still start; a mangled schema surfaces as a failed worker');
597
+ });
598
+
599
+ test('resolveWorkerBin: non-windows never asks for a shell', () => {
600
+ assert.deepEqual(runner.resolveWorkerBin({ PATH: '/usr/bin' }, 'linux'), { bin: 'claude', shell: false });
601
+ });
602
+
603
+ test('the schema argument is the one that cannot survive concatenation', () => {
604
+ const args = buildWorkerArgs({ sessionId: 'abc' });
605
+ const schema = args[args.indexOf('--json-schema') + 1];
606
+ assert.ok(schema.includes('"'), 'the regression only bites because the schema carries quotes');
607
+ assert.ok(schema.length > 50, 'and because it must arrive as one intact argv slot');
608
+ });
609
+
610
+ // The test that would actually have caught the shipped bug: the resolver's
611
+ // answer has to REACH the spawn options. Testing resolveWorkerBin alone leaves
612
+ // `shell: process.platform === 'win32'` free to reappear at the call site.
613
+ test('spawnWorker passes the resolver’s shell verdict straight to spawn', async () => {
614
+ const { EventEmitter } = require('node:events');
615
+ let opts = null; let binSeen = null; let argvSeen = null;
616
+ const fakeSpawn = (bin, args, o) => {
617
+ binSeen = bin; argvSeen = args; opts = o;
618
+ const child = new EventEmitter();
619
+ child.stdout = new EventEmitter();
620
+ child.stderr = new EventEmitter();
621
+ child.stdin = { end: () => setImmediate(() => child.emit('close', 0)) };
622
+ child.kill = () => {};
623
+ return child;
624
+ };
625
+ await runner.spawnWorker({
626
+ task: { id: 7, title: 't', description: 'd' },
627
+ timeoutMs: 1000,
628
+ cwd: '.',
629
+ deps: { spawn: fakeSpawn, resolveBin: () => ({ bin: 'C:/x/claude.exe', shell: false }) },
630
+ });
631
+ assert.equal(opts.shell, false, 'a resolved .exe must spawn WITHOUT a shell — this is the whole fix');
632
+ assert.equal(binSeen, 'C:/x/claude.exe');
633
+ const schema = argvSeen[argvSeen.indexOf('--json-schema') + 1];
634
+ assert.equal(schema, JSON.stringify(WORKER_VERDICT_SCHEMA), 'the schema must arrive as one intact argv slot');
635
+ });
636
+
637
+ // The worker's own credential must survive the env allowlist (task 1003907).
638
+ // Stripping it meant every worker on a headless host started unauthenticated and
639
+ // exited in ~2s with no usable output — while the supervisor recorded only
640
+ // "worker output was not JSON" and the cadence called that quiet.
641
+ test('workerEnv passes Claude auth through, and still refuses the rest', () => {
642
+ const src = {
643
+ PATH: '/usr/bin',
644
+ CLAUDE_CODE_OAUTH_TOKEN: 'tok',
645
+ ANTHROPIC_API_KEY: 'key',
646
+ DATABASE_URL: 'postgres://nope',
647
+ GITHUB_TOKEN: 'ghp_nope',
648
+ NPM_TOKEN: 'npm_nope',
649
+ };
650
+ const env = runner.workerEnv(src);
651
+ assert.equal(env.CLAUDE_CODE_OAUTH_TOKEN, 'tok', 'a worker that cannot authenticate cannot work');
652
+ assert.equal(env.ANTHROPIC_API_KEY, 'key');
653
+ for (const leak of ['DATABASE_URL', 'GITHUB_TOKEN', 'NPM_TOKEN']) {
654
+ assert.equal(env[leak], undefined, `${leak} must never reach a bypassPermissions worker`);
655
+ }
656
+ });
@@ -50,12 +50,15 @@ async function t(name, fn) {
50
50
  // asserts the params the real query actually bound.
51
51
  function mockPool(answer = () => ({ rows: [] })) {
52
52
  const calls = [];
53
+ const query = async (text, params = []) => {
54
+ calls.push({ text: String(text), params });
55
+ return answer(String(text), params) || { rows: [] };
56
+ };
53
57
  return {
54
58
  calls,
55
- query: async (text, params = []) => {
56
- calls.push({ text: String(text), params });
57
- return answer(String(text), params) || { rows: [] };
58
- },
59
+ query,
60
+ // upsertActivity runs its upsert + snapshot as one transaction on a pooled client.
61
+ connect: async () => ({ query, release: () => {} }),
59
62
  find: (re) => calls.find((c) => re.test(c.text)),
60
63
  };
61
64
  }
@@ -73,6 +73,7 @@ require.cache[moduleApiPath] = {
73
73
  id: moduleApiPath, filename: moduleApiPath, loaded: true,
74
74
  exports: {
75
75
  pool,
76
+ withTx: (fn) => fn(pool), // transactions are not under test; keep the queue positional
76
77
  validateOrRespond: helpers.validateOrRespond,
77
78
  resolveSession: async () => null, // the browser leg is not under test
78
79
  logger: () => noopLog,