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