@vellumai/assistant 0.9.0-dev.202606181735.ae6e1d8 → 0.9.0-dev.202606181858.5b26a8a

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 (80) hide show
  1. package/docs/activation-funnel-telemetry.md +24 -18
  2. package/openapi.yaml +146 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +6 -8
  5. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  6. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  7. package/src/__tests__/channel-inbound-disk-pressure.test.ts +0 -2
  8. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  9. package/src/__tests__/contacts-tools.test.ts +29 -0
  10. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  11. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  12. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  13. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  14. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  15. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  16. package/src/__tests__/invite-redemption-service.test.ts +0 -3
  17. package/src/__tests__/mcp-health-check.test.ts +6 -7
  18. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  19. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  20. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  21. package/src/__tests__/trusted-contact-verification.test.ts +2 -4
  22. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  23. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  24. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  25. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  26. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  27. package/src/config/feature-flag-registry.json +8 -8
  28. package/src/config/schema.ts +6 -6
  29. package/src/contacts/contact-store.ts +2 -10
  30. package/src/contacts/contacts-write.ts +1 -2
  31. package/src/contacts/types.ts +0 -1
  32. package/src/context/token-estimator.ts +1 -1
  33. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  34. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  35. package/src/daemon/handlers/config-a2a.ts +0 -2
  36. package/src/daemon/handlers/config-channels.ts +5 -10
  37. package/src/daemon/lifecycle.ts +23 -12
  38. package/src/daemon/message-types/contacts.ts +0 -1
  39. package/src/events/tool-audit-listener.ts +2 -2
  40. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  41. package/src/memory/auth-fallback-events-store.ts +2 -2
  42. package/src/memory/db-init.ts +2 -0
  43. package/src/memory/lifecycle-events-store.ts +2 -2
  44. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  45. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  46. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  47. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
  48. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  49. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  50. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  51. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
  52. package/src/memory/migrations/index.ts +1 -0
  53. package/src/memory/onboarding-events-store.ts +3 -3
  54. package/src/memory/schema/contacts.ts +0 -1
  55. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  56. package/src/memory/skill-loaded-events-store.ts +2 -2
  57. package/src/memory/tool-executed-events-store.test.ts +7 -7
  58. package/src/notifications/destination-resolver.ts +4 -6
  59. package/src/permissions/checker.ts +22 -7
  60. package/src/permissions/ipc-risk-types.ts +10 -0
  61. package/src/platform/client.test.ts +96 -0
  62. package/src/platform/client.ts +52 -0
  63. package/src/platform/consent-cache.test.ts +159 -0
  64. package/src/platform/consent-cache.ts +107 -0
  65. package/src/prompts/persona-resolver.ts +2 -2
  66. package/src/runtime/access-request-helper.ts +2 -3
  67. package/src/runtime/actor-trust-resolver.ts +1 -2
  68. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  69. package/src/runtime/channel-verification-service.ts +1 -2
  70. package/src/runtime/invite-redemption-service.ts +6 -22
  71. package/src/runtime/routes/contact-routes.ts +31 -8
  72. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  73. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  74. package/src/telemetry/usage-telemetry-reporter.test.ts +40 -25
  75. package/src/telemetry/usage-telemetry-reporter.ts +24 -23
  76. package/src/tools/registry.ts +20 -0
  77. package/src/util/platform.ts +8 -1
  78. package/src/watcher/telemetry.ts +2 -2
  79. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  80. package/src/workspace/migrations/registry.ts +2 -0
@@ -50,8 +50,9 @@ Flow, end to end:
50
50
  `assistant/src/telemetry/activation-funnel.ts`;
51
51
  - emission is best-effort (wrapped in try/catch) and never blocks or alters
52
52
  the surface-action flow;
53
- - `recordActivationEvent` respects the `getConfig().collectUsageData` opt-out
54
- gate (returns `null` / no row when disabled).
53
+ - `recordActivationEvent` respects the platform `share_analytics` consent gate
54
+ via `getCachedShareAnalytics()` (returns `null` / no row when the platform
55
+ user has not consented; default-off when there is no platform session).
55
56
  2. **Reporter flushes** every ~5 min: `usage-telemetry-reporter.ts`
56
57
  (`REPORT_INTERVAL_MS = 5 * 60 * 1000`, with a one-time
57
58
  `INITIAL_FLUSH_DELAY_MS = 30_000` after startup) POSTs unreported onboarding
