@vellumai/assistant 0.10.3 → 0.10.4-staging.1

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 (239) hide show
  1. package/openapi.yaml +73 -56
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  4. package/src/__tests__/assistant-stream-state.test.ts +3 -76
  5. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
  6. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  7. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  8. package/src/__tests__/channel-guardian.test.ts +82 -32
  9. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  10. package/src/__tests__/channel-reply-delivery.test.ts +6 -2
  11. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +148 -0
  13. package/src/__tests__/consult-deadline.test.ts +60 -0
  14. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  15. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  16. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  17. package/src/__tests__/contacts-write.test.ts +0 -2
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +98 -7
  20. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  21. package/src/__tests__/conversation-error.test.ts +18 -0
  22. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  23. package/src/__tests__/conversation-title-service.test.ts +222 -201
  24. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  25. package/src/__tests__/delete-propagation.test.ts +5 -3
  26. package/src/__tests__/dm-backfill.test.ts +6 -4
  27. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  28. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  29. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  30. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  31. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  32. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  33. package/src/__tests__/helpers/mock-logger.ts +1 -0
  34. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  35. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  36. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  37. package/src/__tests__/invite-routes-http.test.ts +34 -0
  38. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  39. package/src/__tests__/list-messages-page-latest.test.ts +173 -4
  40. package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
  41. package/src/__tests__/non-member-access-request.test.ts +15 -13
  42. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  43. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  44. package/src/__tests__/persona-resolver.test.ts +75 -45
  45. package/src/__tests__/plugin-bootstrap.test.ts +13 -5
  46. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  47. package/src/__tests__/provider-usage-tracking.test.ts +1 -1
  48. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  49. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  50. package/src/__tests__/reaction-persistence.test.ts +51 -4
  51. package/src/__tests__/relay-server.test.ts +88 -31
  52. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  53. package/src/__tests__/settings-routes.test.ts +32 -0
  54. package/src/__tests__/slack-block-formatting.test.ts +1 -38
  55. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  56. package/src/__tests__/stt-hints.test.ts +6 -3
  57. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  58. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  59. package/src/__tests__/subagent-role-registry.test.ts +17 -4
  60. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  61. package/src/__tests__/subagent-tools.test.ts +398 -3
  62. package/src/__tests__/thread-backfill.test.ts +3 -3
  63. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
  64. package/src/__tests__/tool-start-timestamp.test.ts +4 -3
  65. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  66. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  67. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  68. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  69. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  70. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  71. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  72. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  73. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
  74. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  75. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  76. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  77. package/src/agent/loop-exclusive-tool.test.ts +19 -15
  78. package/src/agent/loop-native-web-search.test.ts +200 -0
  79. package/src/agent/loop.ts +108 -1
  80. package/src/api/responses/conversation-message.ts +9 -0
  81. package/src/approvals/guardian-request-resolvers.ts +16 -4
  82. package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
  83. package/src/calls/guardian-dispatch.ts +14 -11
  84. package/src/calls/inbound-trust-reader.ts +7 -1
  85. package/src/calls/relay-access-wait.ts +6 -6
  86. package/src/calls/relay-server.ts +22 -2
  87. package/src/calls/relay-setup-router.ts +10 -10
  88. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  89. package/src/cli/commands/contacts.ts +10 -7
  90. package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
  91. package/src/cli/commands/memory/worker.ts +97 -30
  92. package/src/cli/commands/plugins.ts +3 -146
  93. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
  94. package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
  95. package/src/cli/lib/publish-plugin.ts +231 -1
  96. package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
  97. package/src/config/bundled-skills/subagent/SKILL.md +16 -1
  98. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  99. package/src/config/call-site-defaults.ts +0 -6
  100. package/src/config/llm-resolver.ts +0 -3
  101. package/src/config/schemas/call-site-catalog.ts +0 -7
  102. package/src/config/schemas/heartbeat.ts +2 -5
  103. package/src/config/schemas/llm.ts +3 -12
  104. package/src/config/schemas/memory-lifecycle.ts +1 -1
  105. package/src/config/seed-inference-profiles.ts +76 -35
  106. package/src/config/sync-gated-profiles.ts +0 -3
  107. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  108. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  109. package/src/contacts/contact-store.ts +27 -237
  110. package/src/contacts/contacts-write.ts +18 -58
  111. package/src/contacts/gateway-channel-read.ts +51 -0
  112. package/src/contacts/member-write-relay.ts +25 -31
  113. package/src/contacts/types.ts +3 -15
  114. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  115. package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
  116. package/src/daemon/conversation-agent-loop.ts +68 -61
  117. package/src/daemon/conversation-error.ts +7 -10
  118. package/src/daemon/conversation-tool-setup.ts +0 -10
  119. package/src/daemon/conversation.ts +10 -0
  120. package/src/daemon/external-plugins-bootstrap.ts +8 -2
  121. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  122. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  123. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  124. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  125. package/src/daemon/handlers/config-channels.ts +14 -29
  126. package/src/daemon/lifecycle.ts +16 -4
  127. package/src/daemon/message-types/surfaces.ts +2 -0
  128. package/src/heartbeat/heartbeat-service.ts +5 -0
  129. package/src/home/relationship-state-writer.ts +5 -0
  130. package/src/memory/__tests__/embedding-cache.test.ts +136 -0
  131. package/src/memory/compaction-ledger-store.ts +107 -0
  132. package/src/memory/conversation-crud.ts +136 -61
  133. package/src/memory/conversation-title-service.ts +173 -24
  134. package/src/memory/embedding-backend.ts +8 -1
  135. package/src/memory/embedding-cache.ts +139 -0
  136. package/src/memory/jobs-worker.ts +75 -29
  137. package/src/memory/memory-retrospective-job.ts +5 -0
  138. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
  139. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  140. package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
  141. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
  142. package/src/memory/schema/contacts.ts +6 -2
  143. package/src/memory/schema/conversations.ts +39 -0
  144. package/src/memory/steps.ts +1090 -367
  145. package/src/memory/worker-control.ts +104 -18
  146. package/src/memory/worker-process.ts +17 -0
  147. package/src/messaging/channel-binding-metadata.ts +31 -0
  148. package/src/messaging/channel-binding-schema.ts +51 -0
  149. package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
  150. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
  151. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
  152. package/src/messaging/providers/a2a/deliver.ts +5 -1
  153. package/src/messaging/providers/a2a/transport.ts +10 -0
  154. package/src/messaging/providers/callback-routing.ts +48 -0
  155. package/src/messaging/providers/channel-transport.ts +55 -0
  156. package/src/messaging/providers/index.ts +65 -241
  157. package/src/messaging/providers/slack/binding-metadata.ts +62 -0
  158. package/src/messaging/providers/slack/transport.ts +92 -0
  159. package/src/messaging/providers/telegram-bot/transport.ts +51 -0
  160. package/src/messaging/providers/whatsapp/transport.ts +38 -0
  161. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  162. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  163. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  164. package/src/notifications/destination-resolver.ts +7 -23
  165. package/src/notifications/emit-signal.ts +5 -11
  166. package/src/plugins/defaults/index.ts +0 -35
  167. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
  168. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
  169. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
  170. package/src/plugins/disabled-state.ts +31 -0
  171. package/src/plugins/registry.ts +55 -12
  172. package/src/prompts/persona-resolver.ts +43 -11
  173. package/src/providers/call-site-routing.ts +41 -0
  174. package/src/providers/provider-send-message.ts +6 -0
  175. package/src/providers/ratelimit.ts +6 -0
  176. package/src/providers/registry.ts +1 -1
  177. package/src/providers/retry.ts +6 -0
  178. package/src/providers/types.ts +13 -0
  179. package/src/providers/usage-tracking.ts +6 -0
  180. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  181. package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
  182. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  183. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
  184. package/src/runtime/access-request-helper.ts +1 -2
  185. package/src/runtime/actor-trust-resolver.ts +44 -17
  186. package/src/runtime/anchored-guardian.test.ts +7 -54
  187. package/src/runtime/anchored-guardian.ts +4 -53
  188. package/src/runtime/assistant-stream-state.ts +12 -74
  189. package/src/runtime/channel-reply-delivery.ts +3 -8
  190. package/src/runtime/guardian-vellum-migration.ts +18 -16
  191. package/src/runtime/invite-redemption-service.ts +25 -10
  192. package/src/runtime/local-actor-identity.test.ts +108 -0
  193. package/src/runtime/local-actor-identity.ts +27 -20
  194. package/src/runtime/member-verdict-cache.ts +0 -0
  195. package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
  196. package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
  197. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  198. package/src/runtime/routes/contact-routes.ts +40 -25
  199. package/src/runtime/routes/conversation-list-routes.ts +1 -29
  200. package/src/runtime/routes/conversation-routes.ts +27 -7
  201. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  202. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  203. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  204. package/src/runtime/routes/settings-routes.ts +8 -3
  205. package/src/runtime/services/conversation-serializer.ts +6 -49
  206. package/src/runtime/slack-block-formatting.ts +0 -15
  207. package/src/runtime/trust-verdict-consumer.ts +36 -41
  208. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  209. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  210. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
  211. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  212. package/src/subagent/index.ts +1 -1
  213. package/src/subagent/manager.ts +245 -33
  214. package/src/subagent/types.ts +8 -1
  215. package/src/tools/registry.ts +10 -3
  216. package/src/tools/subagent/consult-deadline.ts +49 -0
  217. package/src/tools/subagent/spawn.ts +234 -5
  218. package/src/util/logger.ts +9 -0
  219. package/src/util/platform.ts +14 -0
  220. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  221. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  222. package/src/workspace/migrations/registry.ts +2 -0
  223. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  224. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  225. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  226. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
  227. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  228. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  229. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  230. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  231. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  232. package/src/plugins/defaults/advisor/config.ts +0 -21
  233. package/src/plugins/defaults/advisor/consult.ts +0 -197
  234. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  235. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  236. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  237. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  238. package/src/plugins/defaults/advisor/package.json +0 -14
  239. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
