@bongos/core 1.19.822 → 1.19.823

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.19.822",
6
- "core_contract": "1.19.822",
7
- "source_commit": "5a9c009cff17c2a500ff23f5d0de66a1b7f00477",
5
+ "core_version": "1.19.823",
6
+ "core_contract": "1.19.823",
7
+ "source_commit": "2dbaf2b0a9d49393d3f3daf6f501a09087b8a156",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-17T11:05:47.254Z",
9
+ "built_at": "2026-09-17T11:55:48.898Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 490,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2228,
14
+ "functional_verbatim": 2230,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2744,
20
- "tree_sha256": "acbc13ab96c308bd0c0aa200c9dd1f6af1013ed6771c70730a797f7a1fb4a697",
19
+ "file_count": 2746,
20
+ "tree_sha256": "6b21d5cba0536e95777730af1de9c226f7d38a89997a4cb586847a39748e7f37",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2547,7 +2547,7 @@
2547
2547
  {
2548
2548
  "path": "docs/module-api-changelog.md",
2549
2549
  "mode": "0000644",
2550
- "sha256": "9182a3b88e6092a231f59db4220628cb8579244c8289350406f07187c959e644"
2550
+ "sha256": "5236a1550b3f87d767e2f59479b3f2ef8859621f0c57bbd7a054b8d056c1a2ef"
2551
2551
  },
