@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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for api-key-kdf.ts (T349).
|
|
3
|
+
*
|
|
4
|
+
* @task T349
|
|
5
|
+
* @epic T310
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHmac, randomBytes } from 'node:crypto';
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { deriveApiKey, deriveLegacyProjectKey } from '../api-key-kdf.js';
|
|
11
|
+
|
|
12
|
+
describe('api-key-kdf', () => {
|
|
13
|
+
const machineKey = Buffer.from(
|
|
14
|
+
'0000000000000000000000000000000000000000000000000000000000000001',
|
|
15
|
+
'hex',
|
|
16
|
+
);
|
|
17
|
+
const globalSalt = Buffer.from(
|
|
18
|
+
'1111111111111111111111111111111111111111111111111111111111111111',
|
|
19
|
+
'hex',
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
describe('deriveApiKey', () => {
|
|
23
|
+
it('returns a 32-byte Buffer', () => {
|
|
24
|
+
const key = deriveApiKey({ machineKey, globalSalt, agentId: 'agent-1' });
|
|
25
|
+
expect(key).toBeInstanceOf(Buffer);
|
|
26
|
+
expect(key.length).toBe(32);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('is deterministic for identical inputs', () => {
|
|
30
|
+
const k1 = deriveApiKey({ machineKey, globalSalt, agentId: 'agent-1' });
|
|
31
|
+
const k2 = deriveApiKey({ machineKey, globalSalt, agentId: 'agent-1' });
|
|
32
|
+
expect(k2.equals(k1)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('produces different keys for different agentIds', () => {
|
|
36
|
+
const k1 = deriveApiKey({ machineKey, globalSalt, agentId: 'agent-1' });
|
|
37
|
+
const k2 = deriveApiKey({ machineKey, globalSalt, agentId: 'agent-2' });
|
|
38
|
+
expect(k2.equals(k1)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('produces different keys for different machineKeys', () => {
|
|
42
|
+
const otherMachine = randomBytes(32);
|
|
43
|
+
const k1 = deriveApiKey({ machineKey, globalSalt, agentId: 'agent-1' });
|
|
44
|
+
const k2 = deriveApiKey({ machineKey: otherMachine, globalSalt, agentId: 'agent-1' });
|
|
45
|
+
expect(k2.equals(k1)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('produces different keys for different globalSalts', () => {
|
|
49
|
+
const otherSalt = randomBytes(32);
|
|
50
|
+
const k1 = deriveApiKey({ machineKey, globalSalt, agentId: 'agent-1' });
|
|
51
|
+
const k2 = deriveApiKey({ machineKey, globalSalt: otherSalt, agentId: 'agent-1' });
|
|
52
|
+
expect(k2.equals(k1)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('throws on empty machineKey', () => {
|
|
56
|
+
expect(() =>
|
|
57
|
+
deriveApiKey({ machineKey: Buffer.alloc(0), globalSalt, agentId: 'agent-1' }),
|
|
58
|
+
).toThrow(/machineKey/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('throws on globalSalt of wrong size', () => {
|
|
62
|
+
expect(() =>
|
|
63
|
+
deriveApiKey({ machineKey, globalSalt: Buffer.alloc(16), agentId: 'agent-1' }),
|
|
64
|
+
).toThrow(/globalSalt.*32/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('throws on empty agentId', () => {
|
|
68
|
+
expect(() => deriveApiKey({ machineKey, globalSalt, agentId: '' })).toThrow(/agentId/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('uses HMAC-SHA256 primitive (compatibility with external crypto libs)', () => {
|
|
72
|
+
const key = Buffer.concat([machineKey, globalSalt]);
|
|
73
|
+
const expected = createHmac('sha256', key).update('agent-1', 'utf8').digest();
|
|
74
|
+
const actual = deriveApiKey({ machineKey, globalSalt, agentId: 'agent-1' });
|
|
75
|
+
expect(actual.equals(expected)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('deriveLegacyProjectKey', () => {
|
|
80
|
+
it('returns a 32-byte Buffer', () => {
|
|
81
|
+
const key = deriveLegacyProjectKey(machineKey, '/home/user/project');
|
|
82
|
+
expect(key).toBeInstanceOf(Buffer);
|
|
83
|
+
expect(key.length).toBe(32);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('is deterministic', () => {
|
|
87
|
+
const k1 = deriveLegacyProjectKey(machineKey, '/p');
|
|
88
|
+
const k2 = deriveLegacyProjectKey(machineKey, '/p');
|
|
89
|
+
expect(k2.equals(k1)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('produces different keys for different project paths', () => {
|
|
93
|
+
const k1 = deriveLegacyProjectKey(machineKey, '/project-a');
|
|
94
|
+
const k2 = deriveLegacyProjectKey(machineKey, '/project-b');
|
|
95
|
+
expect(k2.equals(k1)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('differs from new KDF for same machineKey + agentId', () => {
|
|
99
|
+
const legacy = deriveLegacyProjectKey(machineKey, 'agent-1');
|
|
100
|
+
const modern = deriveApiKey({ machineKey, globalSalt, agentId: 'agent-1' });
|
|
101
|
+
// Would be a bug if identical — legacy has no salt
|
|
102
|
+
expect(modern.equals(legacy)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('throws on empty machineKey', () => {
|
|
106
|
+
expect(() => deriveLegacyProjectKey(Buffer.alloc(0), '/p')).toThrow(/machineKey/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('throws on empty projectPath', () => {
|
|
110
|
+
expect(() => deriveLegacyProjectKey(machineKey, '')).toThrow(/projectPath/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for conduit-sqlite.ts — project-tier conduit.db module.
|
|
3
|
+
*
|
|
4
|
+
* Covers path resolution, DDL creation, idempotent re-open, FTS5 triggers,
|
|
5
|
+
* getConduitNativeDb, and integrity_check.
|
|
6
|
+
*
|
|
7
|
+
* Uses real node:sqlite DatabaseSync (genuine SQLite operations, not mocks).
|
|
8
|
+
* All filesystem interactions occur in tmp directories; the real user's
|
|
9
|
+
* project root is never touched.
|
|
10
|
+
*
|
|
11
|
+
* @task T344
|
|
12
|
+
* @epic T310
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
20
|
+
import {
|
|
21
|
+
applyConduitSchema,
|
|
22
|
+
attachAgentToProject,
|
|
23
|
+
CONDUIT_DB_FILENAME,
|
|
24
|
+
CONDUIT_SCHEMA_VERSION,
|
|
25
|
+
checkConduitDbHealth,
|
|
26
|
+
closeConduitDb,
|
|
27
|
+
detachAgentFromProject,
|
|
28
|
+
ensureConduitDb,
|
|
29
|
+
getConduitDbPath,
|
|
30
|
+
getConduitNativeDb,
|
|
31
|
+
getProjectAgentRef,
|
|
32
|
+
listProjectAgentRefs,
|
|
33
|
+
updateProjectAgentLastUsed,
|
|
34
|
+
} from '../conduit-sqlite.js';
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/** Expected project-local messaging + tracking table names in conduit.db. */
|
|
41
|
+
const EXPECTED_TABLES = [
|
|
42
|
+
'_conduit_meta',
|
|
43
|
+
'_conduit_migrations',
|
|
44
|
+
'attachment_approvals',
|
|
45
|
+
'attachment_contributors',
|
|
46
|
+
'attachment_versions',
|
|
47
|
+
'attachments',
|
|
48
|
+
'conversations',
|
|
49
|
+
'dead_letters',
|
|
50
|
+
'delivery_jobs',
|
|
51
|
+
'message_pins',
|
|
52
|
+
'messages',
|
|
53
|
+
'project_agent_refs',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Suite
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
describe('conduit-sqlite', () => {
|
|
61
|
+
let tmpRoot: string;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'cleo-t344-'));
|
|
65
|
+
// Ensure each test starts with a clean singleton.
|
|
66
|
+
closeConduitDb();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
closeConduitDb();
|
|
71
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// TC-001 — path resolution
|
|
75
|
+
it('getConduitDbPath resolves to <projectRoot>/.cleo/conduit.db', () => {
|
|
76
|
+
const expected = join(tmpRoot, '.cleo', CONDUIT_DB_FILENAME);
|
|
77
|
+
expect(getConduitDbPath(tmpRoot)).toBe(expected);
|
|
78
|
+
expect(getConduitDbPath(tmpRoot)).toMatch(/\.cleo[/\\]conduit\.db$/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// TC-002 — fresh install creates file + directory
|
|
82
|
+
it('ensureConduitDb creates .cleo/ dir and conduit.db on a fresh project root', () => {
|
|
83
|
+
expect(existsSync(join(tmpRoot, '.cleo'))).toBe(false);
|
|
84
|
+
|
|
85
|
+
const result = ensureConduitDb(tmpRoot);
|
|
86
|
+
|
|
87
|
+
expect(result.path).toBe(getConduitDbPath(tmpRoot));
|
|
88
|
+
expect(result.action).toBe('created');
|
|
89
|
+
expect(existsSync(join(tmpRoot, '.cleo'))).toBe(true);
|
|
90
|
+
expect(existsSync(join(tmpRoot, '.cleo', 'conduit.db'))).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// TC-002 continued — all 11 messaging tables + 2 tracking tables created
|
|
94
|
+
it('ensureConduitDb creates all expected tables on fresh install', () => {
|
|
95
|
+
ensureConduitDb(tmpRoot);
|
|
96
|
+
const db = getConduitNativeDb();
|
|
97
|
+
expect(db).not.toBeNull();
|
|
98
|
+
|
|
99
|
+
const rows = db!
|
|
100
|
+
.prepare(
|
|
101
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
|
|
102
|
+
)
|
|
103
|
+
.all() as Array<{ name: string }>;
|
|
104
|
+
|
|
105
|
+
const tableNames = rows.map((r) => r.name).sort();
|
|
106
|
+
// messages_fts appears as a virtual table with shadow tables; the main
|
|
107
|
+
// virtual table itself is type='table' in sqlite_master.
|
|
108
|
+
const nonFts = tableNames.filter((n) => !n.startsWith('messages_fts'));
|
|
109
|
+
expect(nonFts).toEqual(EXPECTED_TABLES);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// TC-011 — messages_fts virtual table created
|
|
113
|
+
it('ensureConduitDb creates messages_fts virtual table', () => {
|
|
114
|
+
ensureConduitDb(tmpRoot);
|
|
115
|
+
const db = getConduitNativeDb();
|
|
116
|
+
expect(db).not.toBeNull();
|
|
117
|
+
|
|
118
|
+
const row = db!
|
|
119
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'")
|
|
120
|
+
.get() as { name: string } | undefined;
|
|
121
|
+
expect(row?.name).toBe('messages_fts');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// TC-012 — FTS5 triggers created and functional
|
|
125
|
+
it('messages_fts triggers are created and FTS search works after insert', () => {
|
|
126
|
+
ensureConduitDb(tmpRoot);
|
|
127
|
+
const db = getConduitNativeDb();
|
|
128
|
+
expect(db).not.toBeNull();
|
|
129
|
+
|
|
130
|
+
// Verify all three triggers exist.
|
|
131
|
+
const triggers = db!
|
|
132
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='trigger' ORDER BY name")
|
|
133
|
+
.all() as Array<{ name: string }>;
|
|
134
|
+
const triggerNames = triggers.map((t) => t.name);
|
|
135
|
+
expect(triggerNames).toContain('messages_ai');
|
|
136
|
+
expect(triggerNames).toContain('messages_ad');
|
|
137
|
+
expect(triggerNames).toContain('messages_au');
|
|
138
|
+
|
|
139
|
+
// Insert a conversation first (FK constraint on messages).
|
|
140
|
+
db!.exec(`INSERT INTO conversations (id, participants, created_at, updated_at)
|
|
141
|
+
VALUES ('conv-1', '["a","b"]', 1000, 1000)`);
|
|
142
|
+
|
|
143
|
+
// Insert a message — messages_ai trigger should populate messages_fts.
|
|
144
|
+
db!.exec(`INSERT INTO messages
|
|
145
|
+
(id, conversation_id, from_agent_id, to_agent_id, content, created_at)
|
|
146
|
+
VALUES ('msg-1', 'conv-1', 'agent-a', 'agent-b', 'hello world', 1000)`);
|
|
147
|
+
|
|
148
|
+
const ftsRow = db!
|
|
149
|
+
.prepare("SELECT * FROM messages_fts WHERE messages_fts MATCH 'hello'")
|
|
150
|
+
.get() as Record<string, unknown> | undefined;
|
|
151
|
+
expect(ftsRow).toBeDefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// TC-003 — idempotent re-open
|
|
155
|
+
it('ensureConduitDb returns action=exists on second call for same project root', () => {
|
|
156
|
+
const first = ensureConduitDb(tmpRoot);
|
|
157
|
+
expect(first.action).toBe('created');
|
|
158
|
+
|
|
159
|
+
// Close the singleton to simulate a second process open.
|
|
160
|
+
closeConduitDb();
|
|
161
|
+
|
|
162
|
+
const second = ensureConduitDb(tmpRoot);
|
|
163
|
+
expect(second.action).toBe('exists');
|
|
164
|
+
expect(second.path).toBe(first.path);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('ensureConduitDb is idempotent: data survives across two opens', () => {
|
|
168
|
+
ensureConduitDb(tmpRoot);
|
|
169
|
+
const db1 = getConduitNativeDb()!;
|
|
170
|
+
db1.exec(`INSERT INTO project_agent_refs (agent_id, attached_at)
|
|
171
|
+
VALUES ('test-agent', '2026-04-12T00:00:00Z')`);
|
|
172
|
+
closeConduitDb();
|
|
173
|
+
|
|
174
|
+
ensureConduitDb(tmpRoot);
|
|
175
|
+
const db2 = getConduitNativeDb()!;
|
|
176
|
+
const row = db2
|
|
177
|
+
.prepare('SELECT agent_id FROM project_agent_refs WHERE agent_id = ?')
|
|
178
|
+
.get('test-agent') as { agent_id: string } | undefined;
|
|
179
|
+
expect(row?.agent_id).toBe('test-agent');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// getConduitNativeDb — returns null before init
|
|
183
|
+
it('getConduitNativeDb returns null before ensureConduitDb is called', () => {
|
|
184
|
+
expect(getConduitNativeDb()).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// getConduitNativeDb — returns live handle after init
|
|
188
|
+
it('getConduitNativeDb returns the live DatabaseSync handle after ensureConduitDb', () => {
|
|
189
|
+
ensureConduitDb(tmpRoot);
|
|
190
|
+
const db = getConduitNativeDb();
|
|
191
|
+
expect(db).not.toBeNull();
|
|
192
|
+
expect(db!.isOpen).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// getConduitNativeDb — returns null after close
|
|
196
|
+
it('getConduitNativeDb returns null after closeConduitDb', () => {
|
|
197
|
+
ensureConduitDb(tmpRoot);
|
|
198
|
+
closeConduitDb();
|
|
199
|
+
expect(getConduitNativeDb()).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// integrity_check
|
|
203
|
+
it('conduit.db passes PRAGMA integrity_check after creation', () => {
|
|
204
|
+
ensureConduitDb(tmpRoot);
|
|
205
|
+
const db = getConduitNativeDb()!;
|
|
206
|
+
const result = db.prepare('PRAGMA integrity_check').get() as { integrity_check: string };
|
|
207
|
+
expect(result.integrity_check).toBe('ok');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// project_agent_refs schema
|
|
211
|
+
it('project_agent_refs has expected columns with correct constraints', () => {
|
|
212
|
+
ensureConduitDb(tmpRoot);
|
|
213
|
+
const db = getConduitNativeDb()!;
|
|
214
|
+
|
|
215
|
+
const cols = db.prepare('PRAGMA table_info(project_agent_refs)').all() as Array<{
|
|
216
|
+
cid: number;
|
|
217
|
+
name: string;
|
|
218
|
+
type: string;
|
|
219
|
+
notnull: number;
|
|
220
|
+
dflt_value: unknown;
|
|
221
|
+
pk: number;
|
|
222
|
+
}>;
|
|
223
|
+
|
|
224
|
+
const colNames = cols.map((c) => c.name);
|
|
225
|
+
expect(colNames).toEqual([
|
|
226
|
+
'agent_id',
|
|
227
|
+
'attached_at',
|
|
228
|
+
'role',
|
|
229
|
+
'capabilities_override',
|
|
230
|
+
'last_used_at',
|
|
231
|
+
'enabled',
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const agentIdCol = cols.find((c) => c.name === 'agent_id');
|
|
235
|
+
expect(agentIdCol?.pk).toBe(1);
|
|
236
|
+
|
|
237
|
+
const enabledCol = cols.find((c) => c.name === 'enabled');
|
|
238
|
+
expect(enabledCol?.notnull).toBe(1);
|
|
239
|
+
expect(enabledCol?.dflt_value).toBe('1');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// partial index on project_agent_refs
|
|
243
|
+
it('idx_project_agent_refs_enabled partial index exists on project_agent_refs', () => {
|
|
244
|
+
ensureConduitDb(tmpRoot);
|
|
245
|
+
const db = getConduitNativeDb()!;
|
|
246
|
+
|
|
247
|
+
const indices = db.prepare('PRAGMA index_list(project_agent_refs)').all() as Array<{
|
|
248
|
+
seq: number;
|
|
249
|
+
name: string;
|
|
250
|
+
unique: number;
|
|
251
|
+
origin: string;
|
|
252
|
+
partial: number;
|
|
253
|
+
}>;
|
|
254
|
+
const enabledIdx = indices.find((i) => i.name === 'idx_project_agent_refs_enabled');
|
|
255
|
+
expect(enabledIdx).toBeDefined();
|
|
256
|
+
expect(enabledIdx?.partial).toBe(1);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// applyConduitSchema — idempotent on existing db
|
|
260
|
+
it('applyConduitSchema is idempotent when called twice on the same db', () => {
|
|
261
|
+
const dbPath = join(tmpRoot, 'manual.db');
|
|
262
|
+
mkdirSync(tmpRoot, { recursive: true });
|
|
263
|
+
const db = new DatabaseSync(dbPath);
|
|
264
|
+
expect(() => applyConduitSchema(db)).not.toThrow();
|
|
265
|
+
expect(() => applyConduitSchema(db)).not.toThrow();
|
|
266
|
+
db.close();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// schema version recorded in _conduit_meta
|
|
270
|
+
it('ensureConduitDb records CONDUIT_SCHEMA_VERSION in _conduit_meta', () => {
|
|
271
|
+
ensureConduitDb(tmpRoot);
|
|
272
|
+
const db = getConduitNativeDb()!;
|
|
273
|
+
const meta = db.prepare("SELECT value FROM _conduit_meta WHERE key = 'schema_version'").get() as
|
|
274
|
+
| { value: string }
|
|
275
|
+
| undefined;
|
|
276
|
+
expect(meta?.value).toBe(CONDUIT_SCHEMA_VERSION);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// migration row recorded in _conduit_migrations
|
|
280
|
+
it('ensureConduitDb records initial migration in _conduit_migrations', () => {
|
|
281
|
+
ensureConduitDb(tmpRoot);
|
|
282
|
+
const db = getConduitNativeDb()!;
|
|
283
|
+
const mig = db
|
|
284
|
+
.prepare(
|
|
285
|
+
"SELECT name FROM _conduit_migrations WHERE name = '2026-04-12-000000_initial_conduit'",
|
|
286
|
+
)
|
|
287
|
+
.get() as { name: string } | undefined;
|
|
288
|
+
expect(mig?.name).toBe('2026-04-12-000000_initial_conduit');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// checkConduitDbHealth — db absent
|
|
292
|
+
it('checkConduitDbHealth returns exists=false when conduit.db does not exist', () => {
|
|
293
|
+
const health = checkConduitDbHealth(tmpRoot);
|
|
294
|
+
expect(health.exists).toBe(false);
|
|
295
|
+
expect(health.tableCount).toBe(0);
|
|
296
|
+
expect(health.walMode).toBe(false);
|
|
297
|
+
expect(health.schemaVersion).toBeNull();
|
|
298
|
+
expect(health.foreignKeysEnabled).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// checkConduitDbHealth — after creation
|
|
302
|
+
it('checkConduitDbHealth returns correct health after ensureConduitDb', () => {
|
|
303
|
+
ensureConduitDb(tmpRoot);
|
|
304
|
+
closeConduitDb();
|
|
305
|
+
|
|
306
|
+
const health = checkConduitDbHealth(tmpRoot);
|
|
307
|
+
expect(health.exists).toBe(true);
|
|
308
|
+
expect(health.walMode).toBe(true);
|
|
309
|
+
expect(health.schemaVersion).toBe(CONDUIT_SCHEMA_VERSION);
|
|
310
|
+
expect(health.foreignKeysEnabled).toBe(true);
|
|
311
|
+
// At minimum all non-FTS tables + FTS main table + meta tables.
|
|
312
|
+
expect(health.tableCount).toBeGreaterThanOrEqual(EXPECTED_TABLES.length);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ensureConduitDb with pre-existing .cleo/ dir
|
|
316
|
+
it('ensureConduitDb works when .cleo/ dir already exists', () => {
|
|
317
|
+
mkdirSync(join(tmpRoot, '.cleo'), { recursive: true });
|
|
318
|
+
expect(() => ensureConduitDb(tmpRoot)).not.toThrow();
|
|
319
|
+
expect(existsSync(join(tmpRoot, '.cleo', 'conduit.db'))).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// project_agent_refs CRUD (T353)
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
describe('project_agent_refs CRUD (T353)', () => {
|
|
327
|
+
it('TC-004: attachAgentToProject inserts a new row with enabled=1', () => {
|
|
328
|
+
ensureConduitDb(tmpRoot);
|
|
329
|
+
const db = getConduitNativeDb()!;
|
|
330
|
+
attachAgentToProject(db, 'agent-1');
|
|
331
|
+
const ref = getProjectAgentRef(db, 'agent-1');
|
|
332
|
+
expect(ref).not.toBeNull();
|
|
333
|
+
expect(ref?.agentId).toBe('agent-1');
|
|
334
|
+
expect(ref?.enabled).toBe(1);
|
|
335
|
+
expect(ref?.role).toBeNull();
|
|
336
|
+
expect(ref?.capabilitiesOverride).toBeNull();
|
|
337
|
+
expect(ref?.lastUsedAt).toBeNull();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('TC-005: attachAgentToProject re-enables an existing enabled=0 row without duplicate', () => {
|
|
341
|
+
ensureConduitDb(tmpRoot);
|
|
342
|
+
const db = getConduitNativeDb()!;
|
|
343
|
+
attachAgentToProject(db, 'agent-1');
|
|
344
|
+
detachAgentFromProject(db, 'agent-1');
|
|
345
|
+
const detached = getProjectAgentRef(db, 'agent-1');
|
|
346
|
+
expect(detached?.enabled).toBe(0);
|
|
347
|
+
attachAgentToProject(db, 'agent-1', { role: 'reviewer' });
|
|
348
|
+
const reattached = getProjectAgentRef(db, 'agent-1');
|
|
349
|
+
expect(reattached?.enabled).toBe(1);
|
|
350
|
+
expect(reattached?.role).toBe('reviewer');
|
|
351
|
+
const count = (
|
|
352
|
+
db
|
|
353
|
+
.prepare('SELECT COUNT(*) AS c FROM project_agent_refs WHERE agent_id = ?')
|
|
354
|
+
.get('agent-1') as {
|
|
355
|
+
c: number;
|
|
356
|
+
}
|
|
357
|
+
).c;
|
|
358
|
+
expect(count).toBe(1);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('TC-006: detachAgentFromProject sets enabled=0 without deleting', () => {
|
|
362
|
+
ensureConduitDb(tmpRoot);
|
|
363
|
+
const db = getConduitNativeDb()!;
|
|
364
|
+
attachAgentToProject(db, 'agent-1');
|
|
365
|
+
detachAgentFromProject(db, 'agent-1');
|
|
366
|
+
const ref = getProjectAgentRef(db, 'agent-1');
|
|
367
|
+
expect(ref).not.toBeNull();
|
|
368
|
+
expect(ref?.enabled).toBe(0);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('TC-007: listProjectAgentRefs returns only enabled=1 rows by default', () => {
|
|
372
|
+
ensureConduitDb(tmpRoot);
|
|
373
|
+
const db = getConduitNativeDb()!;
|
|
374
|
+
attachAgentToProject(db, 'agent-1');
|
|
375
|
+
attachAgentToProject(db, 'agent-2');
|
|
376
|
+
attachAgentToProject(db, 'agent-3');
|
|
377
|
+
detachAgentFromProject(db, 'agent-2');
|
|
378
|
+
const enabled = listProjectAgentRefs(db);
|
|
379
|
+
expect(enabled.length).toBe(2);
|
|
380
|
+
expect(enabled.map((r) => r.agentId).sort()).toEqual(['agent-1', 'agent-3']);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('TC-008: listProjectAgentRefs returns all rows when enabledOnly=false', () => {
|
|
384
|
+
ensureConduitDb(tmpRoot);
|
|
385
|
+
const db = getConduitNativeDb()!;
|
|
386
|
+
attachAgentToProject(db, 'agent-1');
|
|
387
|
+
attachAgentToProject(db, 'agent-2');
|
|
388
|
+
detachAgentFromProject(db, 'agent-2');
|
|
389
|
+
const all = listProjectAgentRefs(db, { enabledOnly: false });
|
|
390
|
+
expect(all.length).toBe(2);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('TC-009: getProjectAgentRef returns null for unknown agent', () => {
|
|
394
|
+
ensureConduitDb(tmpRoot);
|
|
395
|
+
const db = getConduitNativeDb()!;
|
|
396
|
+
const ref = getProjectAgentRef(db, 'nonexistent');
|
|
397
|
+
expect(ref).toBeNull();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('TC-010: updateProjectAgentLastUsed sets last_used_at to current ISO timestamp', () => {
|
|
401
|
+
ensureConduitDb(tmpRoot);
|
|
402
|
+
const db = getConduitNativeDb()!;
|
|
403
|
+
attachAgentToProject(db, 'agent-1');
|
|
404
|
+
const before = new Date().toISOString();
|
|
405
|
+
updateProjectAgentLastUsed(db, 'agent-1');
|
|
406
|
+
const after = new Date().toISOString();
|
|
407
|
+
const ref = getProjectAgentRef(db, 'agent-1');
|
|
408
|
+
expect(ref?.lastUsedAt).not.toBeNull();
|
|
409
|
+
expect(ref!.lastUsedAt! >= before).toBe(true);
|
|
410
|
+
expect(ref!.lastUsedAt! <= after).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|