@@ -1,126 +1,104 @@
1
1
  /**
2
2
  * Tests for workspace migration `031-drop-user-md`.
3
3
  *
4
- * Validates the five behavioral contracts spelled out in the plan:
5
- * 1. Fresh install (no guardian) no-op.
6
- * 2. Pre-017 customized USER.md, guardian has no userFile — backfill slug,
7
- * copy content into users/<slug>.md, delete USER.md.
8
- * 3. Post-017 state (users/<slug>.md already populated, USER.md still on disk
9
- * as template) — migration does NOT overwrite users/<slug>.md, deletes USER.md.
10
- * 4. Idempotent re-run — running twice has no additional effect.
11
- * 5. Guardian with missing users/ directory — migration creates the directory.
4
+ * The migration resolves the guardian contact via a frozen raw-SQL read of
5
+ * the local `contacts`/`contact_channels` tables, backfills a `user_file`
6
+ * slug when missing, migrates any customized `USER.md` into
7
+ * `users/<slug>.md`, and deletes the legacy root `USER.md`.
12
8
  *
13
- * The migration imports `findGuardianForChannel`, `listGuardianChannels`,
14
- * and `generateUserFileSlug` from `contacts/contact-store.js`, and calls
15
- * `getDb()` to persist backfilled slugs. These tests stub the contact
16
- * store and DB layer so no real DB is exercised.
9
+ * These tests seed the guardian directly in the local DB so the migration
10
+ * is exercised end-to-end against real SQLite — no gateway or mocks.
17
11
  */