@@ -172,22 +173,27 @@ When the flag is ON, the web prechat context selects
172
173
  `BOOTSTRAP-ACTIVATION-RAIL.md` as the bootstrap template, and the daemon marks
173
174
  the conversation in `activation_sessions` on first build of the system prompt.
174
175
 
175
- ### 5.2 Confirm usage-data collection is enabled — and do NOT run in dev mode
176
-
177
- Two gates must both be satisfied or no rows reach BigQuery:
178
-
179
- 1. `recordActivationEvent` no-ops when `config.collectUsageData` is false, so the
180
- dev build/config must have usage-data collection enabled, or no rows are
181
- written to SQLite.
182
- 2. **Dev mode disables the flush entirely.** `assistant/src/daemon/lifecycle.ts`
183
- computes the effective setting as `collectUsageData = !isDevMode &&
184
- config.collectUsageData`, so when the daemon runs in dev mode (`VELLUM_DEV=1`)
185
- the `UsageTelemetryReporter` is never started — rows accumulate in SQLite but
186
- are never POSTed, and the "wait/restart for flush" steps below will never
187
- reach BigQuery. For an end-to-end smoke test, run the daemon **outside dev
188
- mode** (so the reporter starts), or explicitly invoke the reporter's
189
- `flush()` via a dev hook. The SQLite rows can still be inspected directly in
190
- dev mode, but the BigQuery verification (§6) requires a real flush.
176
+ ### 5.2 Confirm share_analytics consent is on — and pick the right daemon mode
177
+
178
+ Two distinct concerns: whether record-time rows reach SQLite (consent gate), and
179
+ whether those rows reach BigQuery (flush gate, dev-disabled).
180
+
181
+ 1. `recordActivationEvent` no-ops when the platform `share_analytics` consent is
182
+ off (it reads `getCachedShareAnalytics()`), and the consent is **default-off
183
+ when there is no platform session**. So the dev session must be signed in to
184
+ the platform with `share_analytics` consent enabled, or no rows are written to
185
+ SQLite. The consent cache is refreshed by `startConsentRefresh()` in
186
+ `assistant/src/daemon/lifecycle.ts`, which runs **regardless of dev mode** — so
187
+ a dev session signed in with `share_analytics` consent enabled writes
188
+ record-time rows to SQLite, where they can be inspected directly.
189
+ 2. **Dev mode disables the flush entirely.** When the daemon runs in dev mode
190
+ (`VELLUM_DEV=1`), `assistant/src/daemon/lifecycle.ts` never starts the
191
+ `UsageTelemetryReporter` — rows accumulate in SQLite (consent permitting) but
192
+ are never POSTed, and the "wait/restart for flush" steps below will never reach
193
+ BigQuery. For an end-to-end smoke test, run the daemon **outside dev mode** (so
194
+ the reporter starts), or explicitly invoke the reporter's `flush()` via a dev
195
+ hook. The SQLite rows can be inspected directly in dev mode, but the BigQuery
196
+ verification (§6) requires a real flush.
191
197
 
192
198
  ### 5.3 Start a fresh activation conversation and capture its id
193
199
 
package/openapi.yaml CHANGED
@@ -14569,6 +14569,54 @@ paths:
14569
14569
  responses:
14570
14570
  "200":
14571
14571
  description: Successful response
14572
+ content:
14573
+ application/json:
14574
+ schema:
14575
+ type: object
14576
+ properties:
14577
+ servers:
14578
+ type: array
14579
+ items:
14580
+ type: object
14581
+ properties:
14582
+ id:
14583
+ type: string
14584
+ status:
14585
+ type: string
14586
+ transport:
14587
+ type: object
14588
+ properties:
14589
+ type:
14590
+ type: string
14591
+ enum:
14592
+ - stdio
14593
+ - sse
14594
+ - streamable-http
14595
+ required:
14596
+ - type
14597
+ additionalProperties: {}
14598
+ enabled:
14599
+ type: boolean
14600
+ defaultRiskLevel:
14601
+ type: string
14602
+ allowedTools:
14603
+ type: array
14604
+ items:
14605
+ type: string
14606
+ blockedTools:
14607
+ type: array
14608
+ items:
14609
+ type: string
14610
+ required:
14611
+ - id
14612
+ - status
14613
+ - transport
14614
+ - enabled
14615
+ - defaultRiskLevel
14616
+ additionalProperties: false
14617
+ required:
14618
+ - servers
14619
+ additionalProperties: false
14572
14620
  /v1/internal/mcp/reload:
14573
14621
  post:
14574
14622
  operationId: internal_mcp_reload_post
@@ -14600,6 +14648,104 @@ paths:
14600
14648
  responses:
14601
14649
  "200":
14602
14650
  description: Successful response
14651
+ /v1/internal/mcp/tools-summary:
14652
+ get:
14653
+ operationId: internal_mcp_toolssummary_get
14654
+ summary: Per-server MCP tool counts and token estimates
14655
+ description:
14656
+ Returns registered tool counts, individual tool details, and estimated token overhead for each connected
14657
+ MCP server.
14658
+ tags:
14659
+ - internal
14660
+ responses:
14661
+ "200":
14662
+ description: Successful response
14663
+ content:
14664
+ application/json:
14665
+ schema:
14666
+ type: object
14667
+ properties:
14668
+ servers:
14669
+ type: array
14670
+ items:
14671
+ type: object
14672
+ properties:
14673
+ serverId:
14674
+ type: string
14675
+ toolCount:
14676
+ type: number
14677
+ estimatedTokens:
14678
+ type: number
14679
+ tools:
14680
+ type: array
14681
+ items:
14682
+ type: object
14683
+ properties:
14684
+ name:
14685
+ type: string
14686
+ description:
14687
+ type: string
14688
+ estimatedTokens:
14689
+ type: number
14690
+ required:
14691
+ - name
14692
+ - description
14693
+ - estimatedTokens
14694
+ additionalProperties: false
14695
+ required:
14696
+ - serverId
14697
+ - toolCount
14698
+ - estimatedTokens
14699
+ - tools
14700
+ additionalProperties: false
14701
+ totalToolCount:
14702
+ type: number
14703
+ totalEstimatedTokens:
14704
+ type: number
14705
+ required:
14706
+ - servers
14707
+ - totalToolCount
14708
+ - totalEstimatedTokens
14709
+ additionalProperties: false
14710
+ /v1/internal/mcp/update:
14711
+ post:
14712
+ operationId: internal_mcp_update_post
14713
+ summary: Update an MCP server configuration
14714
+ description: Updates fields on an existing MCP server config entry and triggers a reload.
14715
+ tags:
14716
+ - internal
14717
+ requestBody:
14718
+ required: true
14719
+ content:
14720
+ application/json:
14721
+ schema:
14722
+ type: object
14723
+ properties:
14724
+ name:
14725
+ type: string
14726
+ enabled:
14727
+ type: boolean
14728
+ defaultRiskLevel:
14729
+ type: string
14730
+ maxTools:
14731
+ type: number
14732
+ allowedTools:
14733
+ anyOf:
14734
+ - type: array
14735
+ items:
14736
+ type: string
14737
+ - type: "null"
14738
+ blockedTools:
14739
+ anyOf:
14740
+ - type: array
14741
+ items:
14742
+ type: string
14743
+ - type: "null"
14744
+ required:
14745
+ - name
14746
+ responses:
14747
+ "200":
14748
+ description: Successful response
14603
14749
  /v1/internal/oauth/callback:
14604
14750
  post:
14605
14751
  operationId: internal_oauth_callback_post
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.9.0-dev.202606181735.ae6e1d8",
3
+ "version": "0.9.0-dev.202606181858.5b26a8a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -43,7 +43,6 @@ const PHONE = "+15559871234";
43
43
  function makeContact(
44
44
  role: "guardian" | "contact" = "contact",
45
45
  status: "unverified" | "active" = "unverified",
46
- externalUserId: string | null = null,
47
46
  ): ContactWithChannels {
48
47
  const channelId = "ch-test";
49
48
  return {
@@ -64,7 +63,6 @@ function makeContact(
64
63
  contactId: "contact-test",
65
64
  type: "phone",
66
65
  address: PHONE,
67
- externalUserId,
68
66
  externalChatId: null,
69
67
  isPrimary: true,
70
68
  status,
@@ -94,7 +92,7 @@ describe("resolveActorTrust — address fallback", () => {
94
92
 
95
93
  test("finds unverified channel via address when externalUserId is null", () => {
96
94
  // Simulate a contact registered by name-capture: address set, externalUserId null.
97
- _byAddress = makeContact("contact", "unverified", null);
95
+ _byAddress = makeContact("contact", "unverified");
98
96
 
99
97
  const result = resolveActorTrust({
100
98
  assistantId: "asst-1",
@@ -112,7 +110,7 @@ describe("resolveActorTrust — address fallback", () => {
112
110
  });
113
111
 
114
112
  test("address lookup is the sole member resolution path", () => {
115
- _byAddress = makeContact("contact", "active", null);
113
+ _byAddress = makeContact("contact", "active");
116
114
 
117
115
  const result = resolveActorTrust({
118
116
  assistantId: "asst-1",
@@ -142,7 +140,7 @@ describe("resolveActorTrust — address fallback", () => {
142
140
  test("address-found active channel elevates trust to trusted_contact", () => {
143
141
  // An active channel found via address (e.g. after manual verify without externalUserId set)
144
142
  // should still yield trusted_contact trust class.
145
- _byAddress = makeContact("contact", "active", null);
143
+ _byAddress = makeContact("contact", "active");
146
144
 
147
145
  const result = resolveActorTrust({
148
146
  assistantId: "asst-1",
@@ -159,7 +157,7 @@ describe("resolveActorTrust — address fallback", () => {
159
157
  test("pending-status member is classified as unverified_contact", () => {
160
158
  // Mirrors the unverified branch but for `pending` status (e.g. a phone
161
159
  // contact registered by name-capture awaiting the DTMF challenge).
162
- const contact = makeContact("contact", "unverified", PHONE);
160
+ const contact = makeContact("contact", "unverified");
163
161
  // Override status to "pending" — makeContact only accepts unverified/active
164
162
  contact.channels[0]!.status = "pending";
165
163
  _byAddress = contact;
@@ -178,7 +176,7 @@ describe("resolveActorTrust — address fallback", () => {
178
176
  test("blocked-status member is classified as unknown (not unverified_contact)", () => {
179
177
  // Hard-deny statuses (blocked, revoked) stay `unknown` — admission-layer
180
178
  // re-checks channel.status and emits the hard-deny reasons.
181
- const contact = makeContact("contact", "unverified", PHONE);
179
+ const contact = makeContact("contact", "unverified");
182
180
  contact.channels[0]!.status = "blocked";
183
181
  _byAddress = contact;
184
182
 
@@ -194,7 +192,7 @@ describe("resolveActorTrust — address fallback", () => {
194
192
  });
195
193
 
196
194
  test("revoked-status member is classified as unknown", () => {
197
- const contact = makeContact("contact", "unverified", PHONE);
195
+ const contact = makeContact("contact", "unverified");
198
196
  contact.channels[0]!.status = "revoked";
199
197
  _byAddress = contact;
200
198
 
@@ -7,18 +7,10 @@ mock.module("../util/logger.js", () => ({
7
7
  }),
8
8
  }));
9
9
 
10
- let collectUsageData = true;
10
+ let shareAnalytics = true;
11
11
 
12
- mock.module("../config/loader.js", () => ({
13
- getConfig: () => ({
14
- ui: {},
15
- model: "test",
16
- provider: "test",
17
- memory: { enabled: false },
18
- rateLimit: { maxRequestsPerMinute: 0 },
19
- secretDetection: { enabled: false },
20
- collectUsageData,
21
- }),
12
+ mock.module("../platform/consent-cache.js", () => ({
13
+ getCachedShareAnalytics: () => shareAnalytics,
22
14
  }));
23
15
 
24
16
  import {
@@ -53,7 +45,7 @@ const SAMPLE: AuthFallbackCount[] = [
53
45
 
54
46
  describe("auth-fallback-events-store", () => {
55
47
  beforeEach(() => {
56
- collectUsageData = true;
48
+ shareAnalytics = true;
57
49
  resetTable();
58
50
  });
59
51
 
@@ -78,8 +70,8 @@ describe("auth-fallback-events-store", () => {
78
70
  });
79
71
  });
80
72
 
81
- test("honors the collectUsageData opt-out (records nothing)", () => {
82
- collectUsageData = false;
73
+ test("honors the share_analytics opt-out (records nothing)", () => {
74
+ shareAnalytics = false;
83
75
  const recorded = recordAuthFallbackCounts(1000, 2000, SAMPLE);
84
76
  expect(recorded).toBe(0);
85
77
  expect(queryUnreportedAuthFallbackEvents(0, undefined, 100).length).toBe(0);
@@ -224,14 +224,12 @@ function ensureTestContact(): void {
224
224
  {
225
225
  type: "telegram",
226
226
  address: "telegram-user-default",
227
- externalUserId: "telegram-user-default",
228
227
  status: "active",
229
228
  policy: "allow",
230
229
  },
231
230
  {
232
231
  type: "slack",
233
232
  address: "slack-user-default",
234
- externalUserId: "slack-user-default",
235
233
  status: "active",
236
234
  policy: "allow",
237
235
  },
@@ -2054,7 +2052,6 @@ describe("requester cancel of guardian-gated pending request", () => {
2054
2052
  {
2055
2053
  type: "telegram",
2056
2054
  address: "requester-cancel-user",
2057
- externalUserId: "requester-cancel-user",
2058
2055
  status: "active",
2059
2056
  policy: "allow",
2060
2057
  },
@@ -2981,7 +2978,6 @@ describe("trusted-contact self-approval blocked before guardian approval row exi
2981
2978
  {
2982
2979
  type: "telegram",
2983
2980
  address: "tc-selfapproval-user",
2984
- externalUserId: "tc-selfapproval-user",
2985
2981
  status: "active",
2986
2982
  policy: "allow",
2987
2983
  },
@@ -97,7 +97,6 @@ function seedTrustedContact(policy: "allow" | "escalate" = "allow"): void {
97
97
  {
98
98
  type: "telegram",
99
99
  address: "telegram-user-1",
100
- externalUserId: "telegram-user-1",
101
100
  status: "active",
102
101
  policy,
103
102
  },
@@ -244,7 +243,6 @@ describe("channel inbound disk pressure gate", () => {
244
243
  {
245
244
  type: "slack",
246
245
  address: "slack-user-1",
247
- externalUserId: "slack-user-1",
248
246
  status: "active",
249
247
  policy: "allow",
250
248
  },
@@ -75,7 +75,6 @@ describe("upsertContact user_file selection", () => {
75
75
  {
76
76
  type: "vellum",
77
77
  address: "vellum-principal-abc",
78
- externalUserId: "vellum-principal-abc",
79
78
  },
80
79
  ],
81
80
  });
@@ -91,7 +90,6 @@ describe("upsertContact user_file selection", () => {
91
90
  {
92
91
  type: "slack",
93
92
  address: "u123456",
94
- externalUserId: "U123456",
95
93
  externalChatId: "D987654",
96
94
  },
97
95
  ],
@@ -109,7 +107,6 @@ describe("upsertContact user_file selection", () => {
109
107
  {
110
108
  type: "slack",
111
109
  address: "ualice",
112
- externalUserId: "UALICE",
113
110
  externalChatId: "DALICE",
114
111
  },
115
112
  ],
@@ -125,7 +122,6 @@ describe("upsertContact user_file selection", () => {
125
122
  {
126
123
  type: "slack",
127
124
  address: "ubob1",
128
- externalUserId: "UBOB1",
129
125
  externalChatId: "DBOB1",
130
126
  },
131
127
  ],
@@ -137,7 +133,6 @@ describe("upsertContact user_file selection", () => {
137
133
  {
138
134
  type: "slack",
139
135
  address: "ubob2",
140
- externalUserId: "UBOB2",
141
136
  externalChatId: "DBOB2",
142
137
  },
143
138
  ],
@@ -164,7 +159,6 @@ describe("upsertContact user_file selection", () => {
164
159
  {
165
160
  type: "phone",
166
161
  address: "+15550000",
167
- externalUserId: "+15550000",
168
162
  externalChatId: "+15550000",
169
163
  },
170
164
  ],
@@ -240,6 +240,35 @@ describe("contact_search tool", () => {
240
240
  });
241
241
  });
242
242
 
243
+ // ── search_contacts route (HTTP/IPC compat shim) ─────────────────────
244
+
245
+ describe("search_contacts route", () => {
246
+ beforeEach(clearContacts);
247
+
248
+ test("includes externalUserId (= address) on channels for older clients", () => {
249
+ const seeded = upsertFixture({
250
+ display_name: "Dana",
251
+ channels: [{ type: "slack", address: "U12345ABC" }],
252
+ });
253
+ const seededAddress = seeded.channels[0]!.address;
254
+
255
+ const searchRoute = ROUTES.find(
256
+ (r) => r.operationId === "search_contacts",
257
+ )!;
258
+ const contacts = searchRoute.handler({
259
+ body: { channelAddress: seededAddress },
260
+ }) as unknown as Array<{
261
+ channels: Array<{ address: string; externalUserId?: string }>;
262
+ }>;
263
+
264
+ expect(contacts.length).toBeGreaterThanOrEqual(1);
265
+ const channel = contacts[0]!.channels[0]!;
266
+ // The route re-derives the compat field from address, so SDK/macOS
267
+ // clients that read externalUserId keep working.
268
+ expect(channel.externalUserId).toBe(channel.address);
269
+ });
270
+ });
271
+
243
272
  // ── contact_merge ───────────────────────────────────────────────────
244
273
 
245
274
  describe("contact_merge tool", () => {
@@ -76,7 +76,6 @@ function ensureTestContact(): void {
76
76
  {
77
77
  type: "telegram",
78
78
  address: "telegram-user-default",
79
- externalUserId: "telegram-user-default",
80
79
  status: "active",
81
80
  policy: "allow",
82
81
  },
@@ -10,9 +10,11 @@ mock.module("../util/logger.js", () => ({
10
10
  }),
11
11
  }));
12
12
 
13
- // Usage-data collection is enabled so recordActivationEvent writes rows.
14
- mock.module("../config/loader.js", () => ({
15
- getConfig: () => ({ collectUsageData: true }),
13
+ // Analytics consent is granted so recordActivationEvent writes rows.
14
+ let shareAnalytics = true;
15
+
16
+ mock.module("../platform/consent-cache.js", () => ({
17
+ getCachedShareAnalytics: () => shareAnalytics,
16
18
  }));
17
19
 
18
20
  let broadcastedMessages: ServerMessage[] = [];
@@ -117,6 +119,7 @@ async function showTaggedChoice(
117
119
 
118
120
  describe("activation moment emission from ui_show surface commits", () => {
119
121
  beforeEach(() => {
122
+ shareAnalytics = true;
120
123
  broadcastedMessages = [];
121
124
  resetTables();
122
125
  });
@@ -44,7 +44,7 @@ describe("healGuardianBindingDrift", () => {
44
44
  const guardian = findGuardianForChannel("vellum");
45
45
  expect(guardian).not.toBeNull();
46
46
  expect(guardian!.contact.principalId).toBe("vellum-principal-old-uuid");
47
- expect(guardian!.channel.externalUserId).toBe("vellum-principal-old-uuid");
47
+ expect(guardian!.channel.address).toBe("vellum-principal-old-uuid");
48
48
  });
49
49
 
50
50
  test("no-op when principals already match", () => {
@@ -162,7 +162,6 @@ describe("inbound-message-handler trusted-contact interactivity", () => {
162
162
  {
163
163
  type: "telegram",
164
164
  address: "telegram-user-default",
165
- externalUserId: "telegram-user-default",
166
165
  status: "active",
167
166
  policy: "allow",
168
167
  },
@@ -55,7 +55,6 @@ export function createGuardianBinding(params: {
55
55
  {
56
56
  type: params.channel,
57
57
  address: canonicalId,
58
- externalUserId: canonicalId,
59
58
  externalChatId: params.guardianDeliveryChatId,
60
59
  status: "active",
61
60
  verifiedAt: Date.now(),
@@ -7,23 +7,15 @@ mock.module("../util/logger.js", () => ({
7
7
  }),
8
8
  }));
9
9
 
10
- // Toggle for the collectUsageData opt-out the real store consults. The store
10
+ // Toggle for the share_analytics opt-out the real store consults. The store
11
11
  // module is intentionally NOT mocked here — it has its own DB-backed tests, and
12
12
  // Bun's `mock.module` is process-global, so mocking it would leak into those
13
13
  // tests when files share an invocation. Exercising the real store keeps every
14
14
  // auth-fallback test order-independent.
15
- let collectUsageData = true;
15
+ let shareAnalytics = true;
16
16
 
17
- mock.module("../config/loader.js", () => ({
18
- getConfig: () => ({
19
- ui: {},
20
- model: "test",
21
- provider: "test",
22
- memory: { enabled: false },
23
- rateLimit: { maxRequestsPerMinute: 0 },
24
- secretDetection: { enabled: false },
25
- collectUsageData,
26
- }),
17
+ mock.module("../platform/consent-cache.js", () => ({
18
+ getCachedShareAnalytics: () => shareAnalytics,
27
19
  }));
28
20
 
29
21
  import { queryUnreportedAuthFallbackEvents } from "../memory/auth-fallback-events-store.js";
@@ -61,7 +53,7 @@ const VALID_BODY = {
61
53
 
62
54
  describe("internal-telemetry-routes: auth-fallback", () => {
63
55
  beforeEach(() => {
64
- collectUsageData = true;
56
+ shareAnalytics = true;
65
57
  getDb().delete(authFallbackEvents).run();
66
58
  });
67
59
 
@@ -90,7 +82,7 @@ describe("internal-telemetry-routes: auth-fallback", () => {
90
82
  });
91
83
 
92
84
  test("returns skipped and persists nothing under the opt-out", () => {
93
- collectUsageData = false;
85
+ shareAnalytics = false;
94
86
  expect(call(VALID_BODY)).toEqual({ skipped: true });
95
87
  expect(queryUnreportedAuthFallbackEvents(0, undefined, 100).length).toBe(0);
96
88
  });
@@ -292,7 +292,6 @@ describe("invite-redemption-service", () => {
292
292
  {
293
293
  type: "telegram",
294
294
  address: "guardian-tg-id",
295
- externalUserId: "guardian-tg-id",
296
295
  status: "revoked",
297
296
  },
298
297
  ],
@@ -346,7 +345,6 @@ describe("invite-redemption-service", () => {
346
345
  {
347
346
  type: "telegram",
348
347
  address: "guardian-own-id",
349
- externalUserId: "guardian-own-id",
350
348
  status: "revoked",
351
349
  },
352
350
  ],
@@ -381,7 +379,6 @@ describe("invite-redemption-service", () => {
381
379
  {
382
380
  type: "telegram",
383
381
  address: "guardian-code-id",
384
- externalUserId: "guardian-code-id",
385
382
  status: "revoked",
386
383
  },
387
384
  ],
@@ -75,28 +75,28 @@ describe("checkServerHealth (via internal_mcp_list route)", () => {
75
75
  mockLastError = null;
76
76
  });
77
77
 
78
- test("returns Connected when server connects successfully", async () => {
78
+ test("returns connected when server connects successfully", async () => {
79
79
  mockConnect.mockResolvedValue(undefined);
80
80
  mockDisconnect.mockResolvedValue(undefined);
81
81
 
82
82
  const result = (await listHandler({})) as {
83
83
  servers: { status: string }[];
84
84
  };
85
- expect(result.servers[0].status).toContain("Connected");
85
+ expect(result.servers[0].status).toBe("connected");
86
86
  expect(mockDisconnect).toHaveBeenCalled();
87
87
  });
88
88
 
89
- test("returns Needs authentication when isConnected is false and no lastError", async () => {
89
+ test("returns needs-auth when isConnected is false and no lastError", async () => {
90
90
  mockConnect.mockResolvedValue(undefined);
91
91
  mockIsConnected = false;
92
92
 
93
93
  const result = (await listHandler({})) as {
94
94
  servers: { status: string }[];
95
95
  };
96
- expect(result.servers[0].status).toContain("Needs authentication");
96
+ expect(result.servers[0].status).toBe("needs-auth");
97
97
  });
98
98
 
99
- test("returns Error when connect fails with lastError", async () => {
99
+ test("returns error when connect fails with lastError", async () => {
100
100
  mockConnect.mockResolvedValue(undefined);
101
101
  mockIsConnected = false;
102
102
  mockLastError = new Error("Connection refused");
@@ -105,7 +105,6 @@ describe("checkServerHealth (via internal_mcp_list route)", () => {
105
105
  const result = (await listHandler({})) as {
106
106
  servers: { status: string }[];
107
107
  };
108
- expect(result.servers[0].status).toContain("Error");
109
- expect(result.servers[0].status).toContain("Connection refused");
108
+ expect(result.servers[0].status).toBe("error");
110
109
  });
111
110
  });
@@ -256,7 +256,6 @@ describe("WhatsApp channel ingress attachment resolution", () => {
256
256
  {
257
257
  type: "whatsapp",
258
258
  address: WHATSAPP_USER_ID,
259
- externalUserId: WHATSAPP_USER_ID,
260
259
  status: "active",
261
260
  policy: "allow",
262
261
  },