2552
2552
  {
2553
2553
  "path": "docs/modules-contract.md",
@@ -7812,12 +7812,12 @@
7812
7812
  {
7813
7813
  "path": "package-lock.json",
7814
7814
  "mode": "0000644",
7815
- "sha256": "d13165aac1aea3612a3c5af1b5b14267acdde35f2a8d99482310c7a749f9761c"
7815
+ "sha256": "ade8e48efd7ae7eaeb9f51472f731940ca866a1f0a8917de7dadab245428908b"
7816
7816
  },
7817
7817
  {
7818
7818
  "path": "package.json",
7819
7819
  "mode": "0000644",
7820
- "sha256": "0f9f0baab592f70e890e2a39e61dcf21c5170841f97b8c82cb16703c2a959756"
7820
+ "sha256": "bd706beddc637ac4587a99436be805e327d7392306525124874ea39092fa6149"
7821
7821
  },
7822
7822
  {
7823
7823
  "path": "public-docs/index.html",
@@ -8212,7 +8212,7 @@
8212
8212
  {
8213
8213
  "path": "scripts/gds/deploy-divergence.js",
8214
8214
  "mode": "0000644",
8215
- "sha256": "4b77b2c878cace855ad979c66a7bbb0318199d10b1e0f114f79732db2b6cc203"
8215
+ "sha256": "39879d5d26a325529620ab4fd180f133643d0e4c2a5a9ef92f8ee347ad0ba0d7"
8216
8216
  },
8217
8217
  {
8218
8218
  "path": "scripts/gds/design-sync.js",
@@ -8302,7 +8302,7 @@
8302
8302
  {
8303
8303
  "path": "scripts/gds/fetch-sound-prefs.js",
8304
8304
  "mode": "0000644",
8305
- "sha256": "0043a1af935e5dff7d807525eb6426310a8e6517af03f0506a86cb3296d38f74"
8305
+ "sha256": "f2239bdd653ceda6f2595d454e618bff5273f46eb63d3cb6d8040190ee0125e9"
8306
8306
  },
8307
8307
  {
8308
8308
  "path": "scripts/gds/figma-design-sync.js",
@@ -8689,6 +8689,11 @@
8689
8689
  "mode": "0000644",
8690
8690
  "sha256": "9898b6f8e60b9f5ee4aeb9a023d1b68bb5bfbdf589eed10923a8e492716340ed"
8691
8691
  },
8692
+ {
8693
+ "path": "scripts/gds/probe-cache.js",
8694
+ "mode": "0000644",
8695
+ "sha256": "34d891e8ff58f0da957d300021210dbc34448fe676873b6b77e13ad3b7179d2e"
8696
+ },
8692
8697
  {
8693
8698
  "path": "scripts/gds/prove-strict-versioning.js",
8694
8699
  "mode": "0000644",
@@ -9232,7 +9237,7 @@
9232
9237
  {
9233
9238
  "path": "scripts/gds/strand-watch.js",
9234
9239
  "mode": "0000644",
9235
- "sha256": "c7f9c997a6892e6ca083a03789b24524912632731ea550df6ee974f1ef05267e"
9240
+ "sha256": "2a9959bd2e45046dadcda9982998a5b7163cef00b5d719ff299314daa5409770"
9236
9241
  },
9237
9242
  {
9238
9243
  "path": "scripts/gds/stray-unit-sweep.sh",
@@ -9722,7 +9727,7 @@
9722
9727
  {
9723
9728
  "path": "src/module-api.js",
9724
9729
  "mode": "0000644",
9725
- "sha256": "782160b1ae7c1483c92337a589f92d8099a8bbee37005ea744c78bd4fcb5803c"
9730
+ "sha256": "2ac366c05863ab86d0bdb5b3df3bf6f605e4edb0182a977c83c781cf503bf5b7"
9726
9731
  },
9727
9732
  {
9728
9733
  "path": "src/module-loader/catalog.js",
@@ -12344,6 +12349,11 @@
12344
12349
  "mode": "0000644",
12345
12350
  "sha256": "3c85d28ecc55e375988f3e55aee993c8c84220889678262d8207146c2772f3f5"
12346
12351
  },
12352
+ {
12353
+ "path": "tests/probe_cache.mjs",
12354
+ "mode": "0000644",
12355
+ "sha256": "26c734a1ab1996e1aff6197a9e0b19a448b28c772e0e75b00fee2433535c1193"
12356
+ },
12347
12357
  {
12348
12358
  "path": "tests/profile_activity.mjs",
12349
12359
  "mode": "0000644",
@@ -2118,5 +2118,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2118
2118
  landed since 1.19.820 with no explicit bump. run 35212866365. (task 1002620)
2119
2119
  1.19.822 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2120
2120
  landed since 1.19.821 with no explicit bump. run 35213847140. (task 1002620)
2121
+ 1.19.823 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2122
+ landed since 1.19.822 with no explicit bump. run 35218228051. (task 1002620)
2121
2123
  ---------------------------------------------------------------------------
2122
2124
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.822",
3
+ "version": "1.19.823",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.822",
9
+ "version": "1.19.823",
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.822",
3
+ "version": "1.19.823",
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",
@@ -321,7 +321,11 @@ async function main() {
321
321
  if (hasFlag('--line')) {
322
322
  const ms = Number(arg('--timeout-ms')) || DEFAULT_TIMEOUT_MS;
323
323
  try {
324
- const one = line(await check({ timeoutMs: ms }));
324
+ // Shared across concurrent session starts (task 1003689): four sessions opened
325
+ // seconds apart asked this same question four times and all four timed out, which
326
+ // emits NO line — and silence from a truth-teller reads as "nothing to report".
327
+ const { cached } = require('./probe-cache.js');
328
+ const { value: one } = await cached('deploy-divergence-line', async () => line(await check({ timeoutMs: ms })));
325
329
  if (one) console.log(one);
326
330
  } catch { /* offline, no git, unreadable module-api — all silence */ }
327
331
  process.exitCode = 0;
@@ -36,10 +36,16 @@ function writeAtomic(targetPath, contents) {
36
36
  fs.renameSync(tmp, targetPath);
37
37
  }
38
38
 
39
- async function main() {
39
+ // The network half, on its own so probe-cache can own it: cached() records a value
40
+ // only when its producer RESOLVES, so a failed fetch leaves nothing cached and the
41
+ // next session retries instead of inheriting a minute of "already done".
42
+ //
43
+ // Returns { soundData, eventData } or null. Null is an ordinary outcome — offline, a
44
+ // timeout, or both endpoints refusing — and the caller writes nothing, which leaves
45
+ // the prefs file exactly as the last good run left it (fail-open).
46
+ async function fetchPrefs() {
40
47
  const sess = readSessionToken();
41
- if (!sess) return; // not signed in — player fails open
42
-
48
+ if (!sess) return null;
43
49
  const ctrl = new AbortController();
44
50
  const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
45
51
  let soundData = null;
@@ -57,15 +63,44 @@ async function main() {
57
63
  signal: ctrl.signal,
58
64
  }),
59
65
  ]);
60
- if (!soundRes.ok && !eventRes.ok) return;
66
+ if (!soundRes.ok && !eventRes.ok) return null;
61
67
  if (soundRes.ok) soundData = await soundRes.json();
62
68
  if (eventRes.ok) eventData = await eventRes.json();
63
69
  } catch (_) {
64
- return; // network/timeout — no-op
70
+ return null; // network/timeout — no-op
65
71
  } finally {
66
72
  clearTimeout(timer);
67
73
  }
68
74
 
75
+ return { soundData, eventData };
76
+ }
77
+
78
+ async function main() {
79
+ const sess = readSessionToken();
80
+ if (!sess) return; // not signed in — player fails open
81
+
82
+ // SHARED ACROSS CONCURRENT SESSION STARTS (task 1003689). This is the third of the
83
+ // three SessionStart hooks that call the server, and when four sessions open within
84
+ // seconds they contend and every hook in all four hits its timeout. Unlike the
85
+ // truth-tellers, a lost run here is invisible rather than merely silent: the prefs
86
+ // file simply keeps its previous contents.
87
+ //
88
+ // Which is also why skipping the write on a cache hit is correct — the file already
89
+ // holds what the previous run fetched, and these prefs change when a builder edits
90
+ // them in Settings, not between two sessions opened three seconds apart.
91
+ // The cache must be marked fresh only by a SUCCESSFUL fetch. The first cut cached
92
+ // Date.now() BEFORE fetching, so a failed or timed-out fetch still recorded 'done'
93
+ // and blocked every retry for the whole TTL - turning a transient network error into
94
+ // a minute of silently stale prefs. cached() writes its record only when the producer
95
+ // RESOLVES, so the work belongs inside it, the way strand-watch and deploy-divergence
96
+ // already do it.
97
+ const { cached } = require('./probe-cache.js');
98
+ const { value: fetched, from } = await cached('sound-prefs', async () => fetchPrefs());
99
+ if (from !== 'fresh') return; // another session already fetched and wrote the file
100
+ if (!fetched) return; // not signed in, offline, or both requests failed
101
+ const { soundData, eventData } = fetched;
102
+
103
+
69
104
  // effective_map is { <sound>: <bool> } for every known sound (defaults
70
105
  // already applied server-side); volume is the master 0-100 (#788). The local
71
106
  // file shape is { sounds: {...}, volume: N, event_sounds: {...} } — play.js
Binary file
@@ -245,7 +245,14 @@ async function main() {
245
245
  // deploy-divergence's identical comment).
246
246
  if (hasFlag('--line')) {
247
247
  try {
248
- const one = line(await check(opts));
248
+ // SHARED ACROSS CONCURRENT SESSION STARTS (task 1003689). Four sessions opened
249
+ // within three seconds — one worktree per claim, the normal pattern here — used
250
+ // to make this same call four times and ALL of them timed out, which produces no
251
+ // line at all. Silence from a truth-teller is indistinguishable from 'nothing to
252
+ // report', so the brief was lost without a symptom. The first session to ask now
253
+ // answers for the rest.
254
+ const { cached } = require('./probe-cache.js');
255
+ const { value: one } = await cached('strand-watch-line', async () => line(await check(opts)));
249
256
  if (one) console.log(one);
250
257
  } catch { /* no session, offline, unreadable — all silence */ }
251
258
  process.exitCode = 0;
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.822'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.823'; // 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');
@@ -0,0 +1,200 @@
1
+ // tests/probe_cache.mjs — one answer, shared by the sessions that start together
2
+ // (task 1003689).
3
+ //
4
+ // THE MEASUREMENT THIS EXISTS FOR. SessionStart runs six hooks, three of which call the
5
+ // server. Four sessions opened within three seconds — one worktree per claim, the normal
6
+ // pattern here — and every hook in all four hit its 10-15s timeout, while the sessions
7
+ // immediately before and after ran clean at ~5s on identical config.
8
+ //
9
+ // A timed-out SessionStart hook emits NO LINE, so the truth-teller briefs vanish. Silence
10
+ // is indistinguishable from "nothing to report", which is the one thing these probes are
11
+ // built not to be.
12
+ //
13
+ // The cache must therefore fail toward DOING THE WORK, never toward silence: every error
14
+ // path below returns the producer's answer rather than nothing.
15
+
16
+ import { test } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import { execFileSync } from 'node:child_process';
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { createRequire } from 'node:module';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const pc = require('../scripts/gds/probe-cache.js');
27
+
28
+ // fileURLToPath, never URL.pathname (fitness check 30 / task 1002889): a %20 or a
29
+ // Windows drive slash makes require() throw at load, and CI's spaceless checkout
30
+ // cannot see it.
31
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ const fresh = () => `k-${Math.random().toString(36).slice(2)}`;
33
+
34
+ test('the second asker does not repeat the call', async () => {
35
+ const k = fresh();
36
+ let calls = 0;
37
+ const produce = async () => { calls += 1; return `answer-${calls}`; };
38
+ const a = await pc.cached(k, produce, { ttlMs: 5000 });
39
+ const b = await pc.cached(k, produce, { ttlMs: 5000 });
40
+ assert.equal(calls, 1, 'the whole point: four sessions, one question');
41
+ assert.equal(a.from, 'fresh');
42
+ assert.equal(b.from, 'cache');
43
+ assert.equal(b.value, a.value);
44
+ });
45
+
46
+ test('an expired answer is asked again', async () => {
47
+ const k = fresh();
48
+ let calls = 0;
49
+ const produce = async () => { calls += 1; return calls; };
50
+ await pc.cached(k, produce, { ttlMs: 1 });
51
+ await new Promise((r) => setTimeout(r, 10));
52
+ const b = await pc.cached(k, produce, { ttlMs: 1 });
53
+ assert.equal(calls, 2, 'a cache that never expires is a lie, not a cache');
54
+ assert.equal(b.from, 'fresh');
55
+ });
56
+
57
+ test('a record from the FUTURE is treated as stale, not trusted forever', () => {
58
+ // A clock that jumped would otherwise pin a stale answer permanently.
59
+ assert.equal(pc.isFresh({ at: 5_000 }, 1_000, 4_000), false);
60
+ assert.equal(pc.isFresh({ at: 3_500 }, 1_000, 4_000), true);
61
+ assert.equal(pc.isFresh(null, 1_000, 4_000), false);
62
+ assert.equal(pc.isFresh({}, 1_000, 4_000), false);
63
+ });
64
+
65
+ test('two checkouts of the same instance do not read each other\'s answers', async () => {
66
+ const k = fresh();
67
+ const a = await pc.cached(k, async () => 'from-A', { ttlMs: 5000, scope: '/repo/a' });
68
+ const b = await pc.cached(k, async () => 'from-B', { ttlMs: 5000, scope: '/repo/b' });
69
+ assert.equal(a.value, 'from-A');
70
+ assert.equal(b.value, 'from-B', 'the scope is part of the key — a worktree must not inherit another\'s brief');
71
+ });
72
+
73
+ test('a producer that throws is NOT cached, and the error reaches the caller', async () => {
74
+ const k = fresh();
75
+ await assert.rejects(() => pc.cached(k, async () => { throw new Error('probe failed'); }, { ttlMs: 5000 }));
76
+ // and the next asker gets a real attempt, not a cached failure
77
+ const r = await pc.cached(k, async () => 'recovered', { ttlMs: 5000 });
78
+ assert.equal(r.value, 'recovered');
79
+ assert.equal(r.from, 'fresh', 'a failure must not be remembered as an answer');
80
+ });
81
+
82
+ // ---- the real scenario: four processes at once -----------------------------
83
+ //
84
+ // The unit cases above all run in ONE process, where a lock is trivially uncontended.
85
+ // The measured failure is four SEPARATE processes starting within three seconds on a
86
+ // cold cache — and the first cut of this cache did NOT fix it: with nothing cached and
87
+ // the lock held, every waiter fell through and made the call anyway, so all four still
88
+ // ran. This is the case that caught it.
89
+ test('four concurrent PROCESSES on a cold cache produce ONE call, and all four get the answer', async () => {
90
+ const counter = path.join(os.tmpdir(), `bongos-probe-test-${Date.now()}.txt`);
91
+ try { fs.rmSync(counter, { force: true }); } catch { /* not there */ }
92
+ execFileSync(process.execPath, [path.join(REPO_ROOT, 'scripts', 'gds', 'probe-cache.js'), '--clear'], { encoding: 'utf8' });
93
+
94
+ const child = `
95
+ const pc = require(${JSON.stringify(path.join(REPO_ROOT, 'scripts', 'gds', 'probe-cache.js'))});
96
+ const fs = require('fs');
97
+ (async () => {
98
+ const r = await pc.cached('probe-cache-concurrency-test', async () => {
99
+ fs.appendFileSync(${JSON.stringify(counter)}, 'ran\\n');
100
+ await new Promise((res) => setTimeout(res, 400));
101
+ return 'the-answer';
102
+ }, { ttlMs: 30000 });
103
+ process.stdout.write(r.value);
104
+ })();
105
+ `;
106
+
107
+ const { spawn } = await import('node:child_process');
108
+ const results = await Promise.all([0, 1, 2, 3].map(() => new Promise((resolve) => {
109
+ const p = spawn(process.execPath, ['-e', child]);
110
+ let out = '';
111
+ p.stdout.on('data', (d) => { out += d; });
112
+ p.on('close', () => resolve(out.trim()));
113
+ })));
114
+
115
+ const ran = fs.existsSync(counter) ? fs.readFileSync(counter, 'utf8').trim().split('\n').filter(Boolean).length : 0;
116
+ try { fs.rmSync(counter, { force: true }); } catch { /* best effort */ }
117
+
118
+ assert.equal(ran, 1, `the probe must run ONCE across four concurrent starts; it ran ${ran}`);
119
+ assert.deepEqual(results, ['the-answer', 'the-answer', 'the-answer', 'the-answer'],
120
+ 'and every session must still get its brief — trading a timeout for a silence would be no fix at all');
121
+ });
122
+
123
+ // ---- FOUR DIFFERENT WORKTREES (grade round 1) ------------------------------
124
+ //
125
+ // The test above spawns four processes from ONE directory, and it passed while the
126
+ // cache was keyed on process.cwd() — which meant the fix did nothing for the case it
127
+ // was built for. The measured failure is four sessions in FOUR DIFFERENT WORKTREES,
128
+ // one per claim, so four cwds hashed to four keys and nothing was ever shared.
129
+ //
130
+ // A test that does not model the scenario proves the wrong thing. This one runs each
131
+ // child from a different directory of the SAME checkout.
132
+ test('four processes in DIFFERENT worktrees of one checkout still share the answer', async () => {
133
+ const counter = path.join(os.tmpdir(), `bongos-probe-wt-${Date.now()}.txt`);
134
+ try { fs.rmSync(counter, { force: true }); } catch { /* not there */ }
135
+ execFileSync(process.execPath, [path.join(REPO_ROOT, 'scripts', 'gds', 'probe-cache.js'), '--clear'], { encoding: 'utf8' });
136
+
137
+ // Four real directories inside this checkout — different cwds, one git common dir,
138
+ // which is exactly the shape of four claim worktrees.
139
+ const cwds = [REPO_ROOT, path.join(REPO_ROOT, 'scripts'), path.join(REPO_ROOT, 'tests'), path.join(REPO_ROOT, 'docs')]
140
+ .filter((d) => fs.existsSync(d));
141
+ assert.equal(cwds.length, 4, 'need four real directories in this checkout to model four worktrees');
142
+
143
+ const child = `
144
+ const pc = require(${JSON.stringify(path.join(REPO_ROOT, 'scripts', 'gds', 'probe-cache.js'))});
145
+ const fs = require('fs');
146
+ (async () => {
147
+ const r = await pc.cached('probe-cache-worktree-test', async () => {
148
+ fs.appendFileSync(${JSON.stringify(counter)}, 'ran\\n');
149
+ await new Promise((res) => setTimeout(res, 400));
150
+ return 'shared';
151
+ }, { ttlMs: 30000 });
152
+ process.stdout.write(r.value);
153
+ })();
154
+ `;
155
+
156
+ const { spawn } = await import('node:child_process');
157
+ const results = await Promise.all(cwds.map((cwd) => new Promise((resolve) => {
158
+ const p = spawn(process.execPath, ['-e', child], { cwd });
159
+ let out = '';
160
+ p.stdout.on('data', (d) => { out += d; });
161
+ p.on('close', () => resolve(out.trim()));
162
+ })));
163
+
164
+ const ran = fs.existsSync(counter) ? fs.readFileSync(counter, 'utf8').trim().split('\n').filter(Boolean).length : 0;
165
+ try { fs.rmSync(counter, { force: true }); } catch { /* best effort */ }
166
+
167
+ assert.equal(ran, 1, `four DIFFERENT working directories must still share one answer; the probe ran ${ran} times`);
168
+ assert.deepEqual(results, ['shared', 'shared', 'shared', 'shared']);
169
+ });
170
+
171
+ test('the scope is the CHECKOUT, not the directory', () => {
172
+ // Every worktree of one checkout reports the same git common dir; two unrelated
173
+ // checkouts report different ones. That is the distinction that matters, because the
174
+ // probes ask one server about one repo.
175
+ const scope = pc.defaultScope();
176
+ assert.ok(scope && typeof scope === 'string');
177
+ assert.ok(/\.git$|\.git[\\/]/.test(scope) || scope === process.cwd(),
178
+ `expected a git common dir (or the documented cwd fallback), got ${scope}`);
179
+ });
180
+
181
+ test('clear() with no argument actually empties the cache', async () => {
182
+ // It used to take a scope and delete only when that was exactly null — so calling it
183
+ // with its own default deleted nothing while reporting success.
184
+ const k = fresh();
185
+ let calls = 0;
186
+ await pc.cached(k, async () => { calls += 1; return calls; }, { ttlMs: 60_000 });
187
+ pc.clear();
188
+ await pc.cached(k, async () => { calls += 1; return calls; }, { ttlMs: 60_000 });
189
+ assert.equal(calls, 2, 'after clear(), the next asker must do the work again');
190
+ });
191
+
192
+ test('the three SessionStart hooks that call the server all go through the cache', () => {
193
+ // The wiring is the deliverable; the cache alone changes nothing. These are the three
194
+ // that make network calls — the other three SessionStart hooks are local git.
195
+ for (const rel of ['scripts/gds/strand-watch.js', 'scripts/gds/deploy-divergence.js', 'scripts/gds/fetch-sound-prefs.js']) {
196
+ const src = fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8');
197
+ assert.match(src, /require\('\.\/probe-cache\.js'\)/, `${rel} must share its answer with concurrent sessions`);
198
+ assert.match(src, /cached\(/, `${rel} must actually call cached(), not merely import it`);
199
+ }
200
+ });