18
12
 
19
13
  import {
20
14
  existsSync,
21
15
  mkdirSync,
22
- mkdtempSync,
23
16
  readFileSync,
24
17
  rmSync,
25
18
  writeFileSync,
26
19
  } from "node:fs";
27
- import { tmpdir } from "node:os";
28
20
  import { join } from "node:path";
29
- import {
30
- afterAll,
31
- afterEach,
32
- beforeAll,
33
- beforeEach,
34
- describe,
35
- expect,
36
- mock,
37
- test,
38
- } from "bun:test";
39
-
40
- // ── Mock state ────────────────────────────────────────────────────
41
-
42
- interface MockContact {
21
+ import { beforeEach, describe, expect, test } from "bun:test";
22
+
23
+ import { getSqlite } from "../memory/db-connection.js";
24
+ import { initializeDb } from "../memory/db-init.js";
25
+
26
+ await initializeDb();
27
+
28
+ import { dropUserMdMigration } from "../workspace/migrations/031-drop-user-md.js";
29
+
30
+ // ── DB seeding helpers ────────────────────────────────────────────
31
+
32
+ function resetContactTables(): void {
33
+ const sqlite = getSqlite();
34
+ sqlite.run("DELETE FROM contact_channels");
35
+ sqlite.run("DELETE FROM contacts");
36
+ }
37
+
38
+ /**
39
+ * Insert a guardian contact plus an active channel so the migration's
40
+ * raw-SQL guardian read resolves it.
41
+ */
42
+ function seedGuardian(input: {
43
43
  id: string;
44
44
  displayName: string;
45
45
  userFile: string | null;
46
+ channelType?: string;
47
+ address?: string;
48
+ verifiedAt?: number;
49
+ }): void {
50
+ const now = Date.now();
51
+ const sqlite = getSqlite();
52
+ sqlite.run(
53
+ `INSERT INTO contacts (id, display_name, created_at, updated_at, role, user_file, contact_type)
54
+ VALUES (?, ?, ?, ?, 'guardian', ?, 'human')`,
55
+ [input.id, input.displayName, now, now, input.userFile],
56
+ );
57
+ sqlite.run(
58
+ `INSERT INTO contact_channels (id, contact_id, type, address, status, verified_at, created_at)
59
+ VALUES (?, ?, ?, ?, 'active', ?, ?)`,
60
+ [
61
+ `${input.id}-ch`,
62
+ input.id,
63
+ input.channelType ?? "vellum",
64
+ input.address ?? "vellum:self",
65
+ input.verifiedAt ?? now,
66
+ now,
67
+ ],
68
+ );
46
69
  }
47
70
 
48
- let mockVellumGuardian: {
49
- contact: MockContact;
50
- channel: Record<string, unknown>;
51
- } | null = null;
52
- let mockAnyGuardian: {
53
- contact: MockContact;
54
- channels: Record<string, unknown>[];
55
- } | null = null;
56
- let mockSlugOverride: ((displayName: string) => string) | null = null;
57
-
58
- // Records drizzle `.update(contacts).set({userFile: ...}).where(...).run()` calls.
59
- let updatedUserFiles: Array<{ contactId: string; userFile: string }> = [];
60
-
61
- // ── Mock modules (must precede migration import) ──────────────────
62
-
63
- mock.module("../contacts/contact-store.js", () => ({
64
- findGuardianForChannel: (channelType: string) =>
65
- channelType === "vellum" ? mockVellumGuardian : null,
66
- listGuardianChannels: () => mockAnyGuardian,
67
- generateUserFileSlug: (displayName: string) => {
68
- if (mockSlugOverride) return mockSlugOverride(displayName);
69
- const base =
70
- displayName
71
- .toLowerCase()
72
- .replace(/[^a-z0-9]+/g, "-")
73
- .replace(/^-+|-+$/g, "") || "user";
74
- return `${base}.md`;
75
- },
76
- }));
77
-
78
- // Minimal drizzle-compatible stub for the single `update` call in the
79
- // migration. The migration builds:
80
- // db.update(contacts).set({ userFile: slug }).where(eq(contacts.id, guardian.id)).run();
81
- // The stub captures the payload into `updatedUserFiles` and also mutates
82
- // the active mock guardian in place so downstream reads observe the new slug.
83
- mock.module("../memory/db-connection.js", () => ({
84
- getDb: () => ({
85
- update: () => ({
86
- set: (values: { userFile: string }) => ({
87
- where: () => ({
88
- run: () => {
89
- const guardian =
90
- mockVellumGuardian?.contact ?? mockAnyGuardian?.contact ?? null;
91
- if (guardian) {
92
- guardian.userFile = values.userFile;
93
- updatedUserFiles.push({
94
- contactId: guardian.id,
95
- userFile: values.userFile,
96
- });
97
- }
98
- },
99
- }),
100
- }),
101
- }),
102
- }),
103
- }));
104
-
105
- // drizzle-orm's `eq()` is invoked by the migration; stub it out so we
106
- // don't need the real module loaded for unit tests.
107
- mock.module("drizzle-orm", () => ({
108
- eq: (_col: unknown, value: unknown) => ({ col: _col, value }),
109
- }));
110
-
111
- // Stub the schema import so drizzle operand construction doesn't touch
112
- // the real sqlite schema (which pulls in the DB).
113
- mock.module("../memory/schema/contacts.js", () => ({
114
- contacts: { id: "id", userFile: "userFile" },
115
- }));
116
-
117
- // Import AFTER mocks so the migration binds to the stubs above.
118
- import { dropUserMdMigration } from "../workspace/migrations/031-drop-user-md.js";
71
+ function guardianUserFile(id: string): string | null {
72
+ const row = getSqlite()
73
+ .query<{ user_file: string | null }, [string]>(
74
+ `SELECT user_file FROM contacts WHERE id = ?`,
75
+ )
76
+ .get(id);
77
+ return row?.user_file ?? null;
78
+ }
119
79
 
120
80
  // ── Test workspace scaffold ───────────────────────────────────────
121
81
 
122
- let testRoot: string;
123
- let workspaceDir: string;
82
+ function workspaceDir(): string {
83
+ const dir = process.env.VELLUM_WORKSPACE_DIR;
84
+ if (!dir) {
85
+ throw new Error(
86
+ "VELLUM_WORKSPACE_DIR should be set by the test preload — aborting",
87
+ );
88
+ }
89
+ return dir;
90
+ }
91
+
92
+ function userMdPath(): string {
93
+ return join(workspaceDir(), "USER.md");
94
+ }
95
+
96
+ function cleanupWorkspaceFiles(): void {
97
+ const dir = workspaceDir();
98
+ for (const p of [join(dir, "USER.md"), join(dir, "users")]) {
99
+ rmSync(p, { recursive: true, force: true });
100
+ }
101
+ }
124
102
 
125
103
  function templateContent(): string {
126
104
  return `_ Lines starting with _ are comments - they won't appear in the system prompt
@@ -151,24 +129,9 @@ function customizedContent(): string {
151
129
  `;
152
130
  }
153
131
 
154
- beforeAll(() => {
155
- testRoot = mkdtempSync(join(tmpdir(), "drop-user-md-test-"));
156
- });
157
-
158
- afterAll(() => {
159
- rmSync(testRoot, { recursive: true, force: true });
160
- });
161
-
162
132
  beforeEach(() => {
163
- workspaceDir = mkdtempSync(join(testRoot, "ws-"));
164
- mockVellumGuardian = null;
165
- mockAnyGuardian = null;
166
- mockSlugOverride = null;
167
- updatedUserFiles = [];
168
- });
169
-
170
- afterEach(() => {
171
- rmSync(workspaceDir, { recursive: true, force: true });
133
+ resetContactTables();
134
+ cleanupWorkspaceFiles();
172
135
  });
173
136
 
174
137
  // ── Tests ─────────────────────────────────────────────────────────
@@ -181,188 +144,183 @@ describe("workspace migration 031-drop-user-md", () => {
181
144
  );
182
145
  });
