@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,807 @@
1
+ /**
2
+ * Unit tests for agent-registry-accessor.ts — cross-DB refactor (T355, T310).
3
+ *
4
+ * Covers:
5
+ * - TC-050: lookupAgent returns null for unknown agentId
6
+ * - TC-051: lookupAgent returns agent when project_agent_refs row exists and enabled=1
7
+ * - TC-052: lookupAgent returns null when project_agent_refs row has enabled=0
8
+ * - TC-053: lookupAgent with includeGlobal=true returns agent even without project ref
9
+ * - TC-054: listAgentsForProject returns only project-attached agents by default
10
+ * - TC-055: listAgentsForProject with includeGlobal=true returns all global agents
11
+ * - TC-056: createProjectAgent writes to global signaldock.db AND creates project_agent_refs row
12
+ * - TC-057: AgentRegistryAccessor.remove() detaches from project; global row untouched
13
+ * - TC-058: AgentRegistryAccessor.removeGlobal() deletes global agents row
14
+ * - TC-059: AgentRegistryAccessor.markUsed() updates last_used_at in both DBs
15
+ *
16
+ * Additional tests:
17
+ * - lookupAgent warns on dangling soft-FK (ref exists in conduit but not in global)
18
+ * - createProjectAgent re-enables a previously detached agent
19
+ * - listAgentsForProject with includeDisabled=true includes enabled=0 rows
20
+ * - AgentRegistryAccessor.list() returns project-scoped agents only
21
+ * - AgentRegistryAccessor.listGlobal() returns all global agents
22
+ * - AgentRegistryAccessor.getActive() returns most-recently-used project agent
23
+ *
24
+ * All tests use real node:sqlite in tmp directories. The real user's
25
+ * $XDG_DATA_HOME and project directories are never touched.
26
+ *
27
+ * @task T355
28
+ * @epic T310
29
+ */
30
+
31
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
32
+ import { tmpdir } from 'node:os';
33
+ import { join } from 'node:path';
34
+ import { DatabaseSync } from 'node:sqlite';
35
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Fixtures shared across tests
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /** Minimal valid AgentCredential spec for test agents. */
42
+ const BASE_SPEC = {
43
+ agentId: 'test-agent-alpha',
44
+ displayName: 'Test Agent Alpha',
45
+ apiKey: 'sk_live_test_alpha',
46
+ apiBaseUrl: 'https://api.signaldock.io',
47
+ privacyTier: 'public' as const,
48
+ capabilities: ['chat'],
49
+ skills: ['coding'],
50
+ transportType: 'http' as const,
51
+ transportConfig: {},
52
+ isActive: true,
53
+ };
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Helpers
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Create an isolated tmp directory pair:
61
+ * - cleoHome: simulates $XDG_DATA_HOME/cleo (global tier)
62
+ * - projectRoot: simulates a project directory (project tier)
63
+ *
64
+ * Returns helpers to open both databases directly for assertions.
65
+ */
66
+ function makeTmpEnv(suffix: string): {
67
+ cleoHome: string;
68
+ projectRoot: string;
69
+ openGlobal: () => DatabaseSync;
70
+ openConduit: () => DatabaseSync;
71
+ cleanup: () => void;
72
+ } {
73
+ const base = mkdtempSync(join(tmpdir(), `cleo-t355-${suffix}-`));
74
+ const cleoHome = join(base, 'cleo-home');
75
+ const projectRoot = join(base, 'project');
76
+
77
+ mkdirSync(cleoHome, { recursive: true });
78
+ mkdirSync(join(projectRoot, '.cleo'), { recursive: true });
79
+
80
+ const openGlobal = (): DatabaseSync => {
81
+ const db = new DatabaseSync(join(cleoHome, 'signaldock.db'));
82
+ db.exec('PRAGMA foreign_keys = ON');
83
+ db.exec('PRAGMA journal_mode = WAL');
84
+ return db;
85
+ };
86
+
87
+ const openConduit = (): DatabaseSync => {
88
+ const db = new DatabaseSync(join(projectRoot, '.cleo', 'conduit.db'));
89
+ db.exec('PRAGMA foreign_keys = ON');
90
+ db.exec('PRAGMA journal_mode = WAL');
91
+ return db;
92
+ };
93
+
94
+ const cleanup = (): void => {
95
+ rmSync(base, { recursive: true, force: true });
96
+ };
97
+
98
+ return { cleoHome, projectRoot, openGlobal, openConduit, cleanup };
99
+ }
100
+
101
+ /**
102
+ * Bootstrap both databases (schema only) in the tmp environment.
103
+ * Uses the real ensureGlobalSignaldockDb / ensureConduitDb with mocked paths.
104
+ */
105
+ async function bootstrapDbs(
106
+ cleoHome: string,
107
+ projectRoot: string,
108
+ ): Promise<{
109
+ ensureGlobal: () => Promise<void>;
110
+ ensureConduit: () => void;
111
+ }> {
112
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => cleoHome }));
113
+ // Write a deterministic machine-key and global-salt so KDF is testable
114
+ const machineKey = Buffer.alloc(32, 0xab);
115
+ const globalSalt = Buffer.alloc(32, 0xcd);
116
+ writeFileSync(join(cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
117
+ writeFileSync(join(cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
118
+
119
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
120
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
121
+
122
+ return {
123
+ ensureGlobal: async () => {
124
+ await ensureGlobalSignaldockDb();
125
+ },
126
+ ensureConduit: () => {
127
+ ensureConduitDb(projectRoot);
128
+ closeConduitDb();
129
+ },
130
+ };
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Suite
135
+ // ---------------------------------------------------------------------------
136
+
137
+ describe('agent-registry-accessor (cross-DB T355)', () => {
138
+ let env: ReturnType<typeof makeTmpEnv>;
139
+
140
+ beforeEach(() => {
141
+ vi.resetModules();
142
+ env = makeTmpEnv(`${Math.random().toString(36).slice(2)}`);
143
+ });
144
+
145
+ afterEach(() => {
146
+ vi.restoreAllMocks();
147
+ env.cleanup();
148
+ });
149
+
150
+ // -------------------------------------------------------------------------
151
+ // TC-050: lookupAgent returns null for unknown agentId
152
+ // -------------------------------------------------------------------------
153
+
154
+ it('TC-050: lookupAgent returns null for unknown agentId', async () => {
155
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
156
+ const machineKey = Buffer.alloc(32, 0xab);
157
+ const globalSalt = Buffer.alloc(32, 0xcd);
158
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
159
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
160
+
161
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
162
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
163
+ const { lookupAgent } = await import('../agent-registry-accessor.js');
164
+
165
+ await ensureGlobalSignaldockDb();
166
+ ensureConduitDb(env.projectRoot);
167
+ closeConduitDb();
168
+
169
+ const result = lookupAgent(env.projectRoot, 'nonexistent-agent');
170
+ expect(result).toBeNull();
171
+ });
172
+
173
+ // -------------------------------------------------------------------------
174
+ // TC-051: lookupAgent returns agent when project_agent_refs row exists enabled=1
175
+ // -------------------------------------------------------------------------
176
+
177
+ it('TC-051: lookupAgent returns merged agent when ref exists with enabled=1', async () => {
178
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
179
+ const machineKey = Buffer.alloc(32, 0xab);
180
+ const globalSalt = Buffer.alloc(32, 0xcd);
181
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
182
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
183
+
184
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
185
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
186
+ const { createProjectAgent, lookupAgent } = await import('../agent-registry-accessor.js');
187
+
188
+ await ensureGlobalSignaldockDb();
189
+ ensureConduitDb(env.projectRoot);
190
+ closeConduitDb();
191
+
192
+ // Create agent (writes global + conduit ref)
193
+ const created = createProjectAgent(env.projectRoot, BASE_SPEC);
194
+ expect(created.agentId).toBe(BASE_SPEC.agentId);
195
+ expect(created.projectRef).not.toBeNull();
196
+ expect(created.projectRef?.enabled).toBe(1);
197
+
198
+ // Lookup should return the merged record
199
+ const found = lookupAgent(env.projectRoot, BASE_SPEC.agentId);
200
+ expect(found).not.toBeNull();
201
+ expect(found?.agentId).toBe(BASE_SPEC.agentId);
202
+ expect(found?.displayName).toBe(BASE_SPEC.displayName);
203
+ expect(found?.projectRef).not.toBeNull();
204
+ expect(found?.projectRef?.enabled).toBe(1);
205
+ });
206
+
207
+ // -------------------------------------------------------------------------
208
+ // TC-052: lookupAgent returns null when project_agent_refs row has enabled=0
209
+ // -------------------------------------------------------------------------
210
+
211
+ it('TC-052: lookupAgent returns null when project_agent_refs row has enabled=0', async () => {
212
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
213
+ const machineKey = Buffer.alloc(32, 0xab);
214
+ const globalSalt = Buffer.alloc(32, 0xcd);
215
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
216
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
217
+
218
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
219
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
220
+ const { createProjectAgent, lookupAgent } = await import('../agent-registry-accessor.js');
221
+
222
+ await ensureGlobalSignaldockDb();
223
+ ensureConduitDb(env.projectRoot);
224
+ closeConduitDb();
225
+
226
+ createProjectAgent(env.projectRoot, BASE_SPEC);
227
+
228
+ // Manually set enabled=0 in conduit.db
229
+ const conduitDb = env.openConduit();
230
+ conduitDb
231
+ .prepare('UPDATE project_agent_refs SET enabled = 0 WHERE agent_id = ?')
232
+ .run(BASE_SPEC.agentId);
233
+ conduitDb.close();
234
+
235
+ const result = lookupAgent(env.projectRoot, BASE_SPEC.agentId);
236
+ expect(result).toBeNull();
237
+ });
238
+
239
+ // -------------------------------------------------------------------------
240
+ // TC-053: lookupAgent with includeGlobal=true returns agent without project ref
241
+ // -------------------------------------------------------------------------
242
+
243
+ it('TC-053: lookupAgent with includeGlobal=true returns global agent even without project ref', async () => {
244
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
245
+ const machineKey = Buffer.alloc(32, 0xab);
246
+ const globalSalt = Buffer.alloc(32, 0xcd);
247
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
248
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
249
+
250
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
251
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
252
+ const { lookupAgent } = await import('../agent-registry-accessor.js');
253
+
254
+ await ensureGlobalSignaldockDb();
255
+ ensureConduitDb(env.projectRoot);
256
+ closeConduitDb();
257
+
258
+ // Insert directly into global signaldock.db without touching conduit.db
259
+ const globalDb = env.openGlobal();
260
+ const nowTs = Math.floor(Date.now() / 1000);
261
+ globalDb
262
+ .prepare(
263
+ `INSERT INTO agents (id, agent_id, name, class, privacy_tier, capabilities, skills,
264
+ transport_type, api_base_url, classification, transport_config, is_active, status,
265
+ created_at, updated_at, requires_reauth)
266
+ VALUES (?, ?, ?, 'custom', 'public', '[]', '[]', 'http',
267
+ 'https://api.signaldock.io', NULL, '{}', 1, 'online', ?, ?, 0)`,
268
+ )
269
+ .run(crypto.randomUUID(), 'global-only-agent', 'Global Only Agent', nowTs, nowTs);
270
+ globalDb.close();
271
+
272
+ // Default (includeGlobal=false): should return null because no project ref
273
+ const defaultResult = lookupAgent(env.projectRoot, 'global-only-agent');
274
+ expect(defaultResult).toBeNull();
275
+
276
+ // includeGlobal=true: should return the global agent with projectRef=null
277
+ const globalResult = lookupAgent(env.projectRoot, 'global-only-agent', {
278
+ includeGlobal: true,
279
+ });
280
+ expect(globalResult).not.toBeNull();
281
+ expect(globalResult?.agentId).toBe('global-only-agent');
282
+ expect(globalResult?.projectRef).toBeNull();
283
+ });
284
+
285
+ // -------------------------------------------------------------------------
286
+ // TC-054: listAgentsForProject returns only project-attached agents by default
287
+ // -------------------------------------------------------------------------
288
+
289
+ it('TC-054: listAgentsForProject returns only project-attached agents by default', async () => {
290
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
291
+ const machineKey = Buffer.alloc(32, 0xab);
292
+ const globalSalt = Buffer.alloc(32, 0xcd);
293
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
294
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
295
+
296
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
297
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
298
+ const { createProjectAgent, listAgentsForProject } = await import(
299
+ '../agent-registry-accessor.js'
300
+ );
301
+
302
+ await ensureGlobalSignaldockDb();
303
+ ensureConduitDb(env.projectRoot);
304
+ closeConduitDb();
305
+
306
+ // Insert a global-only agent (no project ref)
307
+ const globalDb = env.openGlobal();
308
+ const nowTs = Math.floor(Date.now() / 1000);
309
+ globalDb
310
+ .prepare(
311
+ `INSERT INTO agents (id, agent_id, name, class, privacy_tier, capabilities, skills,
312
+ transport_type, api_base_url, classification, transport_config, is_active, status,
313
+ created_at, updated_at, requires_reauth)
314
+ VALUES (?, ?, ?, 'custom', 'public', '[]', '[]', 'http',
315
+ 'https://api.signaldock.io', NULL, '{}', 1, 'online', ?, ?, 0)`,
316
+ )
317
+ .run(crypto.randomUUID(), 'global-only', 'Global Only', nowTs, nowTs);
318
+ globalDb.close();
319
+
320
+ // Create one project-attached agent
321
+ createProjectAgent(env.projectRoot, BASE_SPEC);
322
+
323
+ const list = listAgentsForProject(env.projectRoot);
324
+ expect(list).toHaveLength(1);
325
+ expect(list[0]?.agentId).toBe(BASE_SPEC.agentId);
326
+ expect(list[0]?.projectRef).not.toBeNull();
327
+ });
328
+
329
+ // -------------------------------------------------------------------------
330
+ // TC-055: listAgentsForProject with includeGlobal=true returns all global agents
331
+ // -------------------------------------------------------------------------
332
+
333
+ it('TC-055: listAgentsForProject with includeGlobal=true returns all global agents', async () => {
334
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
335
+ const machineKey = Buffer.alloc(32, 0xab);
336
+ const globalSalt = Buffer.alloc(32, 0xcd);
337
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
338
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
339
+
340
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
341
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
342
+ const { createProjectAgent, listAgentsForProject } = await import(
343
+ '../agent-registry-accessor.js'
344
+ );
345
+
346
+ await ensureGlobalSignaldockDb();
347
+ ensureConduitDb(env.projectRoot);
348
+ closeConduitDb();
349
+
350
+ // Insert a global-only agent
351
+ const globalDb = env.openGlobal();
352
+ const nowTs = Math.floor(Date.now() / 1000);
353
+ globalDb
354
+ .prepare(
355
+ `INSERT INTO agents (id, agent_id, name, class, privacy_tier, capabilities, skills,
356
+ transport_type, api_base_url, classification, transport_config, is_active, status,
357
+ created_at, updated_at, requires_reauth)
358
+ VALUES (?, ?, ?, 'custom', 'public', '[]', '[]', 'http',
359
+ 'https://api.signaldock.io', NULL, '{}', 1, 'online', ?, ?, 0)`,
360
+ )
361
+ .run(crypto.randomUUID(), 'global-only-2', 'Global Only 2', nowTs, nowTs);
362
+ globalDb.close();
363
+
364
+ // Create one project-attached agent
365
+ createProjectAgent(env.projectRoot, BASE_SPEC);
366
+
367
+ const list = listAgentsForProject(env.projectRoot, { includeGlobal: true });
368
+
369
+ // Should return both agents
370
+ expect(list.length).toBeGreaterThanOrEqual(2);
371
+
372
+ const agentIds = list.map((a) => a.agentId);
373
+ expect(agentIds).toContain(BASE_SPEC.agentId);
374
+ expect(agentIds).toContain('global-only-2');
375
+
376
+ // Project-attached one has projectRef populated; global-only has null
377
+ const attached = list.find((a) => a.agentId === BASE_SPEC.agentId);
378
+ const globalOnly = list.find((a) => a.agentId === 'global-only-2');
379
+ expect(attached?.projectRef).not.toBeNull();
380
+ expect(globalOnly?.projectRef).toBeNull();
381
+ });
382
+
383
+ // -------------------------------------------------------------------------
384
+ // TC-056: createProjectAgent writes to global signaldock.db AND conduit.db
385
+ // -------------------------------------------------------------------------
386
+
387
+ it('TC-056: createProjectAgent writes to global signaldock.db AND project_agent_refs', async () => {
388
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
389
+ const machineKey = Buffer.alloc(32, 0xab);
390
+ const globalSalt = Buffer.alloc(32, 0xcd);
391
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
392
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
393
+
394
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
395
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
396
+ const { createProjectAgent } = await import('../agent-registry-accessor.js');
397
+
398
+ await ensureGlobalSignaldockDb();
399
+ ensureConduitDb(env.projectRoot);
400
+ closeConduitDb();
401
+
402
+ const result = createProjectAgent(env.projectRoot, BASE_SPEC);
403
+
404
+ // Verify return type
405
+ expect(result.agentId).toBe(BASE_SPEC.agentId);
406
+ expect(result.displayName).toBe(BASE_SPEC.displayName);
407
+ expect(result.projectRef).not.toBeNull();
408
+ expect(result.projectRef?.agentId).toBe(BASE_SPEC.agentId);
409
+ expect(result.projectRef?.enabled).toBe(1);
410
+ expect(result.projectRef?.attachedAt).toBeTruthy();
411
+
412
+ // Verify global signaldock.db was written
413
+ const globalDb = env.openGlobal();
414
+ const globalRow = globalDb
415
+ .prepare('SELECT agent_id, name FROM agents WHERE agent_id = ?')
416
+ .get(BASE_SPEC.agentId) as { agent_id: string; name: string } | undefined;
417
+ globalDb.close();
418
+ expect(globalRow).toBeDefined();
419
+ expect(globalRow?.name).toBe(BASE_SPEC.displayName);
420
+
421
+ // Verify conduit.db project_agent_refs was written
422
+ const conduitDb = env.openConduit();
423
+ const refRow = conduitDb
424
+ .prepare('SELECT agent_id, enabled FROM project_agent_refs WHERE agent_id = ?')
425
+ .get(BASE_SPEC.agentId) as { agent_id: string; enabled: number } | undefined;
426
+ conduitDb.close();
427
+ expect(refRow).toBeDefined();
428
+ expect(refRow?.enabled).toBe(1);
429
+ });
430
+
431
+ // -------------------------------------------------------------------------
432
+ // TC-057: AgentRegistryAccessor.remove() detaches from project; global row untouched
433
+ // -------------------------------------------------------------------------
434
+
435
+ it('TC-057: AgentRegistryAccessor.remove() sets project_agent_refs.enabled=0; global row intact', async () => {
436
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
437
+ const machineKey = Buffer.alloc(32, 0xab);
438
+ const globalSalt = Buffer.alloc(32, 0xcd);
439
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
440
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
441
+
442
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
443
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
444
+ const { AgentRegistryAccessor, createProjectAgent } = await import(
445
+ '../agent-registry-accessor.js'
446
+ );
447
+
448
+ await ensureGlobalSignaldockDb();
449
+ ensureConduitDb(env.projectRoot);
450
+ closeConduitDb();
451
+
452
+ createProjectAgent(env.projectRoot, BASE_SPEC);
453
+
454
+ const accessor = new AgentRegistryAccessor(env.projectRoot);
455
+ await accessor.remove(BASE_SPEC.agentId);
456
+
457
+ // project_agent_refs row should be disabled (enabled=0), not deleted
458
+ const conduitDb = env.openConduit();
459
+ const refRow = conduitDb
460
+ .prepare('SELECT agent_id, enabled FROM project_agent_refs WHERE agent_id = ?')
461
+ .get(BASE_SPEC.agentId) as { agent_id: string; enabled: number } | undefined;
462
+ conduitDb.close();
463
+ expect(refRow).toBeDefined();
464
+ expect(refRow?.enabled).toBe(0);
465
+
466
+ // Global signaldock.db row should still exist
467
+ const globalDb = env.openGlobal();
468
+ const globalRow = globalDb
469
+ .prepare('SELECT agent_id FROM agents WHERE agent_id = ?')
470
+ .get(BASE_SPEC.agentId) as { agent_id: string } | undefined;
471
+ globalDb.close();
472
+ expect(globalRow).toBeDefined();
473
+ });
474
+
475
+ // -------------------------------------------------------------------------
476
+ // TC-058: AgentRegistryAccessor.removeGlobal() deletes global agents row
477
+ // -------------------------------------------------------------------------
478
+
479
+ it('TC-058: AgentRegistryAccessor.removeGlobal() deletes row from global signaldock.db', async () => {
480
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
481
+ const machineKey = Buffer.alloc(32, 0xab);
482
+ const globalSalt = Buffer.alloc(32, 0xcd);
483
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
484
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
485
+
486
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
487
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
488
+ const { AgentRegistryAccessor, createProjectAgent } = await import(
489
+ '../agent-registry-accessor.js'
490
+ );
491
+
492
+ await ensureGlobalSignaldockDb();
493
+ ensureConduitDb(env.projectRoot);
494
+ closeConduitDb();
495
+
496
+ createProjectAgent(env.projectRoot, BASE_SPEC);
497
+
498
+ // First detach from project so removeGlobal succeeds without force
499
+ const accessor = new AgentRegistryAccessor(env.projectRoot);
500
+ await accessor.remove(BASE_SPEC.agentId);
501
+
502
+ // Now remove globally (no active project ref → no warning)
503
+ await accessor.removeGlobal(BASE_SPEC.agentId);
504
+
505
+ // Global row should be gone
506
+ const globalDb = env.openGlobal();
507
+ const globalRow = globalDb
508
+ .prepare('SELECT agent_id FROM agents WHERE agent_id = ?')
509
+ .get(BASE_SPEC.agentId) as { agent_id: string } | undefined;
510
+ globalDb.close();
511
+ expect(globalRow).toBeUndefined();
512
+ });
513
+
514
+ // -------------------------------------------------------------------------
515
+ // TC-059: AgentRegistryAccessor.markUsed() updates last_used_at in both DBs
516
+ // -------------------------------------------------------------------------
517
+
518
+ it('TC-059: AgentRegistryAccessor.markUsed() updates last_used_at in both DBs', async () => {
519
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
520
+ const machineKey = Buffer.alloc(32, 0xab);
521
+ const globalSalt = Buffer.alloc(32, 0xcd);
522
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
523
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
524
+
525
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
526
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
527
+ const { AgentRegistryAccessor, createProjectAgent } = await import(
528
+ '../agent-registry-accessor.js'
529
+ );
530
+
531
+ await ensureGlobalSignaldockDb();
532
+ ensureConduitDb(env.projectRoot);
533
+ closeConduitDb();
534
+
535
+ createProjectAgent(env.projectRoot, BASE_SPEC);
536
+
537
+ const before = Date.now();
538
+
539
+ const accessor = new AgentRegistryAccessor(env.projectRoot);
540
+ await accessor.markUsed(BASE_SPEC.agentId);
541
+
542
+ const after = Date.now();
543
+
544
+ // Check global signaldock.db last_used_at was updated (stored as Unix timestamp)
545
+ const globalDb = env.openGlobal();
546
+ const globalRow = globalDb
547
+ .prepare('SELECT last_used_at FROM agents WHERE agent_id = ?')
548
+ .get(BASE_SPEC.agentId) as { last_used_at: number } | undefined;
549
+ globalDb.close();
550
+ expect(globalRow?.last_used_at).toBeDefined();
551
+ expect(globalRow!.last_used_at * 1000).toBeGreaterThanOrEqual(Math.floor(before / 1000) * 1000);
552
+ expect(globalRow!.last_used_at * 1000).toBeLessThanOrEqual(after + 1000);
553
+
554
+ // Check conduit.db project_agent_refs last_used_at was updated (stored as ISO string)
555
+ const conduitDb = env.openConduit();
556
+ const refRow = conduitDb
557
+ .prepare('SELECT last_used_at FROM project_agent_refs WHERE agent_id = ?')
558
+ .get(BASE_SPEC.agentId) as { last_used_at: string | null } | undefined;
559
+ conduitDb.close();
560
+ expect(refRow?.last_used_at).toBeTruthy();
561
+ // Should be a parseable ISO string
562
+ const ts = new Date(refRow!.last_used_at!).getTime();
563
+ expect(ts).toBeGreaterThanOrEqual(before - 1000);
564
+ expect(ts).toBeLessThanOrEqual(after + 1000);
565
+ });
566
+
567
+ // -------------------------------------------------------------------------
568
+ // Additional: dangling soft-FK logs warn and returns null
569
+ // -------------------------------------------------------------------------
570
+
571
+ it('lookupAgent logs warn and returns null for dangling soft-FK', async () => {
572
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
573
+ const machineKey = Buffer.alloc(32, 0xab);
574
+ const globalSalt = Buffer.alloc(32, 0xcd);
575
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
576
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
577
+
578
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
579
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
580
+ const { lookupAgent } = await import('../agent-registry-accessor.js');
581
+
582
+ await ensureGlobalSignaldockDb();
583
+ ensureConduitDb(env.projectRoot);
584
+ closeConduitDb();
585
+
586
+ // Insert a project_agent_refs row without a corresponding global agent
587
+ const conduitDb = env.openConduit();
588
+ conduitDb
589
+ .prepare(
590
+ `INSERT INTO project_agent_refs (agent_id, attached_at, enabled)
591
+ VALUES (?, ?, 1)`,
592
+ )
593
+ .run('dangling-agent', new Date().toISOString());
594
+ conduitDb.close();
595
+
596
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
597
+ const result = lookupAgent(env.projectRoot, 'dangling-agent');
598
+ expect(result).toBeNull();
599
+ expect(warnSpy).toHaveBeenCalledWith(
600
+ expect.stringContaining('dangling project_agent_refs row'),
601
+ );
602
+ warnSpy.mockRestore();
603
+ });
604
+
605
+ // -------------------------------------------------------------------------
606
+ // Additional: createProjectAgent re-enables previously detached agent
607
+ // -------------------------------------------------------------------------
608
+
609
+ it('createProjectAgent re-enables a previously detached (enabled=0) project ref', async () => {
610
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
611
+ const machineKey = Buffer.alloc(32, 0xab);
612
+ const globalSalt = Buffer.alloc(32, 0xcd);
613
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
614
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
615
+
616
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
617
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
618
+ const { AgentRegistryAccessor, createProjectAgent, lookupAgent } = await import(
619
+ '../agent-registry-accessor.js'
620
+ );
621
+
622
+ await ensureGlobalSignaldockDb();
623
+ ensureConduitDb(env.projectRoot);
624
+ closeConduitDb();
625
+
626
+ // Create and then detach
627
+ createProjectAgent(env.projectRoot, BASE_SPEC);
628
+ const accessor = new AgentRegistryAccessor(env.projectRoot);
629
+ await accessor.remove(BASE_SPEC.agentId);
630
+
631
+ // Confirm detached
632
+ const afterRemove = lookupAgent(env.projectRoot, BASE_SPEC.agentId);
633
+ expect(afterRemove).toBeNull();
634
+
635
+ // Re-create should re-enable
636
+ createProjectAgent(env.projectRoot, BASE_SPEC);
637
+
638
+ const afterReCreate = lookupAgent(env.projectRoot, BASE_SPEC.agentId);
639
+ expect(afterReCreate).not.toBeNull();
640
+ expect(afterReCreate?.projectRef?.enabled).toBe(1);
641
+
642
+ // Verify no duplicate rows in conduit.db
643
+ const conduitDb = env.openConduit();
644
+ const rows = conduitDb
645
+ .prepare('SELECT agent_id FROM project_agent_refs WHERE agent_id = ?')
646
+ .all(BASE_SPEC.agentId) as Array<{ agent_id: string }>;
647
+ conduitDb.close();
648
+ expect(rows).toHaveLength(1);
649
+ });
650
+
651
+ // -------------------------------------------------------------------------
652
+ // Additional: listAgentsForProject with includeDisabled=true
653
+ // -------------------------------------------------------------------------
654
+
655
+ it('listAgentsForProject with includeDisabled=true includes enabled=0 rows', async () => {
656
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
657
+ const machineKey = Buffer.alloc(32, 0xab);
658
+ const globalSalt = Buffer.alloc(32, 0xcd);
659
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
660
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
661
+
662
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
663
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
664
+ const { AgentRegistryAccessor, createProjectAgent, listAgentsForProject } = await import(
665
+ '../agent-registry-accessor.js'
666
+ );
667
+
668
+ await ensureGlobalSignaldockDb();
669
+ ensureConduitDb(env.projectRoot);
670
+ closeConduitDb();
671
+
672
+ createProjectAgent(env.projectRoot, BASE_SPEC);
673
+
674
+ // Detach the agent
675
+ const accessor = new AgentRegistryAccessor(env.projectRoot);
676
+ await accessor.remove(BASE_SPEC.agentId);
677
+
678
+ // Default: should not include disabled
679
+ const defaultList = listAgentsForProject(env.projectRoot);
680
+ expect(defaultList).toHaveLength(0);
681
+
682
+ // includeDisabled=true: should include disabled
683
+ const disabledList = listAgentsForProject(env.projectRoot, { includeDisabled: true });
684
+ expect(disabledList).toHaveLength(1);
685
+ expect(disabledList[0]?.agentId).toBe(BASE_SPEC.agentId);
686
+ expect(disabledList[0]?.projectRef?.enabled).toBe(0);
687
+ });
688
+
689
+ // -------------------------------------------------------------------------
690
+ // Additional: AgentRegistryAccessor.list() is project-scoped only
691
+ // -------------------------------------------------------------------------
692
+
693
+ it('AgentRegistryAccessor.list() returns only project-attached agents', async () => {
694
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
695
+ const machineKey = Buffer.alloc(32, 0xab);
696
+ const globalSalt = Buffer.alloc(32, 0xcd);
697
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
698
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
699
+
700
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
701
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
702
+ const { AgentRegistryAccessor, createProjectAgent } = await import(
703
+ '../agent-registry-accessor.js'
704
+ );
705
+
706
+ await ensureGlobalSignaldockDb();
707
+ ensureConduitDb(env.projectRoot);
708
+ closeConduitDb();
709
+
710
+ // Add global-only agent
711
+ const globalDb = env.openGlobal();
712
+ const nowTs = Math.floor(Date.now() / 1000);
713
+ globalDb
714
+ .prepare(
715
+ `INSERT INTO agents (id, agent_id, name, class, privacy_tier, capabilities, skills,
716
+ transport_type, api_base_url, classification, transport_config, is_active, status,
717
+ created_at, updated_at, requires_reauth)
718
+ VALUES (?, ?, ?, 'custom', 'public', '[]', '[]', 'http',
719
+ 'https://api.signaldock.io', NULL, '{}', 1, 'online', ?, ?, 0)`,
720
+ )
721
+ .run(crypto.randomUUID(), 'global-only-list', 'Global Only List', nowTs, nowTs);
722
+ globalDb.close();
723
+
724
+ createProjectAgent(env.projectRoot, BASE_SPEC);
725
+
726
+ const accessor = new AgentRegistryAccessor(env.projectRoot);
727
+ const list = await accessor.list();
728
+ expect(list).toHaveLength(1);
729
+ expect(list[0]?.agentId).toBe(BASE_SPEC.agentId);
730
+ });
731
+
732
+ // -------------------------------------------------------------------------
733
+ // Additional: AgentRegistryAccessor.listGlobal() returns all global agents
734
+ // -------------------------------------------------------------------------
735
+
736
+ it('AgentRegistryAccessor.listGlobal() returns all global agents', async () => {
737
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
738
+ const machineKey = Buffer.alloc(32, 0xab);
739
+ const globalSalt = Buffer.alloc(32, 0xcd);
740
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
741
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
742
+
743
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
744
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
745
+ const { AgentRegistryAccessor, createProjectAgent } = await import(
746
+ '../agent-registry-accessor.js'
747
+ );
748
+
749
+ await ensureGlobalSignaldockDb();
750
+ ensureConduitDb(env.projectRoot);
751
+ closeConduitDb();
752
+
753
+ // Add global-only agent
754
+ const globalDb = env.openGlobal();
755
+ const nowTs = Math.floor(Date.now() / 1000);
756
+ globalDb
757
+ .prepare(
758
+ `INSERT INTO agents (id, agent_id, name, class, privacy_tier, capabilities, skills,
759
+ transport_type, api_base_url, classification, transport_config, is_active, status,
760
+ created_at, updated_at, requires_reauth)
761
+ VALUES (?, ?, ?, 'custom', 'public', '[]', '[]', 'http',
762
+ 'https://api.signaldock.io', NULL, '{}', 1, 'online', ?, ?, 0)`,
763
+ )
764
+ .run(crypto.randomUUID(), 'global-only-listg', 'Global Only ListG', nowTs, nowTs);
765
+ globalDb.close();
766
+
767
+ createProjectAgent(env.projectRoot, BASE_SPEC);
768
+
769
+ const accessor = new AgentRegistryAccessor(env.projectRoot);
770
+ const list = await accessor.listGlobal();
771
+ const agentIds = list.map((a) => a.agentId);
772
+ expect(agentIds).toContain(BASE_SPEC.agentId);
773
+ expect(agentIds).toContain('global-only-listg');
774
+ });
775
+
776
+ // -------------------------------------------------------------------------
777
+ // Additional: removeGlobal throws when active project ref exists (no force)
778
+ // -------------------------------------------------------------------------
779
+
780
+ it('AgentRegistryAccessor.removeGlobal() throws when active project ref exists without force', async () => {
781
+ vi.doMock('../../paths.js', () => ({ getCleoHome: () => env.cleoHome }));
782
+ const machineKey = Buffer.alloc(32, 0xab);
783
+ const globalSalt = Buffer.alloc(32, 0xcd);
784
+ writeFileSync(join(env.cleoHome, 'machine-key'), machineKey, { mode: 0o600 });
785
+ writeFileSync(join(env.cleoHome, 'global-salt'), globalSalt, { mode: 0o600 });
786
+
787
+ const { ensureGlobalSignaldockDb } = await import('../signaldock-sqlite.js');
788
+ const { ensureConduitDb, closeConduitDb } = await import('../conduit-sqlite.js');
789
+ const { AgentRegistryAccessor, createProjectAgent } = await import(
790
+ '../agent-registry-accessor.js'
791
+ );
792
+
793
+ await ensureGlobalSignaldockDb();
794
+ ensureConduitDb(env.projectRoot);
795
+ closeConduitDb();
796
+
797
+ createProjectAgent(env.projectRoot, BASE_SPEC);
798
+
799
+ const accessor = new AgentRegistryAccessor(env.projectRoot);
800
+ await expect(accessor.removeGlobal(BASE_SPEC.agentId)).rejects.toThrow(
801
+ /still has project references/,
802
+ );
803
+
804
+ // With force=true: should succeed
805
+ await expect(accessor.removeGlobal(BASE_SPEC.agentId, { force: true })).resolves.not.toThrow();
806
+ });
807
+ });