@cleocode/core 2026.4.11 → 2026.4.13

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 (184) hide show
  1. package/dist/codebase-map/analyzers/architecture.d.ts.map +1 -1
  2. package/dist/codebase-map/analyzers/architecture.js +0 -1
  3. package/dist/codebase-map/analyzers/architecture.js.map +1 -1
  4. package/dist/conduit/local-transport.d.ts +18 -8
  5. package/dist/conduit/local-transport.d.ts.map +1 -1
  6. package/dist/conduit/local-transport.js +23 -13
  7. package/dist/conduit/local-transport.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +0 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/errors.d.ts +19 -0
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +6 -0
  14. package/dist/errors.js.map +1 -1
  15. package/dist/index.js +175 -68950
  16. package/dist/index.js.map +1 -7
  17. package/dist/init.d.ts +1 -2
  18. package/dist/init.d.ts.map +1 -1
  19. package/dist/init.js +1 -2
  20. package/dist/init.js.map +1 -1
  21. package/dist/internal.d.ts +8 -3
  22. package/dist/internal.d.ts.map +1 -1
  23. package/dist/internal.js +13 -6
  24. package/dist/internal.js.map +1 -1
  25. package/dist/memory/learnings.d.ts +2 -2
  26. package/dist/memory/patterns.d.ts +6 -6
  27. package/dist/output.d.ts +32 -11
  28. package/dist/output.d.ts.map +1 -1
  29. package/dist/output.js +67 -67
  30. package/dist/output.js.map +1 -1
  31. package/dist/paths.js +80 -14
  32. package/dist/paths.js.map +1 -1
  33. package/dist/skills/dynamic-skill-generator.d.ts +0 -2
  34. package/dist/skills/dynamic-skill-generator.d.ts.map +1 -1
  35. package/dist/skills/dynamic-skill-generator.js.map +1 -1
  36. package/dist/store/agent-registry-accessor.d.ts +203 -12
  37. package/dist/store/agent-registry-accessor.d.ts.map +1 -1
  38. package/dist/store/agent-registry-accessor.js +618 -100
  39. package/dist/store/agent-registry-accessor.js.map +1 -1
  40. package/dist/store/api-key-kdf.d.ts +73 -0
  41. package/dist/store/api-key-kdf.d.ts.map +1 -0
  42. package/dist/store/api-key-kdf.js +84 -0
  43. package/dist/store/api-key-kdf.js.map +1 -0
  44. package/dist/store/cleanup-legacy.js +171 -0
  45. package/dist/store/cleanup-legacy.js.map +1 -0
  46. package/dist/store/conduit-sqlite.d.ts +184 -0
  47. package/dist/store/conduit-sqlite.d.ts.map +1 -0
  48. package/dist/store/conduit-sqlite.js +570 -0
  49. package/dist/store/conduit-sqlite.js.map +1 -0
  50. package/dist/store/global-salt.d.ts +78 -0
  51. package/dist/store/global-salt.d.ts.map +1 -0
  52. package/dist/store/global-salt.js +147 -0
  53. package/dist/store/global-salt.js.map +1 -0
  54. package/dist/store/migrate-signaldock-to-conduit.d.ts +81 -0
  55. package/dist/store/migrate-signaldock-to-conduit.d.ts.map +1 -0
  56. package/dist/store/migrate-signaldock-to-conduit.js +555 -0
  57. package/dist/store/migrate-signaldock-to-conduit.js.map +1 -0
  58. package/dist/store/nexus-sqlite.js +28 -3
  59. package/dist/store/nexus-sqlite.js.map +1 -1
  60. package/dist/store/signaldock-sqlite.d.ts +122 -19
  61. package/dist/store/signaldock-sqlite.d.ts.map +1 -1
  62. package/dist/store/signaldock-sqlite.js +401 -251
  63. package/dist/store/signaldock-sqlite.js.map +1 -1
  64. package/dist/store/sqlite-backup.js +122 -4
  65. package/dist/store/sqlite-backup.js.map +1 -1
  66. package/dist/system/backup.d.ts +0 -26
  67. package/dist/system/backup.d.ts.map +1 -1
  68. package/dist/system/runtime.d.ts +0 -2
  69. package/dist/system/runtime.d.ts.map +1 -1
  70. package/dist/system/runtime.js +3 -3
  71. package/dist/system/runtime.js.map +1 -1
  72. package/dist/tasks/add.d.ts +1 -1
  73. package/dist/tasks/add.d.ts.map +1 -1
  74. package/dist/tasks/add.js +98 -23
  75. package/dist/tasks/add.js.map +1 -1
  76. package/dist/tasks/complete.d.ts.map +1 -1
  77. package/dist/tasks/complete.js +4 -1
  78. package/dist/tasks/complete.js.map +1 -1
  79. package/dist/tasks/find.d.ts.map +1 -1
  80. package/dist/tasks/find.js +4 -1
  81. package/dist/tasks/find.js.map +1 -1
  82. package/dist/tasks/labels.d.ts.map +1 -1
  83. package/dist/tasks/labels.js +4 -1
  84. package/dist/tasks/labels.js.map +1 -1
  85. package/dist/tasks/relates.d.ts.map +1 -1
  86. package/dist/tasks/relates.js +16 -4
  87. package/dist/tasks/relates.js.map +1 -1
  88. package/dist/tasks/show.d.ts.map +1 -1
  89. package/dist/tasks/show.js +4 -1
  90. package/dist/tasks/show.js.map +1 -1
  91. package/dist/tasks/update.d.ts.map +1 -1
  92. package/dist/tasks/update.js +32 -6
  93. package/dist/tasks/update.js.map +1 -1
  94. package/dist/validation/engine.d.ts.map +1 -1
  95. package/dist/validation/engine.js +16 -4
  96. package/dist/validation/engine.js.map +1 -1
  97. package/dist/validation/param-utils.d.ts +5 -3
  98. package/dist/validation/param-utils.d.ts.map +1 -1
  99. package/dist/validation/param-utils.js +8 -6
  100. package/dist/validation/param-utils.js.map +1 -1
  101. package/dist/validation/protocols/_shared.d.ts.map +1 -1
  102. package/dist/validation/protocols/_shared.js +13 -6
  103. package/dist/validation/protocols/_shared.js.map +1 -1
  104. package/package.json +9 -7
  105. package/src/adapters/__tests__/manager.test.ts +0 -1
  106. package/src/codebase-map/analyzers/architecture.ts +0 -1
  107. package/src/conduit/__tests__/local-credential-flow.test.ts +20 -18
  108. package/src/conduit/__tests__/local-transport.test.ts +14 -12
  109. package/src/conduit/local-transport.ts +23 -13
  110. package/src/config.ts +0 -1
  111. package/src/errors.ts +24 -0
  112. package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +2 -5
  113. package/src/init.ts +1 -2
  114. package/src/internal.ts +96 -2
  115. package/src/lifecycle/cant/lifecycle-rcasd.cant +133 -0
  116. package/src/memory/__tests__/engine-compat.test.ts +2 -2
  117. package/src/memory/__tests__/pipeline-manifest-sqlite.test.ts +4 -4
  118. package/src/observability/__tests__/index.test.ts +4 -4
  119. package/src/observability/__tests__/log-filter.test.ts +4 -4
  120. package/src/output.ts +73 -75
  121. package/src/sessions/__tests__/session-grade.integration.test.ts +1 -1
  122. package/src/sessions/__tests__/session-grade.test.ts +2 -2
  123. package/src/skills/__tests__/dynamic-skill-generator.test.ts +0 -2
  124. package/src/skills/dynamic-skill-generator.ts +0 -2
  125. package/src/store/__tests__/agent-registry-accessor.test.ts +807 -0
  126. package/src/store/__tests__/api-key-kdf.test.ts +113 -0
  127. package/src/store/__tests__/backup-crypto.test.ts +101 -0
  128. package/src/store/__tests__/backup-pack.test.ts +491 -0
  129. package/src/store/__tests__/backup-unpack.test.ts +298 -0
  130. package/src/store/__tests__/conduit-sqlite.test.ts +413 -0
  131. package/src/store/__tests__/global-salt.test.ts +195 -0
  132. package/src/store/__tests__/migrate-signaldock-to-conduit.test.ts +715 -0
  133. package/src/store/__tests__/regenerators.test.ts +234 -0
  134. package/src/store/__tests__/restore-conflict-report.test.ts +274 -0
  135. package/src/store/__tests__/restore-json-merge.test.ts +521 -0
  136. package/src/store/__tests__/signaldock-sqlite.test.ts +652 -0
  137. package/src/store/__tests__/sqlite-backup-global.test.ts +307 -3
  138. package/src/store/__tests__/sqlite-backup.test.ts +5 -1
  139. package/src/store/__tests__/t310-integration.test.ts +1150 -0
  140. package/src/store/__tests__/t310-readiness.test.ts +111 -0
  141. package/src/store/__tests__/t311-integration.test.ts +661 -0
  142. package/src/store/agent-registry-accessor.ts +847 -140
  143. package/src/store/api-key-kdf.ts +104 -0
  144. package/src/store/backup-crypto.ts +209 -0
  145. package/src/store/backup-pack.ts +739 -0
  146. package/src/store/backup-unpack.ts +583 -0
  147. package/src/store/conduit-sqlite.ts +655 -0
  148. package/src/store/global-salt.ts +175 -0
  149. package/src/store/migrate-signaldock-to-conduit.ts +669 -0
  150. package/src/store/regenerators.ts +243 -0
  151. package/src/store/restore-conflict-report.ts +317 -0
  152. package/src/store/restore-json-merge.ts +653 -0
  153. package/src/store/signaldock-sqlite.ts +431 -254
  154. package/src/store/sqlite-backup.ts +185 -10
  155. package/src/store/t310-readiness.ts +119 -0
  156. package/src/system/backup.ts +2 -62
  157. package/src/system/runtime.ts +4 -6
  158. package/src/tasks/__tests__/error-hints.test.ts +256 -0
  159. package/src/tasks/add.ts +99 -9
  160. package/src/tasks/complete.ts +4 -1
  161. package/src/tasks/find.ts +4 -1
  162. package/src/tasks/labels.ts +4 -1
  163. package/src/tasks/relates.ts +16 -4
  164. package/src/tasks/show.ts +4 -1
  165. package/src/tasks/update.ts +32 -3
  166. package/src/validation/__tests__/error-hints.test.ts +97 -0
  167. package/src/validation/engine.ts +16 -1
  168. package/src/validation/param-utils.ts +10 -7
  169. package/src/validation/protocols/_shared.ts +14 -6
  170. package/src/validation/protocols/cant/architecture-decision.cant +80 -0
  171. package/src/validation/protocols/cant/artifact-publish.cant +95 -0
  172. package/src/validation/protocols/cant/consensus.cant +74 -0
  173. package/src/validation/protocols/cant/contribution.cant +82 -0
  174. package/src/validation/protocols/cant/decomposition.cant +92 -0
  175. package/src/validation/protocols/cant/implementation.cant +67 -0
  176. package/src/validation/protocols/cant/provenance.cant +88 -0
  177. package/src/validation/protocols/cant/release.cant +96 -0
  178. package/src/validation/protocols/cant/research.cant +66 -0
  179. package/src/validation/protocols/cant/specification.cant +67 -0
  180. package/src/validation/protocols/cant/testing.cant +88 -0
  181. package/src/validation/protocols/cant/validation.cant +65 -0
  182. package/src/validation/protocols/protocols-markdown/decomposition.md +0 -4
  183. package/templates/config.template.json +0 -1
  184. package/templates/global-config.template.json +0 -1