183
146
 
147
+ test("does not declare retryFailedCheckpoint (no gateway coupling)", () => {
148
+ expect(dropUserMdMigration.retryFailedCheckpoint).toBeUndefined();
149
+ });
150
+
184
151
  test("fresh install (no guardian, no USER.md) is a no-op", () => {
185
- // No guardian stubbed in, no USER.md on disk.
186
- dropUserMdMigration.run(workspaceDir);
152
+ dropUserMdMigration.run(workspaceDir());
153
+
154
+ expect(existsSync(userMdPath())).toBe(false);
155
+ expect(existsSync(join(workspaceDir(), "users"))).toBe(false);
156
+ });
157
+
158
+ test("no guardian with unmodified-template USER.md — deletes it", () => {
159
+ writeFileSync(userMdPath(), templateContent(), "utf-8");
160
+
161
+ dropUserMdMigration.run(workspaceDir());
187
162
 
188
- expect(existsSync(join(workspaceDir, "USER.md"))).toBe(false);
189
- expect(existsSync(join(workspaceDir, "users"))).toBe(false);
190
- expect(updatedUserFiles).toEqual([]);
163
+ expect(existsSync(userMdPath())).toBe(false);
164
+ });
165
+
166
+ test("no guardian with customized USER.md — preserves it (mirror may be stale)", () => {
167
+ const content = customizedContent();
168
+ writeFileSync(userMdPath(), content, "utf-8");
169
+
170
+ dropUserMdMigration.run(workspaceDir());
171
+
172
+ // The local guardian mirror can be stale, so a customized profile must
173
+ // not be destroyed when no guardian row resolves.
174
+ expect(existsSync(userMdPath())).toBe(true);
175
+ expect(readFileSync(userMdPath(), "utf-8")).toBe(content);
191
176
  });
