@mycadet/gateway 0.12.11 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.json +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/actor-token-revocation.test.ts +41 -34
  11. package/src/__tests__/contact-store-mirror-op-missing.test.ts +33 -70
  12. package/src/__tests__/contacts-mirror-op-reporter.test.ts +33 -89
  13. package/src/__tests__/credential-requests.test.ts +4 -4
  14. package/src/__tests__/devices-mine.test.ts +187 -0
  15. package/src/__tests__/edge-auth.test.ts +1 -20
  16. package/src/__tests__/edge-guardian-auth.test.ts +1 -17
  17. package/src/__tests__/feature-flag-resolver.test.ts +2 -30
  18. package/src/__tests__/feature-flag-watcher-callback.test.ts +0 -22
  19. package/src/__tests__/feature-flags-route.test.ts +2 -191
  20. package/src/__tests__/guardian-bootstrap-lookups.test.ts +18 -25
  21. package/src/__tests__/guardian-integrity-reporter.test.ts +51 -82
  22. package/src/__tests__/guardian-integrity.test.ts +16 -20
  23. package/src/__tests__/handle-inbound-trust-verdict.test.ts +1 -14
  24. package/src/__tests__/ipc-feature-flag-routes.test.ts +0 -56
  25. package/src/__tests__/lum-2783-recovery-e2e.test.ts +0 -12
  26. package/src/__tests__/post-assistant-ready-deferred.test.ts +0 -18
  27. package/src/__tests__/refresh-device-binding.test.ts +0 -12
  28. package/src/__tests__/remote-web-pairing-token.test.ts +4 -14
  29. package/src/__tests__/runtime-client.test.ts +7 -7
  30. package/src/__tests__/speech-relay-websocket.test.ts +4 -4
  31. package/src/__tests__/trust-verdict-resolver-guardian-integrity.test.ts +30 -36
  32. package/src/__tests__/twilio-webhooks.test.ts +2 -2
  33. package/src/auth/actor-token-revocation.ts +34 -9
  34. package/src/cli/__tests__/pairing.test.ts +95 -0
  35. package/src/cli/index.ts +1 -1
  36. package/src/cli/pairing.ts +81 -6
  37. package/src/contacts-mirror-op-reporter.ts +19 -36
  38. package/src/db/connection.ts +0 -10
  39. package/src/feature-flag-resolver.ts +2 -7
  40. package/src/feature-flag-watcher.ts +1 -14
  41. package/src/guardian-integrity-reporter.ts +19 -47
  42. package/src/http/middleware/auth.ts +33 -27
  43. package/src/http/routes/auth-token.ts +1 -1
  44. package/src/http/routes/devices-mine.ts +103 -0
  45. package/src/http/routes/feature-flags.ts +1 -8
  46. package/src/http/routes/guardian-pin.ts +2 -11
  47. package/src/http/routes/ipc-runtime-proxy.ts +1 -1
  48. package/src/http/routes/live-voice-websocket.ts +1 -1
  49. package/src/http/routes/runtime-audio-stream.ts +1 -1
  50. package/src/http/routes/runtime-proxy.ts +2 -2
  51. package/src/http/routes/twilio-voice-webhook.test.ts +13 -13
  52. package/src/index.ts +11 -198
  53. package/src/ipc/feature-flag-handlers.ts +2 -9
  54. package/src/remote-web/pairing-challenge-store.ts +15 -0
  55. package/src/risk/command-registry/commands/assistant.ts +0 -24
  56. package/src/risk/command-registry.test.ts +2 -4
  57. package/src/schema.ts +44 -372
  58. package/src/__tests__/auth-fallback-count-tracker.test.ts +0 -92
  59. package/src/__tests__/auth-fallback-reporter.test.ts +0 -101
  60. package/src/__tests__/backup-worker-cloud-mode.test.ts +0 -44
  61. package/src/__tests__/backup-worker-export-timeout.test.ts +0 -164
  62. package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -216
  63. package/src/__tests__/remote-feature-flag-sync.test.ts +0 -961
  64. package/src/auth-fallback-count-tracker.ts +0 -113
  65. package/src/auth-fallback-reporter.ts +0 -127
  66. package/src/backup/backup-key.ts +0 -138
  67. package/src/backup/backup-routes.ts +0 -161
  68. package/src/backup/backup-worker.ts +0 -425
  69. package/src/backup/list-snapshots.ts +0 -97
  70. package/src/backup/local-writer.ts +0 -87
  71. package/src/backup/offsite-writer.ts +0 -182
  72. package/src/backup/paths.ts +0 -113
  73. package/src/backup/stream-crypt.ts +0 -258
  74. package/src/device-id.ts +0 -18
  75. package/src/feature-flag-remote-store.ts +0 -166
  76. package/src/http/routes/migration-proxy.ts +0 -636
  77. package/src/internal-telemetry-client.ts +0 -39
  78. package/src/ipc/debug-export-handlers.test.ts +0 -102
  79. package/src/ipc/debug-export-handlers.ts +0 -81
  80. package/src/remote-feature-flag-sync.ts +0 -440
  81. package/src/watchdog-reporter.ts +0 -110
