@cleocode/core 2026.4.11 → 2026.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codebase-map/analyzers/architecture.d.ts.map +1 -1
- package/dist/codebase-map/analyzers/architecture.js +0 -1
- package/dist/codebase-map/analyzers/architecture.js.map +1 -1
- package/dist/conduit/local-transport.d.ts +18 -8
- package/dist/conduit/local-transport.d.ts.map +1 -1
- package/dist/conduit/local-transport.js +23 -13
- package/dist/conduit/local-transport.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -1
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.js +175 -68950
- package/dist/index.js.map +1 -7
- package/dist/init.d.ts +1 -2
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1 -2
- package/dist/init.js.map +1 -1
- package/dist/internal.d.ts +8 -3
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +13 -6
- package/dist/internal.js.map +1 -1
- package/dist/memory/learnings.d.ts +2 -2
- package/dist/memory/patterns.d.ts +6 -6
- package/dist/output.d.ts +32 -11
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +67 -67
- package/dist/output.js.map +1 -1
- package/dist/paths.js +80 -14
- package/dist/paths.js.map +1 -1
- package/dist/skills/dynamic-skill-generator.d.ts +0 -2
- package/dist/skills/dynamic-skill-generator.d.ts.map +1 -1
- package/dist/skills/dynamic-skill-generator.js.map +1 -1
- package/dist/store/agent-registry-accessor.d.ts +203 -12
- package/dist/store/agent-registry-accessor.d.ts.map +1 -1
- package/dist/store/agent-registry-accessor.js +618 -100
- package/dist/store/agent-registry-accessor.js.map +1 -1
- package/dist/store/api-key-kdf.d.ts +73 -0
- package/dist/store/api-key-kdf.d.ts.map +1 -0
- package/dist/store/api-key-kdf.js +84 -0
- package/dist/store/api-key-kdf.js.map +1 -0
- package/dist/store/cleanup-legacy.js +171 -0
- package/dist/store/cleanup-legacy.js.map +1 -0
- package/dist/store/conduit-sqlite.d.ts +184 -0
- package/dist/store/conduit-sqlite.d.ts.map +1 -0
- package/dist/store/conduit-sqlite.js +570 -0
- package/dist/store/conduit-sqlite.js.map +1 -0
- package/dist/store/global-salt.d.ts +78 -0
- package/dist/store/global-salt.d.ts.map +1 -0
- package/dist/store/global-salt.js +147 -0
- package/dist/store/global-salt.js.map +1 -0
- package/dist/store/migrate-signaldock-to-conduit.d.ts +81 -0
- package/dist/store/migrate-signaldock-to-conduit.d.ts.map +1 -0
- package/dist/store/migrate-signaldock-to-conduit.js +555 -0
- package/dist/store/migrate-signaldock-to-conduit.js.map +1 -0
- package/dist/store/nexus-sqlite.js +28 -3
- package/dist/store/nexus-sqlite.js.map +1 -1
- package/dist/store/signaldock-sqlite.d.ts +122 -19
- package/dist/store/signaldock-sqlite.d.ts.map +1 -1
- package/dist/store/signaldock-sqlite.js +401 -251
- package/dist/store/signaldock-sqlite.js.map +1 -1
- package/dist/store/sqlite-backup.js +122 -4
- package/dist/store/sqlite-backup.js.map +1 -1
- package/dist/system/backup.d.ts +0 -26
- package/dist/system/backup.d.ts.map +1 -1
- package/dist/system/runtime.d.ts +0 -2
- package/dist/system/runtime.d.ts.map +1 -1
- package/dist/system/runtime.js +3 -3
- package/dist/system/runtime.js.map +1 -1
- package/dist/tasks/add.d.ts +1 -1
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/add.js +98 -23
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +4 -1
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/find.d.ts.map +1 -1
- package/dist/tasks/find.js +4 -1
- package/dist/tasks/find.js.map +1 -1
- package/dist/tasks/labels.d.ts.map +1 -1
- package/dist/tasks/labels.js +4 -1
- package/dist/tasks/labels.js.map +1 -1
- package/dist/tasks/relates.d.ts.map +1 -1
- package/dist/tasks/relates.js +16 -4
- package/dist/tasks/relates.js.map +1 -1
- package/dist/tasks/show.d.ts.map +1 -1
- package/dist/tasks/show.js +4 -1
- package/dist/tasks/show.js.map +1 -1
- package/dist/tasks/update.d.ts.map +1 -1
- package/dist/tasks/update.js +32 -6
- package/dist/tasks/update.js.map +1 -1
- package/dist/validation/engine.d.ts.map +1 -1
- package/dist/validation/engine.js +16 -4
- package/dist/validation/engine.js.map +1 -1
- package/dist/validation/param-utils.d.ts +5 -3
- package/dist/validation/param-utils.d.ts.map +1 -1
- package/dist/validation/param-utils.js +8 -6
- package/dist/validation/param-utils.js.map +1 -1
- package/dist/validation/protocols/_shared.d.ts.map +1 -1
- package/dist/validation/protocols/_shared.js +13 -6
- package/dist/validation/protocols/_shared.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/__tests__/manager.test.ts +0 -1
- package/src/codebase-map/analyzers/architecture.ts +0 -1
- package/src/conduit/__tests__/local-credential-flow.test.ts +20 -18
- package/src/conduit/__tests__/local-transport.test.ts +14 -12
- package/src/conduit/local-transport.ts +23 -13
- package/src/config.ts +0 -1
- package/src/errors.ts +24 -0
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +2 -5
- package/src/init.ts +1 -2
- package/src/internal.ts +49 -2
- package/src/lifecycle/cant/lifecycle-rcasd.cant +133 -0
- package/src/memory/__tests__/engine-compat.test.ts +2 -2
- package/src/memory/__tests__/pipeline-manifest-sqlite.test.ts +4 -4
- package/src/observability/__tests__/index.test.ts +4 -4
- package/src/observability/__tests__/log-filter.test.ts +4 -4
- package/src/output.ts +73 -75
- package/src/sessions/__tests__/session-grade.integration.test.ts +1 -1
- package/src/sessions/__tests__/session-grade.test.ts +2 -2
- package/src/skills/__tests__/dynamic-skill-generator.test.ts +0 -2
- package/src/skills/dynamic-skill-generator.ts +0 -2
- package/src/store/__tests__/agent-registry-accessor.test.ts +807 -0
- package/src/store/__tests__/api-key-kdf.test.ts +113 -0
- package/src/store/__tests__/conduit-sqlite.test.ts +413 -0
- package/src/store/__tests__/global-salt.test.ts +195 -0
- package/src/store/__tests__/migrate-signaldock-to-conduit.test.ts +715 -0
- package/src/store/__tests__/signaldock-sqlite.test.ts +652 -0
- package/src/store/__tests__/sqlite-backup-global.test.ts +307 -3
- package/src/store/__tests__/sqlite-backup.test.ts +5 -1
- package/src/store/__tests__/t310-integration.test.ts +1150 -0
- package/src/store/agent-registry-accessor.ts +847 -140
- package/src/store/api-key-kdf.ts +104 -0
- package/src/store/conduit-sqlite.ts +655 -0
- package/src/store/global-salt.ts +175 -0
- package/src/store/migrate-signaldock-to-conduit.ts +669 -0
- package/src/store/signaldock-sqlite.ts +431 -254
- package/src/store/sqlite-backup.ts +185 -10
- package/src/system/backup.ts +2 -62
- package/src/system/runtime.ts +4 -6
- package/src/tasks/__tests__/error-hints.test.ts +256 -0
- package/src/tasks/add.ts +99 -9
- package/src/tasks/complete.ts +4 -1
- package/src/tasks/find.ts +4 -1
- package/src/tasks/labels.ts +4 -1
- package/src/tasks/relates.ts +16 -4
- package/src/tasks/show.ts +4 -1
- package/src/tasks/update.ts +32 -3
- package/src/validation/__tests__/error-hints.test.ts +97 -0
- package/src/validation/engine.ts +16 -1
- package/src/validation/param-utils.ts +10 -7
- package/src/validation/protocols/_shared.ts +14 -6
- package/src/validation/protocols/cant/architecture-decision.cant +80 -0
- package/src/validation/protocols/cant/artifact-publish.cant +95 -0
- package/src/validation/protocols/cant/consensus.cant +74 -0
- package/src/validation/protocols/cant/contribution.cant +82 -0
- package/src/validation/protocols/cant/decomposition.cant +92 -0
- package/src/validation/protocols/cant/implementation.cant +67 -0
- package/src/validation/protocols/cant/provenance.cant +88 -0
- package/src/validation/protocols/cant/release.cant +96 -0
- package/src/validation/protocols/cant/research.cant +66 -0
- package/src/validation/protocols/cant/specification.cant +67 -0
- package/src/validation/protocols/cant/testing.cant +88 -0
- package/src/validation/protocols/cant/validation.cant +65 -0
- package/src/validation/protocols/protocols-markdown/decomposition.md +0 -4
- package/templates/config.template.json +0 -1
- 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
|
+
});
|