192
177
 
193
178
  test("pre-017 customized USER.md with guardian missing userFile backfills slug and migrates content", () => {
194
- // Guardian exists on the 'vellum' channel but has no userFile.
195
- mockVellumGuardian = {
196
- contact: {
197
- id: "guardian-1",
198
- displayName: "Chris",
199
- userFile: null,
200
- },
201
- channel: { type: "vellum" },
202
- };
203
-
204
- const userMdPath = join(workspaceDir, "USER.md");
179
+ seedGuardian({ id: "guardian-1", displayName: "Chris", userFile: null });
180
+
205
181
  const content = customizedContent();
206
- writeFileSync(userMdPath, content, "utf-8");
182
+ writeFileSync(userMdPath(), content, "utf-8");
207
183
 
208
- dropUserMdMigration.run(workspaceDir);
184
+ dropUserMdMigration.run(workspaceDir());
209
185
 
210
- // Backfill happened: drizzle update was called with the generated slug.
211
- expect(updatedUserFiles).toHaveLength(1);
212
- expect(updatedUserFiles[0].contactId).toBe("guardian-1");
213
- expect(updatedUserFiles[0].userFile).toBe("chris.md");
186
+ // Backfill happened: the contact's user_file is now the generated slug.
187
+ expect(guardianUserFile("guardian-1")).toBe("chris.md");
214
188
 
215
189
  // Content was migrated into users/chris.md.
216
- const destPath = join(workspaceDir, "users", "chris.md");
190
+ const destPath = join(workspaceDir(), "users", "chris.md");
217
191
  expect(existsSync(destPath)).toBe(true);
218
192
  expect(readFileSync(destPath, "utf-8")).toBe(content);
219
193
 
220
194
  // Legacy USER.md was deleted.
221
- expect(existsSync(userMdPath)).toBe(false);
195
+ expect(existsSync(userMdPath())).toBe(false);
222
196
  });
