@mycadet/gateway 0.12.10 → 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.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/actor-token-revocation.test.ts +41 -34
- package/src/__tests__/contact-store-mirror-op-missing.test.ts +33 -70
- package/src/__tests__/contacts-mirror-op-reporter.test.ts +33 -89
- package/src/__tests__/credential-requests.test.ts +4 -4
- package/src/__tests__/devices-mine.test.ts +187 -0
- package/src/__tests__/edge-auth.test.ts +1 -20
- package/src/__tests__/edge-guardian-auth.test.ts +1 -17
- package/src/__tests__/feature-flag-resolver.test.ts +2 -30
- package/src/__tests__/feature-flag-watcher-callback.test.ts +0 -22
- package/src/__tests__/feature-flags-route.test.ts +2 -191
- package/src/__tests__/guardian-bootstrap-lookups.test.ts +22 -29
- package/src/__tests__/guardian-init-lockfile.test.ts +2 -2
- package/src/__tests__/guardian-integrity-reporter.test.ts +51 -82
- package/src/__tests__/guardian-integrity.test.ts +16 -20
- package/src/__tests__/handle-inbound-trust-verdict.test.ts +1 -14
- package/src/__tests__/ipc-feature-flag-routes.test.ts +0 -56
- package/src/__tests__/lum-2783-recovery-e2e.test.ts +0 -12
- package/src/__tests__/post-assistant-ready-deferred.test.ts +0 -18
- package/src/__tests__/refresh-device-binding.test.ts +0 -12
- package/src/__tests__/remote-web-pairing-token.test.ts +5 -15
- package/src/__tests__/runtime-client.test.ts +7 -7
- package/src/__tests__/speech-relay-websocket.test.ts +4 -4
- package/src/__tests__/trust-verdict-resolver-guardian-integrity.test.ts +30 -36
- package/src/__tests__/twilio-webhooks.test.ts +2 -2
- package/src/auth/actor-token-revocation.ts +34 -9
- package/src/auth/guardian-bootstrap.ts +1 -1
- package/src/cli/__tests__/pairing.test.ts +95 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/pairing.ts +81 -6
- package/src/contacts-mirror-op-reporter.ts +19 -36
- package/src/db/connection.ts +0 -10
- package/src/feature-flag-resolver.ts +2 -7
- package/src/feature-flag-watcher.ts +1 -14
- package/src/guardian-integrity-reporter.ts +19 -47
- package/src/http/middleware/auth.ts +33 -27
- package/src/http/routes/auth-token.ts +1 -1
- package/src/http/routes/devices-mine.ts +103 -0
- package/src/http/routes/feature-flags.ts +1 -8
- package/src/http/routes/guardian-pin.ts +2 -11
- package/src/http/routes/ipc-runtime-proxy.ts +1 -1
- package/src/http/routes/live-voice-websocket.ts +1 -1
- package/src/http/routes/runtime-audio-stream.ts +1 -1
- package/src/http/routes/runtime-proxy.ts +2 -2
- package/src/http/routes/speech-relay-websocket.ts +2 -2
- package/src/http/routes/twilio-voice-webhook.test.ts +13 -13
- package/src/index.ts +11 -198
- package/src/ipc/feature-flag-handlers.ts +2 -9
- package/src/remote-web/pairing-challenge-store.ts +15 -0
- package/src/risk/command-registry/commands/assistant.ts +0 -24
- package/src/risk/command-registry.test.ts +2 -4
- package/src/schema.ts +44 -372
- package/src/__tests__/auth-fallback-count-tracker.test.ts +0 -92
- package/src/__tests__/auth-fallback-reporter.test.ts +0 -101
- package/src/__tests__/backup-worker-cloud-mode.test.ts +0 -44
- package/src/__tests__/backup-worker-export-timeout.test.ts +0 -164
- package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -216
- package/src/__tests__/remote-feature-flag-sync.test.ts +0 -961
- package/src/auth-fallback-count-tracker.ts +0 -113
- package/src/auth-fallback-reporter.ts +0 -127
- package/src/backup/backup-key.ts +0 -138
- package/src/backup/backup-routes.ts +0 -161
- package/src/backup/backup-worker.ts +0 -425
- package/src/backup/list-snapshots.ts +0 -97
- package/src/backup/local-writer.ts +0 -87
- package/src/backup/offsite-writer.ts +0 -182
- package/src/backup/paths.ts +0 -113
- package/src/backup/stream-crypt.ts +0 -258
- package/src/device-id.ts +0 -18
- package/src/feature-flag-remote-store.ts +0 -166
- package/src/http/routes/migration-proxy.ts +0 -636
- package/src/internal-telemetry-client.ts +0 -39
- package/src/ipc/debug-export-handlers.test.ts +0 -102
- package/src/ipc/debug-export-handlers.ts +0 -81
- package/src/remote-feature-flag-sync.ts +0 -440
- package/src/watchdog-reporter.ts +0 -110
|
@@ -50,7 +50,6 @@ beforeEach(() => {
|
|
|
50
50
|
_setRegistryCandidateOverrides([defaultsPath]);
|
|
51
51
|
resetFeatureFlagDefaultsCache();
|
|
52
52
|
clearFeatureFlagStoreCache();
|
|
53
|
-
clearRemoteFeatureFlagStoreCache();
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
afterEach(() => {
|
|
@@ -63,7 +62,6 @@ afterEach(() => {
|
|
|
63
62
|
_setRegistryCandidateOverrides(null);
|
|
64
63
|
resetFeatureFlagDefaultsCache();
|
|
65
64
|
clearFeatureFlagStoreCache();
|
|
66
|
-
clearRemoteFeatureFlagStoreCache();
|
|
67
65
|
resetEnvOverridesCache();
|
|
68
66
|
delete process.env.CADET_FLAG_BROWSER;
|
|
69
67
|
delete process.env.CADET_FLAG_A2A_CHANNEL;
|
|
@@ -76,8 +74,6 @@ const { resetFeatureFlagDefaultsCache, _setRegistryCandidateOverrides } =
|
|
|
76
74
|
await import("../feature-flag-defaults.js");
|
|
77
75
|
const { clearFeatureFlagStoreCache, writeFeatureFlag } =
|
|
78
76
|
await import("../feature-flag-store.js");
|
|
79
|
-
const { clearRemoteFeatureFlagStoreCache, writeRemoteFeatureFlags } =
|
|
80
|
-
await import("../feature-flag-remote-store.js");
|
|
81
77
|
const { resetEnvOverridesCache } =
|
|
82
78
|
await import("../feature-flag-env-overrides.js");
|
|
83
79
|
|
|
@@ -95,19 +91,8 @@ describe("isFeatureFlagEnabled", () => {
|
|
|
95
91
|
expect(isFeatureFlagEnabled("a2a-channel")).toBe(true);
|
|
96
92
|
});
|
|
97
93
|
|
|
98
|
-
test("
|
|
99
|
-
writeRemoteFeatureFlags({
|
|
100
|
-
browser: false,
|
|
101
|
-
"a2a-channel": true,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
expect(isFeatureFlagEnabled("browser")).toBe(false);
|
|
105
|
-
expect(isFeatureFlagEnabled("a2a-channel")).toBe(true);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("ignores persisted and remote values for undeclared flags", () => {
|
|
94
|
+
test("ignores persisted values for undeclared flags", () => {
|
|
109
95
|
writeFeatureFlag("unknown", true);
|
|
110
|
-
writeRemoteFeatureFlags({ unknown: true });
|
|
111
96
|
|
|
112
97
|
expect(isFeatureFlagEnabled("unknown")).toBe(false);
|
|
113
98
|
});
|
|
@@ -146,19 +131,7 @@ describe("getFeatureFlagValue", () => {
|
|
|
146
131
|
expect(getFeatureFlagValue("default-model")).toBe("gpt-4");
|
|
147
132
|
});
|
|
148
133
|
|
|
149
|
-
test("
|
|
150
|
-
writeRemoteFeatureFlags({ "default-model": "gpt-4" });
|
|
151
|
-
expect(getFeatureFlagValue("default-model")).toBe("gpt-4");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("persisted takes precedence over remote for string flags", () => {
|
|
155
|
-
writeRemoteFeatureFlags({ "default-model": "remote-model" });
|
|
156
|
-
writeFeatureFlag("default-model", "persisted-model");
|
|
157
|
-
expect(getFeatureFlagValue("default-model")).toBe("persisted-model");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("env override wins over persisted, remote, and default values", () => {
|
|
161
|
-
writeRemoteFeatureFlags({ browser: false });
|
|
134
|
+
test("env override wins over persisted and default values", () => {
|
|
162
135
|
writeFeatureFlag("browser", false);
|
|
163
136
|
|
|
164
137
|
process.env.CADET_FLAG_BROWSER = "true";
|
|
@@ -168,7 +141,6 @@ describe("getFeatureFlagValue", () => {
|
|
|
168
141
|
});
|
|
169
142
|
|
|
170
143
|
test("env override wins for string flags", () => {
|
|
171
|
-
writeRemoteFeatureFlags({ "default-model": "remote-model" });
|
|
172
144
|
writeFeatureFlag("default-model", "persisted-model");
|
|
173
145
|
|
|
174
146
|
process.env.CADET_FLAG_DEFAULT_MODEL = "env-model";
|
|
@@ -13,12 +13,6 @@ mock.module("../feature-flag-store.js", () => ({
|
|
|
13
13
|
getFeatureFlagStorePath: () => join(flagDir, "feature-flags.json"),
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
|
-
mock.module("../feature-flag-remote-store.js", () => ({
|
|
17
|
-
refreshRemoteFeatureFlagStoreCache: mock(() => {}),
|
|
18
|
-
getRemoteFeatureFlagStorePath: () =>
|
|
19
|
-
join(flagDir, "remote-feature-flags.json"),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
16
|
const { FeatureFlagWatcher } = await import("../feature-flag-watcher.js");
|
|
23
17
|
|
|
24
18
|
describe("FeatureFlagWatcher onChanged callback", () => {
|
|
@@ -52,22 +46,6 @@ describe("FeatureFlagWatcher onChanged callback", () => {
|
|
|
52
46
|
watcher.stop();
|
|
53
47
|
});
|
|
54
48
|
|
|
55
|
-
test("calls onChanged after debounce fires on remote flag file change", async () => {
|
|
56
|
-
const onChanged = mock(() => {});
|
|
57
|
-
const watcher = new FeatureFlagWatcher({ onChanged });
|
|
58
|
-
watcher.start();
|
|
59
|
-
|
|
60
|
-
writeFileSync(
|
|
61
|
-
join(flagDir, "remote-feature-flags.json"),
|
|
62
|
-
JSON.stringify({ remote: true }),
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
await new Promise((r) => setTimeout(r, 700));
|
|
66
|
-
|
|
67
|
-
expect(onChanged).toHaveBeenCalledTimes(1);
|
|
68
|
-
watcher.stop();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
49
|
test("does not call onChanged when no callback is provided", async () => {
|
|
72
50
|
const watcher = new FeatureFlagWatcher();
|
|
73
51
|
watcher.start();
|
|
@@ -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("
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
169
|
-
//
|
|
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();
|
|
@@ -337,7 +330,7 @@ describe("resolve-or-mint (resolveOrCreateCadetGuardian)", () => {
|
|
|
337
330
|
assistantBackend = makeAssistantBackend();
|
|
338
331
|
|
|
339
332
|
const principalId = await ensureCadetGuardianBinding();
|
|
340
|
-
expect(principalId).toMatch(/^
|
|
333
|
+
expect(principalId).toMatch(/^principal-/);
|
|
341
334
|
|
|
342
335
|
// Gateway now has the row; the assistant DB must not be read again.
|
|
343
336
|
assistantBackend = throwingBackend;
|
|
@@ -350,7 +343,7 @@ describe("resolve-or-mint (resolveOrCreateCadetGuardian)", () => {
|
|
|
350
343
|
|
|
351
344
|
const principalId = await ensureCadetGuardianBinding();
|
|
352
345
|
|
|
353
|
-
expect(principalId).toMatch(/^
|
|
346
|
+
expect(principalId).toMatch(/^principal-/);
|
|
354
347
|
const gwRows = gatewayCadetGuardians();
|
|
355
348
|
expect(gwRows).toHaveLength(1);
|
|
356
349
|
expect(gwRows[0]?.principalId).toBe(principalId);
|
|
@@ -383,7 +376,7 @@ describe("resolve-or-mint (resolveOrCreateCadetGuardian)", () => {
|
|
|
383
376
|
deviceId: "device-1",
|
|
384
377
|
});
|
|
385
378
|
|
|
386
|
-
expect(result.guardianPrincipalId).toMatch(/^
|
|
379
|
+
expect(result.guardianPrincipalId).toMatch(/^principal-/);
|
|
387
380
|
expect(result.isNew).toBe(true);
|
|
388
381
|
expect(result.mintedOverPriorEvidence).toBe(false);
|
|
389
382
|
});
|
|
@@ -606,7 +599,7 @@ describe("resolve-or-mint (resolveOrCreateCadetGuardian)", () => {
|
|
|
606
599
|
deviceId: "device-1",
|
|
607
600
|
});
|
|
608
601
|
|
|
609
|
-
expect(result.guardianPrincipalId).toMatch(/^
|
|
602
|
+
expect(result.guardianPrincipalId).toMatch(/^principal-/);
|
|
610
603
|
expect(result.isNew).toBe(true);
|
|
611
604
|
expect(result.mintedOverPriorEvidence).toBe(true);
|
|
612
605
|
expect(gatewayCadetGuardians()).toHaveLength(1);
|
|
@@ -271,7 +271,7 @@ describe("guardian/init bootstrap secret", () => {
|
|
|
271
271
|
|
|
272
272
|
expect(res.status).toBe(200);
|
|
273
273
|
const body = await res.json();
|
|
274
|
-
expect(body.guardianPrincipalId).toMatch(/^
|
|
274
|
+
expect(body.guardianPrincipalId).toMatch(/^principal-/);
|
|
275
275
|
expect(body.accessToken).toBeTruthy();
|
|
276
276
|
expect(body.refreshToken).toBeTruthy();
|
|
277
277
|
expect(body.isNew).toBe(true);
|
|
@@ -391,7 +391,7 @@ describe("guardian/init one-time-use lockfile", () => {
|
|
|
391
391
|
|
|
392
392
|
expect(res.status).toBe(200);
|
|
393
393
|
const body = await res.json();
|
|
394
|
-
expect(body.guardianPrincipalId).toMatch(/^
|
|
394
|
+
expect(body.guardianPrincipalId).toMatch(/^principal-/);
|
|
395
395
|
expect(body.accessToken).toBeTruthy();
|
|
396
396
|
expect(body.refreshToken).toBeTruthy();
|
|
397
397
|
expect(lockFileExists()).toBe(true);
|
|
@@ -1,109 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fail-loud
|
|
2
|
+
* Fail-loud guardian-integrity reporter (guardian-integrity-reporter.ts):
|
|
3
3
|
*
|
|
4
|
-
* - First report emits one error log
|
|
5
|
-
*
|
|
6
|
-
* - Subsequent reports inside the hourly window are dropped (rate limit)
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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,
|
|
13
|
+
import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
13
14
|
|
|
14
|
-
const errorLogs: unknown[] = [];
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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(
|
|
65
|
-
|
|
66
|
-
|
|
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",
|
|
77
|
-
reportMissingGuardian({
|
|
78
|
-
reportMissingGuardian({
|
|
79
|
-
|
|
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(
|
|
83
|
-
expect(
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
});
|