@@ -0,0 +1,715 @@
1
+ /**
2
+ * Unit tests for migrate-signaldock-to-conduit.ts (T358).
3
+ *
4
+ * Covers all 9 required test scenarios:
5
+ * 1. Fresh install (no legacy) — needsMigration=false, no-op
6
+ * 2. TC-062/TC-060/TC-061: needsMigration detection variants
7
+ * 3. Legacy with 0 agents — migrated, conduit+global+bak created
8
+ * 4. Legacy with 3 agents — all 3 in global, 3 project_agent_refs, requires_reauth=1
9
+ * 5. Multi-project deduplication — INSERT OR IGNORE, global single row
10
+ * 6. Legacy with messages + conversations — all rows copied to conduit
11
+ * 7. TC-068: Broken legacy integrity_check — aborts, no conduit, no bak
12
+ * 8. TC-067: Idempotent re-run — second call is no-op
13
+ * 9. .pre-t310.bak alongside conduit.db — needsMigration=false
14
+ *
15
+ * Tests use real SQLite in isolated tmp directories. `getCleoHome()` and
16
+ * `getGlobalSalt()` are mocked to prevent touching real XDG_DATA_HOME and
17
+ * to avoid machine-key dependency in tests.
18
+ *
19
+ * @task T358
20
+ * @epic T310
21
+ */
22
+
23
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
24
+ import { tmpdir } from 'node:os';
25
+ import { join } from 'node:path';
26
+ import { DatabaseSync } from 'node:sqlite';
27
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Logger mock — prevents pino from opening real log files during tests
31
+ // ---------------------------------------------------------------------------
32
+
33
+ vi.mock('../../logger.js', () => ({
34
+ getLogger: () => ({
35
+ info: vi.fn(),
36
+ warn: vi.fn(),
37
+ error: vi.fn(),
38
+ debug: vi.fn(),
39
+ }),
40
+ }));
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // global-salt mock — prevents filesystem access to real salt file
44
+ // ---------------------------------------------------------------------------
45
+
46
+ vi.mock('../global-salt.js', () => ({
47
+ getGlobalSalt: () => Buffer.alloc(32, 0xab),
48
+ __clearGlobalSaltCache: vi.fn(),
49
+ }));
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Helpers
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /**
56
+ * Creates a fresh isolated tmp directory pair (projectRoot + cleoHome).
57
+ */
58
+ function createIsolatedDirs(): {
59
+ projectRoot: string;
60
+ home: string;
61
+ cleanup: () => void;
62
+ } {
63
+ const uid = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
64
+ const base = join(tmpdir(), `cleo-t358-${uid}`);
65
+ const projectRoot = join(base, 'project');
66
+ const home = join(base, 'cleo-home');
67
+ mkdirSync(join(projectRoot, '.cleo'), { recursive: true });
68
+ mkdirSync(home, { recursive: true });
69
+ return {
70
+ projectRoot,
71
+ home,
72
+ cleanup: () => rmSync(base, { recursive: true, force: true }),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Creates a minimal global signaldock.db with the agents table and
78
+ * required schema tracking tables.
79
+ */
80
+ function createGlobalSignaldockDb(
81
+ cleoHomeDir: string,
82
+ seedAgents: Array<{
83
+ id: string;
84
+ agent_id: string;
85
+ name: string;
86
+ created_at?: number;
87
+ updated_at?: number;
88
+ requires_reauth?: number;
89
+ }> = [],
90
+ ): string {
91
+ const dbPath = join(cleoHomeDir, 'signaldock.db');
92
+ const db = new DatabaseSync(dbPath);
93
+ const now = Math.floor(Date.now() / 1000);
94
+ db.exec(`
95
+ CREATE TABLE IF NOT EXISTS agents (
96
+ id TEXT PRIMARY KEY,
97
+ agent_id TEXT NOT NULL UNIQUE,
98
+ name TEXT NOT NULL,
99
+ class TEXT NOT NULL DEFAULT 'custom',
100
+ privacy_tier TEXT NOT NULL DEFAULT 'public',
101
+ capabilities TEXT NOT NULL DEFAULT '[]',
102
+ skills TEXT NOT NULL DEFAULT '[]',
103
+ messages_sent INTEGER NOT NULL DEFAULT 0,
104
+ messages_received INTEGER NOT NULL DEFAULT 0,
105
+ conversation_count INTEGER NOT NULL DEFAULT 0,
106
+ friend_count INTEGER NOT NULL DEFAULT 0,
107
+ status TEXT NOT NULL DEFAULT 'online',
108
+ payment_config TEXT,
109
+ api_key_hash TEXT,
110
+ created_at INTEGER NOT NULL,
111
+ updated_at INTEGER NOT NULL,
112
+ transport_type TEXT NOT NULL DEFAULT 'http',
113
+ api_key_encrypted TEXT,
114
+ api_base_url TEXT NOT NULL DEFAULT 'https://api.signaldock.io',
115
+ classification TEXT,
116
+ transport_config TEXT NOT NULL DEFAULT '{}',
117
+ is_active INTEGER NOT NULL DEFAULT 1,
118
+ last_used_at INTEGER,
119
+ requires_reauth INTEGER NOT NULL DEFAULT 0
120
+ );
121
+ CREATE TABLE IF NOT EXISTS capabilities (
122
+ id TEXT PRIMARY KEY, slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL,
123
+ description TEXT NOT NULL, category TEXT NOT NULL, created_at INTEGER NOT NULL
124
+ );
125
+ CREATE TABLE IF NOT EXISTS skills (
126
+ id TEXT PRIMARY KEY, slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL,
127
+ description TEXT NOT NULL, category TEXT NOT NULL, created_at INTEGER NOT NULL
128
+ );
129
+ CREATE TABLE IF NOT EXISTS agent_capabilities (
130
+ agent_id TEXT NOT NULL, capability_id TEXT NOT NULL,
131
+ PRIMARY KEY (agent_id, capability_id)
132
+ );
133
+ CREATE TABLE IF NOT EXISTS agent_skills (
134
+ agent_id TEXT NOT NULL, skill_id TEXT NOT NULL,
135
+ PRIMARY KEY (agent_id, skill_id)
136
+ );
137
+ CREATE TABLE IF NOT EXISTS agent_connections (
138
+ id TEXT PRIMARY KEY NOT NULL, agent_id TEXT NOT NULL,
139
+ transport_type TEXT NOT NULL DEFAULT 'http', connection_id TEXT,
140
+ connected_at BIGINT NOT NULL, last_heartbeat BIGINT NOT NULL,
141
+ connection_metadata TEXT, created_at BIGINT NOT NULL,
142
+ UNIQUE(agent_id, connection_id)
143
+ );
144
+ CREATE TABLE IF NOT EXISTS users (
145
+ id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE,
146
+ password_hash TEXT NOT NULL, name TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
147
+ );
148
+ CREATE TABLE IF NOT EXISTS accounts (
149
+ id TEXT PRIMARY KEY NOT NULL, user_id TEXT NOT NULL, account_id TEXT NOT NULL,
150
+ provider_id TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
151
+ );
152
+ CREATE TABLE IF NOT EXISTS sessions (
153
+ id TEXT PRIMARY KEY NOT NULL, user_id TEXT NOT NULL, token TEXT NOT NULL UNIQUE,
154
+ expires_at TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
155
+ );
156
+ CREATE TABLE IF NOT EXISTS verifications (
157
+ id TEXT PRIMARY KEY NOT NULL, identifier TEXT NOT NULL, value TEXT NOT NULL,
158
+ expires_at TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
159
+ );
160
+ CREATE TABLE IF NOT EXISTS organization (
161
+ id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL,
162
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
163
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
164
+ );
165
+ CREATE TABLE IF NOT EXISTS claim_codes (
166
+ id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, code TEXT NOT NULL UNIQUE,
167
+ expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL
168
+ );
169
+ CREATE TABLE IF NOT EXISTS org_agent_keys (
170
+ id TEXT PRIMARY KEY NOT NULL, organization_id TEXT NOT NULL,
171
+ agent_id TEXT NOT NULL, created_by TEXT NOT NULL, created_at INTEGER NOT NULL
172
+ );
173
+ CREATE TABLE IF NOT EXISTS _signaldock_meta (
174
+ key TEXT PRIMARY KEY, value TEXT NOT NULL,
175
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
176
+ );
177
+ CREATE TABLE IF NOT EXISTS _signaldock_migrations (
178
+ name TEXT PRIMARY KEY,
179
+ applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
180
+ );
181
+ `);
182
+
183
+ for (const agent of seedAgents) {
184
+ db.prepare(
185
+ `INSERT OR IGNORE INTO agents
186
+ (id, agent_id, name, created_at, updated_at, requires_reauth)
187
+ VALUES (?, ?, ?, ?, ?, ?)`,
188
+ ).run(
189
+ agent.id,
190
+ agent.agent_id,
191
+ agent.name,
192
+ agent.created_at ?? now,
193
+ agent.updated_at ?? now,
194
+ agent.requires_reauth ?? 0,
195
+ );
196
+ }
197
+ db.close();
198
+ return dbPath;
199
+ }
200
+
201
+ /**
202
+ * Creates a minimal legacy signaldock.db at `<projectRoot>/.cleo/signaldock.db`.
203
+ */
204
+ function createLegacySignaldockDb(
205
+ projectRoot: string,
206
+ agents: Array<{
207
+ id: string;
208
+ agent_id: string;
209
+ name: string;
210
+ classification?: string;
211
+ created_at?: number;
212
+ last_used_at?: number;
213
+ api_key_encrypted?: string;
214
+ }> = [],
215
+ messages: Array<{
216
+ id: string;
217
+ conversation_id: string;
218
+ from_agent_id: string;
219
+ to_agent_id: string;
220
+ content: string;
221
+ created_at: number;
222
+ }> = [],
223
+ conversations: Array<{
224
+ id: string;
225
+ participants: string;
226
+ created_at: number;
227
+ updated_at: number;
228
+ }> = [],
229
+ ): string {
230
+ const dbPath = join(projectRoot, '.cleo', 'signaldock.db');
231
+ const db = new DatabaseSync(dbPath);
232
+ const now = Math.floor(Date.now() / 1000);
233
+
234
+ db.exec(`
235
+ CREATE TABLE IF NOT EXISTS agents (
236
+ id TEXT PRIMARY KEY,
237
+ agent_id TEXT NOT NULL UNIQUE,
238
+ name TEXT NOT NULL,
239
+ class TEXT NOT NULL DEFAULT 'custom',
240
+ classification TEXT,
241
+ created_at INTEGER NOT NULL,
242
+ updated_at INTEGER NOT NULL,
243
+ api_key_encrypted TEXT,
244
+ last_used_at INTEGER,
245
+ requires_reauth INTEGER NOT NULL DEFAULT 0
246
+ );
247
+ CREATE TABLE IF NOT EXISTS conversations (
248
+ id TEXT PRIMARY KEY,
249
+ participants TEXT NOT NULL,
250
+ visibility TEXT NOT NULL DEFAULT 'private',
251
+ message_count INTEGER NOT NULL DEFAULT 0,
252
+ last_message_at INTEGER,
253
+ created_at INTEGER NOT NULL,
254
+ updated_at INTEGER NOT NULL
255
+ );
256
+ CREATE TABLE IF NOT EXISTS messages (
257
+ id TEXT PRIMARY KEY,
258
+ conversation_id TEXT NOT NULL,
259
+ from_agent_id TEXT NOT NULL,
260
+ to_agent_id TEXT NOT NULL,
261
+ content TEXT NOT NULL,
262
+ content_type TEXT NOT NULL DEFAULT 'text',
263
+ status TEXT NOT NULL DEFAULT 'pending',
264
+ attachments TEXT NOT NULL DEFAULT '[]',
265
+ group_id TEXT,
266
+ metadata TEXT DEFAULT '{}',
267
+ reply_to TEXT,
268
+ created_at INTEGER NOT NULL,
269
+ delivered_at INTEGER,
270
+ read_at INTEGER
271
+ );
272
+ `);
273
+
274
+ for (const agent of agents) {
275
+ db.prepare(
276
+ `INSERT INTO agents
277
+ (id, agent_id, name, classification, created_at, updated_at, api_key_encrypted, last_used_at)
278
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
279
+ ).run(
280
+ agent.id,
281
+ agent.agent_id,
282
+ agent.name,
283
+ agent.classification ?? null,
284
+ agent.created_at ?? now,
285
+ now,
286
+ agent.api_key_encrypted ?? null,
287
+ agent.last_used_at ?? null,
288
+ );
289
+ }
290
+
291
+ for (const conv of conversations) {
292
+ db.prepare(
293
+ `INSERT INTO conversations (id, participants, created_at, updated_at) VALUES (?, ?, ?, ?)`,
294
+ ).run(conv.id, conv.participants, conv.created_at, conv.updated_at);
295
+ }
296
+
297
+ for (const msg of messages) {
298
+ db.prepare(
299
+ `INSERT INTO messages
300
+ (id, conversation_id, from_agent_id, to_agent_id, content, created_at)
301
+ VALUES (?, ?, ?, ?, ?, ?)`,
302
+ ).run(
303
+ msg.id,
304
+ msg.conversation_id,
305
+ msg.from_agent_id,
306
+ msg.to_agent_id,
307
+ msg.content,
308
+ msg.created_at,
309
+ );
310
+ }
311
+
312
+ db.close();
313
+ return dbPath;
314
+ }
315
+
316
+ // ---------------------------------------------------------------------------
317
+ // Run migration with isolated cleoHome mock
318
+ // ---------------------------------------------------------------------------
319
+
320
+ /**
321
+ * Import and run the migration with the cleoHome mock pointing to `home`.
322
+ * Each call resets modules to get a fresh import chain.
323
+ */
324
+ async function runMigration(
325
+ projectRoot: string,
326
+ home: string,
327
+ ): Promise<import('../migrate-signaldock-to-conduit.js').MigrationResult> {
328
+ vi.resetModules();
329
+ vi.doMock('../../paths.js', () => ({
330
+ getCleoHome: () => home,
331
+ getProjectRoot: () => projectRoot,
332
+ }));
333
+ vi.doMock('../global-salt.js', () => ({
334
+ getGlobalSalt: () => Buffer.alloc(32, 0xab),
335
+ __clearGlobalSaltCache: vi.fn(),
336
+ }));
337
+ vi.doMock('../signaldock-sqlite.js', () => ({
338
+ ensureGlobalSignaldockDb: vi.fn(async () => ({
339
+ action: 'exists',
340
+ path: join(home, 'signaldock.db'),
341
+ })),
342
+ getGlobalSignaldockDbPath: () => join(home, 'signaldock.db'),
343
+ }));
344
+ const { migrateSignaldockToConduit } = await import('../migrate-signaldock-to-conduit.js');
345
+ return migrateSignaldockToConduit(projectRoot);
346
+ }
347
+
348
+ async function getNeedsMigration(projectRoot: string, home: string): Promise<boolean> {
349
+ vi.resetModules();
350
+ vi.doMock('../../paths.js', () => ({
351
+ getCleoHome: () => home,
352
+ getProjectRoot: () => projectRoot,
353
+ }));
354
+ const { needsSignaldockToConduitMigration } = await import('../migrate-signaldock-to-conduit.js');
355
+ return needsSignaldockToConduitMigration(projectRoot);
356
+ }
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // Test suite
360
+ // ---------------------------------------------------------------------------
361
+
362
+ describe('migrate-signaldock-to-conduit', () => {
363
+ let dirs: ReturnType<typeof createIsolatedDirs>;
364
+
365
+ beforeEach(() => {
366
+ dirs = createIsolatedDirs();
367
+ });
368
+
369
+ afterEach(() => {
370
+ vi.restoreAllMocks();
371
+ dirs.cleanup();
372
+ });
373
+
374
+ // -------------------------------------------------------------------------
375
+ // Detection: TC-060, TC-061, TC-062
376
+ // -------------------------------------------------------------------------
377
+ describe('needsSignaldockToConduitMigration', () => {
378
+ it('TC-061: returns false when signaldock.db absent (fresh install)', async () => {
379
+ const result = await getNeedsMigration(dirs.projectRoot, dirs.home);
380
+ expect(result).toBe(false);
381
+ });
382
+
383
+ it('TC-060: returns false when conduit.db exists (migration already done)', async () => {
384
+ writeFileSync(join(dirs.projectRoot, '.cleo', 'conduit.db'), '');
385
+ const result = await getNeedsMigration(dirs.projectRoot, dirs.home);
386
+ expect(result).toBe(false);
387
+ });
388
+
389
+ it('TC-062: returns true when signaldock.db present AND conduit.db absent', async () => {
390
+ createLegacySignaldockDb(dirs.projectRoot, []);
391
+ const result = await getNeedsMigration(dirs.projectRoot, dirs.home);
392
+ expect(result).toBe(true);
393
+ });
394
+ });
395
+
396
+ // -------------------------------------------------------------------------
397
+ // Scenario 1: Fresh install — no legacy signaldock.db
398
+ // -------------------------------------------------------------------------
399
+ it('returns no-op when no legacy signaldock.db exists', async () => {
400
+ const result = await runMigration(dirs.projectRoot, dirs.home);
401
+ expect(result.status).toBe('no-op');
402
+ expect(result.agentsCopied).toBe(0);
403
+ expect(result.bakPath).toBeNull();
404
+ expect(result.errors).toHaveLength(0);
405
+ expect(existsSync(join(dirs.projectRoot, '.cleo', 'conduit.db'))).toBe(false);
406
+ });
407
+
408
+ // -------------------------------------------------------------------------
409
+ // TC-063: Legacy with 0 agents
410
+ // -------------------------------------------------------------------------
411
+ it('TC-063: 0-agent migration creates conduit.db + global signaldock.db + .pre-t310.bak', async () => {
412
+ createLegacySignaldockDb(dirs.projectRoot, []);
413
+ createGlobalSignaldockDb(dirs.home, []);
414
+
415
+ const result = await runMigration(dirs.projectRoot, dirs.home);
416
+
417
+ expect(result.status).toBe('migrated');
418
+ expect(result.agentsCopied).toBe(0);
419
+ expect(result.bakPath).not.toBeNull();
420
+ expect(result.errors).toHaveLength(0);
421
+ expect(existsSync(join(dirs.projectRoot, '.cleo', 'conduit.db'))).toBe(true);
422
+ expect(existsSync(join(dirs.projectRoot, '.cleo', 'signaldock.db.pre-t310.bak'))).toBe(true);
423
+ expect(existsSync(join(dirs.projectRoot, '.cleo', 'signaldock.db'))).toBe(false);
424
+ });
425
+
426
+ // -------------------------------------------------------------------------
427
+ // TC-064: Legacy with 3 agents — all migrated, project_agent_refs created
428
+ // -------------------------------------------------------------------------
429
+ it('TC-064: 3 agents migrated to global; 3 project_agent_refs rows; requires_reauth=1', async () => {
430
+ const now = Math.floor(Date.now() / 1000);
431
+ createLegacySignaldockDb(dirs.projectRoot, [
432
+ { id: 'id-1', agent_id: 'agent-alpha', name: 'Alpha', created_at: now },
433
+ { id: 'id-2', agent_id: 'agent-beta', name: 'Beta', created_at: now },
434
+ { id: 'id-3', agent_id: 'agent-gamma', name: 'Gamma', created_at: now },
435
+ ]);
436
+ createGlobalSignaldockDb(dirs.home, []);
437
+
438
+ const result = await runMigration(dirs.projectRoot, dirs.home);
439
+
440
+ expect(result.status).toBe('migrated');
441
+ expect(result.agentsCopied).toBe(3);
442
+ expect(result.errors).toHaveLength(0);
443
+
444
+ // Verify project_agent_refs in conduit.db
445
+ const conduitDb = new DatabaseSync(join(dirs.projectRoot, '.cleo', 'conduit.db'));
446
+ const refs = conduitDb
447
+ .prepare('SELECT agent_id FROM project_agent_refs ORDER BY agent_id')
448
+ .all() as Array<{ agent_id: string }>;
449
+ expect(refs.map((r) => r.agent_id)).toEqual(['agent-alpha', 'agent-beta', 'agent-gamma']);
450
+
451
+ const allEnabled = conduitDb.prepare('SELECT enabled FROM project_agent_refs').all() as Array<{
452
+ enabled: number;
453
+ }>;
454
+ for (const row of allEnabled) {
455
+ expect(row.enabled).toBe(1);
456
+ }
457
+ conduitDb.close();
458
+
459
+ // Verify requires_reauth=1 in global signaldock.db
460
+ const globalDb = new DatabaseSync(join(dirs.home, 'signaldock.db'));
461
+ const agents = globalDb
462
+ .prepare('SELECT agent_id, requires_reauth FROM agents ORDER BY agent_id')
463
+ .all() as Array<{ agent_id: string; requires_reauth: number }>;
464
+ expect(agents).toHaveLength(3);
465
+ for (const agent of agents) {
466
+ expect(agent.requires_reauth).toBe(1);
467
+ }
468
+ globalDb.close();
469
+ });
470
+
471
+ // -------------------------------------------------------------------------
472
+ // TC-066: Multi-project deduplication
473
+ // -------------------------------------------------------------------------
474
+ it('TC-066: same agent in two projects — INSERT OR IGNORE; global has one row', async () => {
475
+ const now = Math.floor(Date.now() / 1000);
476
+
477
+ // Pre-seed global with agent-x already present (migrated from project A)
478
+ createGlobalSignaldockDb(dirs.home, [
479
+ {
480
+ id: 'existing-id',
481
+ agent_id: 'agent-x',
482
+ name: 'Agent X Original',
483
+ created_at: now,
484
+ updated_at: now,
485
+ },
486
+ ]);
487
+
488
+ // Project B also has agent-x in its legacy signaldock.db
489
+ createLegacySignaldockDb(dirs.projectRoot, [
490
+ { id: 'legacy-id', agent_id: 'agent-x', name: 'Agent X Legacy', created_at: now },
491
+ ]);
492
+
493
+ const result = await runMigration(dirs.projectRoot, dirs.home);
494
+
495
+ expect(result.status).toBe('migrated');
496
+ expect(result.errors).toHaveLength(0);
497
+
498
+ // Global should still have ONE agent-x (INSERT OR IGNORE preserved existing)
499
+ const globalDb = new DatabaseSync(join(dirs.home, 'signaldock.db'));
500
+ const agents = globalDb
501
+ .prepare("SELECT agent_id, name FROM agents WHERE agent_id = 'agent-x'")
502
+ .all() as Array<{ agent_id: string; name: string }>;
503
+ expect(agents).toHaveLength(1);
504
+ expect(agents[0]?.name).toBe('Agent X Original');
505
+ globalDb.close();
506
+
507
+ // conduit.db should have a project_agent_refs row for agent-x
508
+ const conduitDb = new DatabaseSync(join(dirs.projectRoot, '.cleo', 'conduit.db'));
509
+ const refs = conduitDb
510
+ .prepare("SELECT agent_id FROM project_agent_refs WHERE agent_id = 'agent-x'")
511
+ .all() as Array<{ agent_id: string }>;
512
+ expect(refs).toHaveLength(1);
513
+ conduitDb.close();
514
+ });
515
+
516
+ // -------------------------------------------------------------------------
517
+ // TC-070/TC-071: Messages + conversations copied
518
+ // -------------------------------------------------------------------------
519
+ it('TC-070/TC-071: 5 messages + 2 conversations preserved in conduit.db', async () => {
520
+ const now = Math.floor(Date.now() / 1000);
521
+ const conversations = [
522
+ { id: 'conv-1', participants: '["a1","a2"]', created_at: now, updated_at: now },
523
+ { id: 'conv-2', participants: '["a1","a3"]', created_at: now + 1, updated_at: now + 1 },
524
+ ];
525
+ const messages = [
526
+ {
527
+ id: 'msg-1',
528
+ conversation_id: 'conv-1',
529
+ from_agent_id: 'a1',
530
+ to_agent_id: 'a2',
531
+ content: 'hello',
532
+ created_at: now,
533
+ },
534
+ {
535
+ id: 'msg-2',
536
+ conversation_id: 'conv-1',
537
+ from_agent_id: 'a2',
538
+ to_agent_id: 'a1',
539
+ content: 'world',
540
+ created_at: now + 1,
541
+ },
542
+ {
543
+ id: 'msg-3',
544
+ conversation_id: 'conv-2',
545
+ from_agent_id: 'a1',
546
+ to_agent_id: 'a3',
547
+ content: 'foo',
548
+ created_at: now + 2,
549
+ },
550
+ {
551
+ id: 'msg-4',
552
+ conversation_id: 'conv-2',
553
+ from_agent_id: 'a3',
554
+ to_agent_id: 'a1',
555
+ content: 'bar',
556
+ created_at: now + 3,
557
+ },
558
+ {
559
+ id: 'msg-5',
560
+ conversation_id: 'conv-1',
561
+ from_agent_id: 'a1',
562
+ to_agent_id: 'a2',
563
+ content: 'baz',
564
+ created_at: now + 4,
565
+ },
566
+ ];
567
+
568
+ createLegacySignaldockDb(dirs.projectRoot, [], messages, conversations);
569
+ createGlobalSignaldockDb(dirs.home, []);
570
+
571
+ const result = await runMigration(dirs.projectRoot, dirs.home);
572
+
573
+ expect(result.status).toBe('migrated');
574
+ expect(result.errors).toHaveLength(0);
575
+
576
+ const conduitDb = new DatabaseSync(join(dirs.projectRoot, '.cleo', 'conduit.db'));
577
+
578
+ const convRows = conduitDb.prepare('SELECT id FROM conversations ORDER BY id').all() as Array<{
579
+ id: string;
580
+ }>;
581
+ expect(convRows.map((r) => r.id)).toEqual(['conv-1', 'conv-2']);
582
+
583
+ const msgRows = conduitDb.prepare('SELECT id FROM messages ORDER BY id').all() as Array<{
584
+ id: string;
585
+ }>;
586
+ expect(msgRows.map((r) => r.id)).toEqual(['msg-1', 'msg-2', 'msg-3', 'msg-4', 'msg-5']);
587
+
588
+ conduitDb.close();
589
+ });
590
+
591
+ // -------------------------------------------------------------------------
592
+ // TC-072: FTS search after migration
593
+ // -------------------------------------------------------------------------
594
+ it('TC-072: messages_fts search returns results after migration', async () => {
595
+ const now = Math.floor(Date.now() / 1000);
596
+ createLegacySignaldockDb(
597
+ dirs.projectRoot,
598
+ [],
599
+ [
600
+ {
601
+ id: 'fts-msg-1',
602
+ conversation_id: 'fts-conv-1',
603
+ from_agent_id: 'fa1',
604
+ to_agent_id: 'fa2',
605
+ content: 'searchable migration content',
606
+ created_at: now,
607
+ },
608
+ ],
609
+ [{ id: 'fts-conv-1', participants: '["fa1","fa2"]', created_at: now, updated_at: now }],
610
+ );
611
+ createGlobalSignaldockDb(dirs.home, []);
612
+
613
+ const result = await runMigration(dirs.projectRoot, dirs.home);
614
+ expect(result.status).toBe('migrated');
615
+
616
+ const conduitDb = new DatabaseSync(join(dirs.projectRoot, '.cleo', 'conduit.db'));
617
+ const ftsResults = conduitDb
618
+ .prepare("SELECT rowid FROM messages_fts WHERE messages_fts MATCH 'migration'")
619
+ .all() as Array<{ rowid: number }>;
620
+ expect(ftsResults.length).toBeGreaterThan(0);
621
+ conduitDb.close();
622
+ });
623
+
624
+ // -------------------------------------------------------------------------
625
+ // TC-068: Broken legacy integrity_check
626
+ // -------------------------------------------------------------------------
627
+ it('TC-068: corrupt legacy DB aborts; no conduit.db; no .pre-t310.bak', async () => {
628
+ // Write a non-SQLite file as legacy DB
629
+ const legacyPath = join(dirs.projectRoot, '.cleo', 'signaldock.db');
630
+ writeFileSync(legacyPath, 'CORRUPTED DATA NOT A VALID SQLITE FILE AT ALL!!!');
631
+ createGlobalSignaldockDb(dirs.home, []);
632
+
633
+ const result = await runMigration(dirs.projectRoot, dirs.home);
634
+
635
+ expect(result.status).toBe('failed');
636
+ expect(result.errors.length).toBeGreaterThan(0);
637
+ // No conduit.db created
638
+ expect(existsSync(join(dirs.projectRoot, '.cleo', 'conduit.db'))).toBe(false);
639
+ // No .pre-t310.bak created
640
+ expect(existsSync(legacyPath + '.pre-t310.bak')).toBe(false);
641
+ });
642
+
643
+ // -------------------------------------------------------------------------
644
+ // TC-067: Idempotent re-run
645
+ // -------------------------------------------------------------------------
646
+ it('TC-067: second migration call is a no-op', async () => {
647
+ createLegacySignaldockDb(dirs.projectRoot, []);
648
+ createGlobalSignaldockDb(dirs.home, []);
649
+
650
+ // First run
651
+ const result1 = await runMigration(dirs.projectRoot, dirs.home);
652
+ expect(result1.status).toBe('migrated');
653
+
654
+ // Second run — conduit.db now exists
655
+ const result2 = await runMigration(dirs.projectRoot, dirs.home);
656
+ expect(result2.status).toBe('no-op');
657
+ expect(result2.errors).toHaveLength(0);
658
+ });
659
+
660
+ // -------------------------------------------------------------------------
661
+ // Scenario 9: .pre-t310.bak alongside conduit.db
662
+ // -------------------------------------------------------------------------
663
+ it('needsMigration returns false when conduit.db exists alongside .pre-t310.bak', async () => {
664
+ writeFileSync(join(dirs.projectRoot, '.cleo', 'conduit.db'), '');
665
+ writeFileSync(join(dirs.projectRoot, '.cleo', 'signaldock.db.pre-t310.bak'), '');
666
+
667
+ const result = await getNeedsMigration(dirs.projectRoot, dirs.home);
668
+ expect(result).toBe(false);
669
+ });
670
+
671
+ // -------------------------------------------------------------------------
672
+ // TC-073: .pre-t310.bak preserved (not deleted)
673
+ // -------------------------------------------------------------------------
674
+ it('TC-073: legacy file renamed to .pre-t310.bak and preserved', async () => {
675
+ createLegacySignaldockDb(dirs.projectRoot, []);
676
+ createGlobalSignaldockDb(dirs.home, []);
677
+
678
+ const result = await runMigration(dirs.projectRoot, dirs.home);
679
+ expect(result.status).toBe('migrated');
680
+
681
+ const bakPath = join(dirs.projectRoot, '.cleo', 'signaldock.db.pre-t310.bak');
682
+ expect(existsSync(bakPath)).toBe(true);
683
+ expect(existsSync(join(dirs.projectRoot, '.cleo', 'signaldock.db'))).toBe(false);
684
+ });
685
+
686
+ // -------------------------------------------------------------------------
687
+ // TC-090: Migrated agents have requires_reauth=1
688
+ // -------------------------------------------------------------------------
689
+ it('TC-090: migrated agents have requires_reauth=1 in global signaldock.db', async () => {
690
+ const now = Math.floor(Date.now() / 1000);
691
+ createLegacySignaldockDb(dirs.projectRoot, [
692
+ { id: 'r1', agent_id: 'reauth-agent-1', name: 'Reauth1', created_at: now },
693
+ { id: 'r2', agent_id: 'reauth-agent-2', name: 'Reauth2', created_at: now },
694
+ ]);
695
+ createGlobalSignaldockDb(dirs.home, []);
696
+
697
+ const result = await runMigration(dirs.projectRoot, dirs.home);
698
+ expect(result.status).toBe('migrated');
699
+
700
+ const globalDb = new DatabaseSync(join(dirs.home, 'signaldock.db'));
701
+ const agents = globalDb
702
+ .prepare(
703
+ 'SELECT agent_id, requires_reauth FROM agents WHERE agent_id IN (?,?) ORDER BY agent_id',
704
+ )
705
+ .all('reauth-agent-1', 'reauth-agent-2') as Array<{
706
+ agent_id: string;
707
+ requires_reauth: number;
708
+ }>;
709
+ expect(agents).toHaveLength(2);
710
+ for (const agent of agents) {
711
+ expect(agent.requires_reauth).toBe(1);
712
+ }
713
+ globalDb.close();
714
+ });
715
+ });