223
197
 
224
198
  test("post-017 users/<slug>.md already populated, USER.md still on disk as template — does not overwrite dest, deletes USER.md", () => {
225
- // Guardian already has a userFile from a prior 017 run.
226
- mockVellumGuardian = {
227
- contact: {
228
- id: "guardian-2",
229
- displayName: "Chris",
230
- userFile: "chris.md",
231
- },
232
- channel: { type: "vellum" },
233
- };
234
-
235
- // Pre-populated persona file (post-017 state).
236
- const usersDir = join(workspaceDir, "users");
199
+ seedGuardian({
200
+ id: "guardian-2",
201
+ displayName: "Chris",
202
+ userFile: "chris.md",
203
+ });
204
+
205
+ const usersDir = join(workspaceDir(), "users");
237
206
  mkdirSync(usersDir, { recursive: true });
238
207
  const destPath = join(usersDir, "chris.md");
239
208
  const existingPersona = "# Chris's Profile\n\n- Loves kayaking\n";
240
209
  writeFileSync(destPath, existingPersona, "utf-8");
241
210
 
242
- // Leftover template-shape USER.md at workspace root.
243
- const userMdPath = join(workspaceDir, "USER.md");
244
- writeFileSync(userMdPath, templateContent(), "utf-8");
211
+ writeFileSync(userMdPath(), templateContent(), "utf-8");
245
212
 
246
- dropUserMdMigration.run(workspaceDir);
213
+ dropUserMdMigration.run(workspaceDir());
247
214
 
248
215
  // users/chris.md is untouched.
249
216
  expect(readFileSync(destPath, "utf-8")).toBe(existingPersona);
250
-
251
217
  // USER.md is gone.
252
- expect(existsSync(userMdPath)).toBe(false);
253
-
218
+ expect(existsSync(userMdPath())).toBe(false);
254
219
  // No slug backfill necessary.
255
- expect(updatedUserFiles).toEqual([]);
220
+ expect(guardianUserFile("guardian-2")).toBe("chris.md");
256
221
  });