@@ -11,10 +11,6 @@ import { testSecurityDir } from "./test-preload.js";
11
11
 
12
12
  const protectedDir = testSecurityDir;
13
13
  const featureFlagStorePath = join(protectedDir, "feature-flags.json");
14
- const remoteFeatureFlagStorePath = join(
15
- protectedDir,
16
- "feature-flags-remote.json",
17
- );
18
14
 
19
15
  // Write the test registry to an isolated temp path so we never touch
20
16
  // the committed gateway/src/feature-flag-registry.json file.
@@ -65,7 +61,6 @@ beforeEach(() => {
65
61
  _setRegistryCandidateOverrides([defaultsPath]);
66
62
  resetFeatureFlagDefaultsCache();
67
63
  clearFeatureFlagStoreCache();
68
- clearRemoteFeatureFlagStoreCache();
69
64
  });
70
65
 
71
66
  afterEach(() => {
@@ -79,7 +74,6 @@ afterEach(() => {
79
74
  _setRegistryCandidateOverrides(null);
80
75
  resetFeatureFlagDefaultsCache();
81
76
  clearFeatureFlagStoreCache();
82
- clearRemoteFeatureFlagStoreCache();
83
77
  resetEnvOverridesCache();
84
78
  delete process.env.CADET_FLAG_A2A_CHANNEL;
85
79
  });
@@ -93,8 +87,6 @@ const {
93
87
  } = await import("../feature-flag-defaults.js");
94
88
  const { clearFeatureFlagStoreCache, readPersistedFeatureFlags } =
95
89
  await import("../feature-flag-store.js");
96
- const { clearRemoteFeatureFlagStoreCache, writeRemoteFeatureFlags } =
97
- await import("../feature-flag-remote-store.js");
98
90
  const { resetEnvOverridesCache } =
99
91
  await import("../feature-flag-env-overrides.js");
100
92
 
@@ -283,132 +275,13 @@ describe("GET /v1/feature-flags handler", () => {
283
275
  expect(browserFlag.defaultEnabled).toBe(true);
284
276
  });
285
277
 
286
- test("remote values fill in when no local override exists", async () => {
287
- // Write a remote store with a2a-channel enabled (overriding registry default of false)
288
- writeFileSync(
289
- remoteFeatureFlagStorePath,
290
- JSON.stringify({
291
- version: 1,
292
- values: {
293
- "a2a-channel": true,
294
- },
295
- }),
296
- );
297
- clearRemoteFeatureFlagStoreCache();
298
-
299
- // No local override for a2a-channel
300
- if (existsSync(featureFlagStorePath)) {
301
- rmSync(featureFlagStorePath);
302
- }
303
- clearFeatureFlagStoreCache();
304
-
305
- const handler = createFeatureFlagsGetHandler();
306
- const res = await handler(
307
- new Request("http://gateway.test/v1/feature-flags"),
308
- );
309
-
310
- expect(res.status).toBe(200);
311
- const body = await res.json();
312
-
313
- const a2aFlag = body.flags.find(
314
- (f: { key: string }) => f.key === "a2a-channel",
315
- );
316
- expect(a2aFlag).toBeDefined();
317
- // Remote value (true) overrides registry default (false)
318
- expect(a2aFlag.enabled).toBe(true);
319
- });
320
-
321
- test("local overrides take precedence over remote values", async () => {
322
- // Set remote value to true
323
- writeFileSync(
324
- remoteFeatureFlagStorePath,
325
- JSON.stringify({
326
- version: 1,
327
- values: {
328
- "a2a-channel": true,
329
- },
330
- }),
331
- );
332
- clearRemoteFeatureFlagStoreCache();
333
-
334
- // Set local override to false
335
- writeFileSync(
336
- featureFlagStorePath,
337
- JSON.stringify({
338
- version: 1,
339
- values: {
340
- "a2a-channel": false,
341
- },
342
- }),
343
- );
344
- clearFeatureFlagStoreCache();
345
-
346
- const handler = createFeatureFlagsGetHandler();
347
- const res = await handler(
348
- new Request("http://gateway.test/v1/feature-flags"),
349
- );
350
-
351
- expect(res.status).toBe(200);
352
- const body = await res.json();
353
-
354
- const a2aFlag = body.flags.find(
355
- (f: { key: string }) => f.key === "a2a-channel",
356
- );
357
- expect(a2aFlag).toBeDefined();
358
- // Local override (false) takes precedence over remote (true)
359
- expect(a2aFlag.enabled).toBe(false);
360
- });
361
-
362
- test("reflects updated flags after remote sync writes new values (stale cache regression)", async () => {
363
- // Scenario: the remote poller (RemoteFeatureFlagSync) writes
364
- // a2a-channel: false, the gateway caches it, then a subsequent
365
- // poll writes a2a-channel: true. The GET handler should return
366
- // the updated value because writeRemoteFeatureFlags() updates
367
- // both disk and the in-memory cache.
368
-
369
- // Step 1: First poll writes a2a-channel: false (simulated via
370
- // writeRemoteFeatureFlags, which is what the poller calls internally).
371
- writeRemoteFeatureFlags({ "a2a-channel": false });
372
-
373
- const handler = createFeatureFlagsGetHandler();
374
- const res1 = await handler(
375
- new Request("http://gateway.test/v1/feature-flags"),
376
- );
377
- const body1 = await res1.json();
378
- const a2aFlag1 = body1.flags.find(
379
- (f: { key: string }) => f.key === "a2a-channel",
380
- );
381
- expect(a2aFlag1.enabled).toBe(false);
382
-
383
- // Step 2: Second poll writes a2a-channel: true — the poller
384
- // calls writeRemoteFeatureFlags which updates file + cache.
385
- writeRemoteFeatureFlags({ "a2a-channel": true });
386
-
387
- // Step 3: The GET handler should immediately reflect the update
388
- // without needing a file-watcher round-trip.
389
- const res2 = await handler(
390
- new Request("http://gateway.test/v1/feature-flags"),
391
- );
392
- const body2 = await res2.json();
393
- const a2aFlag2 = body2.flags.find(
394
- (f: { key: string }) => f.key === "a2a-channel",
395
- );
396
- expect(a2aFlag2.enabled).toBe(true);
397
- });
398
-
399
- test("registry default used when neither local nor remote is set", async () => {
278
+ test("registry default used when no local override is set", async () => {
400
279
  // No local override
401
280
  if (existsSync(featureFlagStorePath)) {
402
281
  rmSync(featureFlagStorePath);
403
282
  }
404
283
  clearFeatureFlagStoreCache();
405
284
 
406
- // No remote value (empty remote store)
407
- if (existsSync(remoteFeatureFlagStorePath)) {
408
- rmSync(remoteFeatureFlagStorePath);
409
- }
410
- clearRemoteFeatureFlagStoreCache();
411
-
412
285
  const handler = createFeatureFlagsGetHandler();
413
286
  const res = await handler(
414
287
  new Request("http://gateway.test/v1/feature-flags"),
@@ -417,11 +290,11 @@ describe("GET /v1/feature-flags handler", () => {
417
290
  expect(res.status).toBe(200);
418
291
  const body = await res.json();
419
292
 
420
- // a2a-channel has defaultEnabled: false in registry
421
293
  const a2aFlag = body.flags.find(
422
294
  (f: { key: string }) => f.key === "a2a-channel",
423
295
  );
424
296
  expect(a2aFlag).toBeDefined();
297
+ // Registry default (false) applies with no local override
425
298
  expect(a2aFlag.enabled).toBe(false);
426
299
  expect(a2aFlag.defaultEnabled).toBe(false);
427
300
 
@@ -434,51 +307,9 @@ describe("GET /v1/feature-flags handler", () => {
434
307
  expect(browserFlag.defaultEnabled).toBe(true);
435
308
  });
436
309
 
437
- test("declared flags missing from a remote snapshot use their registry defaults", async () => {
438
- // No local override
439
- if (existsSync(featureFlagStorePath)) {
440
- rmSync(featureFlagStorePath);
441
- }
442
- clearFeatureFlagStoreCache();
443
-
444
- // Remote snapshot exists, but browser is absent as it would be when the
445
- // platform has no LaunchDarkly value for that key.
446
- writeFileSync(
447
- remoteFeatureFlagStorePath,
448
- JSON.stringify({
449
- version: 1,
450
- values: { "a2a-channel": true },
451
- }),
452
- );
453
- clearRemoteFeatureFlagStoreCache();
454
-
455
- const handler = createFeatureFlagsGetHandler();
456
- const res = await handler(
457
- new Request("http://gateway.test/v1/feature-flags"),
458
- );
459
-
460
- expect(res.status).toBe(200);
461
- const body = await res.json();
462
-
463
- const a2aFlag = body.flags.find(
464
- (f: { key: string }) => f.key === "a2a-channel",
465
- );
466
- expect(a2aFlag.enabled).toBe(true);
467
-
468
- const browserFlag = body.flags.find(
469
- (f: { key: string }) => f.key === "browser",
470
- );
471
- expect(browserFlag).toBeDefined();
472
- expect(browserFlag.enabled).toBe(true);
473
- expect(browserFlag.defaultEnabled).toBe(true);
474
- });
475
-
476
310
  test("string flag uses registry default when no override exists", async () => {
477
311
  if (existsSync(featureFlagStorePath)) rmSync(featureFlagStorePath);
478
- if (existsSync(remoteFeatureFlagStorePath))
479
- rmSync(remoteFeatureFlagStorePath);
480
312
  clearFeatureFlagStoreCache();
481
- clearRemoteFeatureFlagStoreCache();
482
313
 
483
314
  const handler = createFeatureFlagsGetHandler();
484
315
  const res = await handler(
@@ -496,26 +327,6 @@ describe("GET /v1/feature-flags handler", () => {
496
327
  expect(modelFlag.defaultEnabled).toBe("claude-sonnet-4-6");
497
328
  });
498
329
 
499
- test("remote string value overrides registry default for string flag", async () => {
500
- writeRemoteFeatureFlags({ "default-model": "gpt-4" });
501
- if (existsSync(featureFlagStorePath)) rmSync(featureFlagStorePath);
502
- clearFeatureFlagStoreCache();
503
-
504
- const handler = createFeatureFlagsGetHandler();
505
- const res = await handler(
506
- new Request("http://gateway.test/v1/feature-flags"),
507
- );
508
-
509
- expect(res.status).toBe(200);
510
- const body = await res.json();
511
-
512
- const modelFlag = body.flags.find(
513
- (f: { key: string }) => f.key === "default-model",
514
- );
515
- expect(modelFlag).toBeDefined();
516
- expect(modelFlag.enabled).toBe("gpt-4");
517
- });
518
-
519
330
  test("env override takes precedence over persisted and default values", async () => {
520
331
  // Persisted value sets a2a-channel to false
521
332
  writeFileSync(
@@ -5,9 +5,9 @@
5
5
  * (via ensureCadetGuardianBinding / bootstrapGuardian) and its divergent
6
6
  * re-mint guard (evidence of a prior guardian refuses the implicit mint).
7
7
  *
8
- * The guardian-integrity reporter is silenced/observed via its test-only
9
- * overrides, and the integrity cache is busted between tests — same seam
10
- * conventions as the guardian-integrity test trio (no mock.module).
8
+ * The guardian-integrity reporter's report fns are observed via a pass-through
9
+ * spy on the module's live ESM bindings, and the integrity cache is busted
10
+ * between tests.
11
11
  *
12
12
  * Guardian rows are seeded ONLY in the gateway DB. By default the assistant
13
13
  * DB proxy is swapped for a backend that throws, proving the lookups never
@@ -25,7 +25,6 @@ import {
25
25
  expect,
26
26
  beforeAll,
27
27
  beforeEach,
28
- afterEach,
29
28
  afterAll,
30
29
  mock,
31
30
  } from "bun:test";
@@ -84,10 +83,7 @@ import {
84
83
  actorTokenRecords,
85
84
  actorRefreshTokenRecords,
86
85
  } from "../db/schema.js";
87
- import {
88
- resetGuardianIntegrityReporterForTesting,
89
- setGuardianIntegrityReporterOverridesForTesting,
90
- } from "../guardian-integrity-reporter.js";
86
+ import * as guardianIntegrityReporter from "../guardian-integrity-reporter.js";
91
87
  import { seedActorToken, seedContact } from "./helpers/contact-fixtures.js";
92
88
 
93
89
  // Initialize signing key so bootstrapGuardian's JWT minting works.
@@ -165,10 +161,22 @@ beforeAll(async () => {
165
161
  await initGatewayDb();
166
162
  });
167
163
 
168
- // Captured guardian-integrity reporter detail payloads (via the log
169
- // override): missing-guardian and mint-refused reports both land here.
164
+ // Captured guardian-integrity reporter detail payloads (via module
165
+ // re-registration — bun's spyOn does not intercept calls made through the
166
+ // SUTs' direct ESM imports): missing-guardian and mint-refused reports both
167
+ // land here.
170
168
  const reportCalls: Record<string, unknown>[] = [];
171
169
 
170
+ mock.module("../guardian-integrity-reporter.js", () => ({
171
+ ...guardianIntegrityReporter,
172
+ reportMissingGuardian: (detail: Record<string, unknown>) => {
173
+ reportCalls.push(detail);
174
+ },
175
+ reportGuardianMintRefused: (detail: Record<string, unknown>) => {
176
+ reportCalls.push(detail);
177
+ },
178
+ }));
179
+
172
180
  beforeEach(() => {
173
181
  const db = getGatewayDb();
174
182
  db.delete(contactChannels).run();
@@ -180,23 +188,8 @@ beforeEach(() => {
180
188
  bustGuardianIntegrityCache();
181
189
  assistantBackend = throwingBackend;
182
190
  reportCalls.length = 0;
183
- resetGuardianIntegrityReporterForTesting();
184
- setGuardianIntegrityReporterOverridesForTesting({
185
- fetchImpl: async () => new Response("{}"),
186
- mintToken: () => "svc-token",
187
- baseUrl: "http://127.0.0.1:7821",
188
- log: {
189
- error: (detail) => {
190
- reportCalls.push(detail);
191
- },
192
- warn: () => {},
193
- },
194
- });
195
191
  });
196
192
 
197
- afterEach(() => {
198
- resetGuardianIntegrityReporterForTesting();
199
- });
200
193
 
201
194
  afterAll(() => {
202
195
  resetGatewayDb();
@@ -1,109 +1,78 @@
1
1
  /**
2
- * Fail-loud missing-guardian reporter (guardian-integrity-reporter.ts):
2
+ * Fail-loud guardian-integrity reporter (guardian-integrity-reporter.ts):
3
3
  *
4
- * - First report emits one error log and one telemetry relay POST with the
5
- * `gateway_guardian_missing` check name and the caller's detail.
6
- * - Subsequent reports inside the hourly window are dropped (rate limit).
7
- * - Relay failures never throw out of the caller.
4
+ * - First report per check emits one error log carrying the caller's detail
5
+ * and the failure copy.
6
+ * - Subsequent reports inside the hourly window are dropped (rate limit);
7
+ * the sibling check still reports (per-check rate limit).
8
8
  *
9
- * Logging is observed through the reporter's test-only `log` override —
10
- * bun's mock.module is process-global and would leak into other test files.
9
+ * Logs are observed by mocking the logger module (a side-effect boundary —
10
+ * the same convention as sleep-wake-detector.test.ts). Per-file CI truth:
11
+ * the mock is process-global, so this file pins nothing else.
11
12
  */
12
- import { beforeEach, afterEach, describe, expect, test } from "bun:test";
13
+ import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
13
14
 
14
- const errorLogs: unknown[] = [];
15
- const warnLogs: unknown[] = [];
15
+ const errorLogs: [Record<string, unknown>, string][] = [];
16
+
17
+ mock.module("../logger.js", () => ({
18
+ getLogger: () => ({
19
+ error: (detail: Record<string, unknown>, msg: string) => {
20
+ errorLogs.push([detail, msg]);
21
+ },
22
+ warn: () => {},
23
+ }),
24
+ }));
16
25
 
17
26
  await import("./test-preload.js");
18
- const {
19
- GUARDIAN_MISSING_CHECK_NAME,
20
- flushGuardianIntegrityReporterForTesting,
21
- reportMissingGuardian,
22
- resetGuardianIntegrityReporterForTesting,
23
- setGuardianIntegrityReporterOverridesForTesting,
24
- } = await import("../guardian-integrity-reporter.js");
27
+ const { reportGuardianMintRefused, reportMissingGuardian } =
28
+ await import("../guardian-integrity-reporter.js");
25
29
 
26
- type FetchCall = { url: string; init: RequestInit };
27
- let fetchCalls: FetchCall[] = [];
28
- let fetchResult: () => Promise<Response> = async () => new Response("{}");
30
+ let fakeNow = 1_000_000_000_000;
29
31
 
30
32
  beforeEach(() => {
31
- fetchCalls = [];
32
- fetchResult = async () => new Response("{}");
33
33
  errorLogs.length = 0;
34
- warnLogs.length = 0;
35
- resetGuardianIntegrityReporterForTesting();
36
- setGuardianIntegrityReporterOverridesForTesting({
37
- fetchImpl: async (url, init) => {
38
- fetchCalls.push({ url: String(url), init: init ?? {} });
39
- return fetchResult();
40
- },
41
- mintToken: () => "svc-token",
42
- baseUrl: "http://127.0.0.1:7821",
43
- log: {
44
- error: (detail, msg) => {
45
- errorLogs.push([detail, msg]);
46
- },
47
- warn: (detail, msg) => {
48
- warnLogs.push([detail, msg]);
49
- },
50
- },
51
- });
52
- });
53
-
54
- afterEach(() => {
55
- resetGuardianIntegrityReporterForTesting();
34
+ fakeNow += 24 * 60 * 60 * 1000; // start each test outside prior windows
35
+ spyOn(Date, "now").mockImplementation(() => fakeNow);
56
36
  });
57
37
 
58
38
  describe("reportMissingGuardian", () => {
59
- test("first report logs an error and relays one watchdog event", async () => {
39
+ test("first report logs one error with the detail + copy", () => {
60
40
  reportMissingGuardian({ has_contacts: true, has_actor_tokens: false });
61
- await flushGuardianIntegrityReporterForTesting();
62
41
 
63
42
  expect(errorLogs.length).toBe(1);
64
- expect(fetchCalls.length).toBe(1);
65
- expect(fetchCalls[0].url).toBe(
66
- "http://127.0.0.1:7821/v1/internal/telemetry/watchdog",
67
- );
68
- const headers = fetchCalls[0].init.headers as Record<string, string>;
69
- expect(headers.Authorization).toBe("Bearer svc-token");
70
- expect(JSON.parse(String(fetchCalls[0].init.body))).toEqual({
71
- check_name: GUARDIAN_MISSING_CHECK_NAME,
72
- detail: { has_contacts: true, has_actor_tokens: false },
43
+ expect(errorLogs[0]![0]).toMatchObject({
44
+ check: "gateway_guardian_missing",
45
+ has_contacts: true,
46
+ has_actor_tokens: false,
73
47
  });
48
+ expect(errorLogs[0]![1]).toContain("until the guardian is re-seeded");
74
49
  });
75
50
 
76
- test("repeat reports inside the hourly window are dropped", async () => {
77
- reportMissingGuardian({ has_contacts: true, has_actor_tokens: false });
78
- reportMissingGuardian({ has_contacts: true, has_actor_tokens: false });
79
- reportMissingGuardian({ has_contacts: true, has_actor_tokens: true });
80
- await flushGuardianIntegrityReporterForTesting();
51
+ test("repeat reports inside the hourly window are dropped; the sibling check still fires", () => {
52
+ reportMissingGuardian({});
53
+ reportMissingGuardian({});
54
+ reportGuardianMintRefused({ integrity_state: "ok" });
81
55
 
82
- expect(errorLogs.length).toBe(1);
83
- expect(fetchCalls.length).toBe(1);
56
+ expect(errorLogs.length).toBe(2);
57
+ expect(errorLogs[0]![0]).toMatchObject({
58
+ check: "gateway_guardian_missing",
59
+ });
60
+ expect(errorLogs[1]![0]).toMatchObject({
61
+ check: "gateway_guardian_mint_refused",
62
+ integrity_state: "ok",
63
+ });
84
64
  });
65
+ });
85
66
 
86
- test("a rejected relay never throws out of the caller", async () => {
87
- fetchResult = async () => {
88
- throw new Error("daemon unreachable");
89
- };
90
-
91
- expect(() =>
92
- reportMissingGuardian({ has_contacts: false, has_actor_tokens: true }),
93
- ).not.toThrow();
94
- await flushGuardianIntegrityReporterForTesting();
67
+ describe("reportGuardianMintRefused", () => {
68
+ test("first report logs one error with the detail + copy", () => {
69
+ reportGuardianMintRefused({ integrity_state: "ok" });
95
70
 
96
71
  expect(errorLogs.length).toBe(1);
97
- expect(warnLogs.length).toBe(1);
98
- });
99
-
100
- test("a non-ok relay response is swallowed with a warning", async () => {
101
- fetchResult = async () => new Response("nope", { status: 500 });
102
-
103
- reportMissingGuardian({ has_contacts: true, has_actor_tokens: true });
104
- await flushGuardianIntegrityReporterForTesting();
105
-
106
- expect(fetchCalls.length).toBe(1);
107
- expect(warnLogs.length).toBe(1);
72
+ expect(errorLogs[0]![0]).toMatchObject({
73
+ check: "gateway_guardian_mint_refused",
74
+ integrity_state: "ok",
75
+ });
76
+ expect(errorLogs[0]![1]).toContain("guardian_repair_required");
108
77
  });
109
78
  });
@@ -12,7 +12,7 @@
12
12
  * files); its logging/relay behavior has its own tests
13
13
  * (guardian-integrity-reporter.test.ts).
14
14
  */
15
- import { beforeEach, afterEach, describe, expect, test } from "bun:test";
15
+ import { beforeEach, afterEach, describe, expect, mock, test } from "bun:test";
16
16
 
17
17
  await import("./test-preload.js");
18
18
  const { initGatewayDb, resetGatewayDb, getGatewayDb } =
@@ -25,10 +25,8 @@ const {
25
25
  } = await import("../db/schema.js");
26
26
  const { seedActorToken, seedContact } =
27
27
  await import("./helpers/contact-fixtures.js");
28
- const {
29
- resetGuardianIntegrityReporterForTesting,
30
- setGuardianIntegrityReporterOverridesForTesting,
31
- } = await import("../guardian-integrity-reporter.js");
28
+ const actualGuardianIntegrityReporter =
29
+ await import("../guardian-integrity-reporter.js");
32
30
  const {
33
31
  bustGuardianIntegrityCache,
34
32
  guardianIntegrityState,
@@ -37,10 +35,21 @@ const {
37
35
  const { applyGuardianBindingGatewayWrites } =
38
36
  await import("../auth/guardian-bootstrap.js");
39
37
 
40
- // The reporter's first-report error log carries the detail payload; capture
41
- // it as the "reported" signal.
38
+ // The reporter's report fns carry the detail payload; capture it by
39
+ // re-registering the reporter module with recorders (bun's spyOn does not
40
+ // intercept calls made through the SUTs' direct ESM imports).
42
41
  const reportCalls: Record<string, unknown>[] = [];
43
42
 
43
+ mock.module("../guardian-integrity-reporter.js", () => ({
44
+ ...actualGuardianIntegrityReporter,
45
+ reportMissingGuardian: (detail: Record<string, unknown>) => {
46
+ reportCalls.push(detail);
47
+ },
48
+ reportGuardianMintRefused: (detail: Record<string, unknown>) => {
49
+ reportCalls.push(detail);
50
+ },
51
+ }));
52
+
44
53
  function insertRefreshToken(): void {
45
54
  const now = Date.now();
46
55
  getGatewayDb()
@@ -71,23 +80,10 @@ beforeEach(async () => {
71
80
  getGatewayDb().delete(actorRefreshTokenRecords).run();
72
81
  bustGuardianIntegrityCache();
73
82
  reportCalls.length = 0;
74
- resetGuardianIntegrityReporterForTesting();
75
- setGuardianIntegrityReporterOverridesForTesting({
76
- fetchImpl: async () => new Response("{}"),
77
- mintToken: () => "svc-token",
78
- baseUrl: "http://127.0.0.1:7821",
79
- log: {
80
- error: (detail) => {
81
- reportCalls.push(detail);
82
- },
83
- warn: () => {},
84
- },
85
- });
86
83
  });
87
84
 
88
85
  afterEach(() => {
89
86
  resetGatewayDb();
90
- resetGuardianIntegrityReporterForTesting();
91
87
  });
92
88
 
93
89
  describe("hasEvidenceOfPriorGuardian", () => {
@@ -85,10 +85,6 @@ const {
85
85
  } = await import("../db/schema.js");
86
86
  const { bustGuardianIntegrityCache } =
87
87
  await import("../auth/guardian-integrity.js");
88
- const {
89
- resetGuardianIntegrityReporterForTesting,
90
- setGuardianIntegrityReporterOverridesForTesting,
91
- } = await import("../guardian-integrity-reporter.js");
92
88
  const { handleInbound } = await import("../handlers/handle-inbound.js");
93
89
 
94
90
  const CHANNEL = "telegram";
@@ -201,22 +197,13 @@ beforeEach(async () => {
201
197
  runtimePayloads = [];
202
198
  forwardToRuntimeMock.mockClear();
203
199
  interceptResult = { intercepted: false };
204
- // The integrity state is TTL-cached process-wide; recompute per test and
205
- // silence the fail-loud reporter (its behavior has its own suites).
200
+ // The integrity state is TTL-cached process-wide; recompute per test.
206
201
  bustGuardianIntegrityCache();
207
- resetGuardianIntegrityReporterForTesting();
208
- setGuardianIntegrityReporterOverridesForTesting({
209
- fetchImpl: async () => new Response("{}"),
210
- mintToken: () => "svc-token",
211
- baseUrl: "http://127.0.0.1:7821",
212
- log: { error: () => {}, warn: () => {} },
213
- });
214
202
  });
215
203
 
216
204
  afterEach(() => {
217
205
  resetAdmissionPolicyCache();
218
206
  resetGatewayDb();
219
- resetGuardianIntegrityReporterForTesting();
220
207
  });
221
208
 
222
209
  describe("handle-inbound trust verdict stamping", () => {