257
222
 
258
223
  test("idempotent: second run is a no-op after the first run deleted USER.md", () => {
259
- mockVellumGuardian = {
260
- contact: {
261
- id: "guardian-3",
262
- displayName: "Alice",
263
- userFile: "alice.md",
264
- },
265
- channel: { type: "vellum" },
266
- };
267
-
268
- const userMdPath = join(workspaceDir, "USER.md");
269
- writeFileSync(userMdPath, customizedContent(), "utf-8");
270
-
271
- // First run: migrates content and deletes USER.md.
272
- dropUserMdMigration.run(workspaceDir);
273
- expect(existsSync(userMdPath)).toBe(false);
274
- const destPath = join(workspaceDir, "users", "alice.md");
224
+ seedGuardian({
225
+ id: "guardian-3",
226
+ displayName: "Alice",
227
+ userFile: "alice.md",
228
+ });
229
+
230
+ writeFileSync(userMdPath(), customizedContent(), "utf-8");
231
+
232
+ dropUserMdMigration.run(workspaceDir());
233
+ expect(existsSync(userMdPath())).toBe(false);
234
+ const destPath = join(workspaceDir(), "users", "alice.md");
275
235
  expect(existsSync(destPath)).toBe(true);
276
236
  const afterFirst = readFileSync(destPath, "utf-8");
277
237
 
278
- // Second run: no USER.md remains, users/alice.md already has content,
279
- // so the destination is not rewritten and USER.md is still absent.
280
- dropUserMdMigration.run(workspaceDir);
281
- expect(existsSync(userMdPath)).toBe(false);
238
+ dropUserMdMigration.run(workspaceDir());
239
+ expect(existsSync(userMdPath())).toBe(false);
282
240
  expect(existsSync(destPath)).toBe(true);
283
241
  expect(readFileSync(destPath, "utf-8")).toBe(afterFirst);
284
242
  });
285
243
 
286
244
  test("guardian exists but users/ directory is missing — migration creates the directory", () => {
287
- mockVellumGuardian = {
288
- contact: {
289
- id: "guardian-4",
290
- displayName: "Bob",
291
- userFile: "bob.md",
292
- },
293
- channel: { type: "vellum" },
294
- };
295
-
296
- // USER.md present but no users/ dir yet.
297
- const userMdPath = join(workspaceDir, "USER.md");
298
- writeFileSync(userMdPath, customizedContent(), "utf-8");
299
- expect(existsSync(join(workspaceDir, "users"))).toBe(false);
300
-
301
- dropUserMdMigration.run(workspaceDir);
302
-
303
- expect(existsSync(join(workspaceDir, "users"))).toBe(true);
304
- const destPath = join(workspaceDir, "users", "bob.md");
245
+ seedGuardian({ id: "guardian-4", displayName: "Bob", userFile: "bob.md" });
246
+
247
+ writeFileSync(userMdPath(), customizedContent(), "utf-8");
248
+ expect(existsSync(join(workspaceDir(), "users"))).toBe(false);
249
+
250
+ dropUserMdMigration.run(workspaceDir());
251
+
252
+ expect(existsSync(join(workspaceDir(), "users"))).toBe(true);
253
+ const destPath = join(workspaceDir(), "users", "bob.md");
305
254
  expect(existsSync(destPath)).toBe(true);
306
255
  expect(readFileSync(destPath, "utf-8")).toBe(customizedContent());
307
- expect(existsSync(userMdPath)).toBe(false);
256
+ expect(existsSync(userMdPath())).toBe(false);
308
257
  });
309
258
 
310
- // ─── Bonus coverage for edge cases ───────────────────────────────
311
-
312
- test("falls back to listGuardianChannels when no vellum-channel guardian exists", () => {
313
- mockVellumGuardian = null;
314
- mockAnyGuardian = {
315
- contact: {
316
- id: "guardian-5",
317
- displayName: "Carol",
318
- userFile: "carol.md",
319
- },
320
- channels: [{ type: "telegram" }],
321
- };
259
+ test("falls back to any guardian when no vellum-channel guardian exists", () => {
260
+ seedGuardian({
261
+ id: "guardian-5",
262
+ displayName: "Carol",
263
+ userFile: "carol.md",
264
+ channelType: "telegram",
265
+ address: "carol-tg",
266
+ });
322
267
 
323
- const userMdPath = join(workspaceDir, "USER.md");
324
- writeFileSync(userMdPath, customizedContent(), "utf-8");
268
+ writeFileSync(userMdPath(), customizedContent(), "utf-8");
325
269
 
326
- dropUserMdMigration.run(workspaceDir);
270
+ dropUserMdMigration.run(workspaceDir());
327
271
 
328
- const destPath = join(workspaceDir, "users", "carol.md");
272
+ const destPath = join(workspaceDir(), "users", "carol.md");
329
273
  expect(existsSync(destPath)).toBe(true);
330
274
  expect(readFileSync(destPath, "utf-8")).toBe(customizedContent());
331
- expect(existsSync(userMdPath)).toBe(false);
275
+ expect(existsSync(userMdPath())).toBe(false);
276
+ });
277
+
278
+ test("prefers vellum-channel guardian over a more-recently-verified other guardian", () => {
279
+ // Older vellum guardian vs newer telegram guardian: vellum wins.
280
+ seedGuardian({
281
+ id: "guardian-vellum",
282
+ displayName: "Vee",
283
+ userFile: "vee.md",
284
+ channelType: "vellum",
285
+ address: "vellum:self",
286
+ verifiedAt: 1_000,
287
+ });
288
+ seedGuardian({
289
+ id: "guardian-tg",
290
+ displayName: "Tom",
291
+ userFile: "tom.md",
292
+ channelType: "telegram",
293
+ address: "tom-tg",
294
+ verifiedAt: 2_000,
295
+ });
296
+
297
+ writeFileSync(userMdPath(), customizedContent(), "utf-8");
298
+
299
+ dropUserMdMigration.run(workspaceDir());
300
+
301
+ expect(existsSync(join(workspaceDir(), "users", "vee.md"))).toBe(true);
302
+ expect(existsSync(join(workspaceDir(), "users", "tom.md"))).toBe(false);
332
303
  });
333
304
 
334
305
  test("template-shaped USER.md with no destination file — seeds scaffold and deletes USER.md", () => {
335
- mockVellumGuardian = {
336
- contact: {
337
- id: "guardian-6",
338
- displayName: "Dana",
339
- userFile: "dana.md",
340
- },
341
- channel: { type: "vellum" },
342
- };
306
+ seedGuardian({ id: "guardian-6", displayName: "Dana", userFile: "dana.md" });
343
307
 
344
- const userMdPath = join(workspaceDir, "USER.md");
345
- writeFileSync(userMdPath, templateContent(), "utf-8");
308
+ writeFileSync(userMdPath(), templateContent(), "utf-8");
346
309
 
347
- dropUserMdMigration.run(workspaceDir);
310
+ dropUserMdMigration.run(workspaceDir());
348
311
 
349
- // USER.md is gone.
350
- expect(existsSync(userMdPath)).toBe(false);
312
+ expect(existsSync(userMdPath())).toBe(false);
351
313
 
352
- // The destination was scaffolded with the guardian persona template
353
- // (parity with ensureGuardianPersonaFile for new installs).
354
- const destPath = join(workspaceDir, "users", "dana.md");
314
+ const destPath = join(workspaceDir(), "users", "dana.md");
355
315
  expect(existsSync(destPath)).toBe(true);
356
316
  const content = readFileSync(destPath, "utf-8");
357
317
  expect(content).toContain("# User Profile");
358
318
  expect(content).toContain("Preferred name/reference:");
359
- // Not the legacy template header.
360
319
  expect(content).not.toContain("# USER.md");
361
320
  });
362
321
 
363
322
  test("down() is a no-op (deletion is irreversible)", () => {
364
- // Should not throw.
365
- dropUserMdMigration.down(workspaceDir);
366
- expect(existsSync(join(workspaceDir, "USER.md"))).toBe(false);
323
+ dropUserMdMigration.down(workspaceDir());
324
+ expect(existsSync(userMdPath())).toBe(false);
367
325
  });
368
326
  });