hungry-ghost-hive 0.21.5 → 0.21.6
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/config/schema.d.ts +14 -14
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +62 -8
- package/dist/db/client.js.map +1 -1
- package/dist/db/client.test.d.ts +2 -0
- package/dist/db/client.test.d.ts.map +1 -0
- package/dist/db/client.test.js +223 -0
- package/dist/db/client.test.js.map +1 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +9 -0
- package/dist/errors/index.js.map +1 -1
- package/package.json +1 -1
- package/src/db/client.test.ts +276 -0
- package/src/db/client.ts +73 -10
- package/src/errors/index.ts +10 -0
package/dist/config/schema.d.ts
CHANGED
|
@@ -821,6 +821,13 @@ export declare const HiveConfigSchema: z.ZodObject<{
|
|
|
821
821
|
story_similarity_threshold?: number | undefined;
|
|
822
822
|
}>>;
|
|
823
823
|
}, "strip", z.ZodTypeAny, {
|
|
824
|
+
agents: {
|
|
825
|
+
poll_interval: number;
|
|
826
|
+
max_retries: number;
|
|
827
|
+
checkpoint_threshold: number;
|
|
828
|
+
llm_timeout_ms: number;
|
|
829
|
+
llm_max_retries: number;
|
|
830
|
+
};
|
|
824
831
|
qa: {
|
|
825
832
|
quality_checks: string[];
|
|
826
833
|
build_command: string;
|
|
@@ -830,13 +837,6 @@ export declare const HiveConfigSchema: z.ZodObject<{
|
|
|
830
837
|
max_agents: number;
|
|
831
838
|
} | undefined;
|
|
832
839
|
};
|
|
833
|
-
agents: {
|
|
834
|
-
poll_interval: number;
|
|
835
|
-
max_retries: number;
|
|
836
|
-
checkpoint_threshold: number;
|
|
837
|
-
llm_timeout_ms: number;
|
|
838
|
-
llm_max_retries: number;
|
|
839
|
-
};
|
|
840
840
|
scaling: {
|
|
841
841
|
senior_capacity: number;
|
|
842
842
|
junior_max_complexity: number;
|
|
@@ -922,6 +922,13 @@ export declare const HiveConfigSchema: z.ZodObject<{
|
|
|
922
922
|
auth_token?: string | undefined;
|
|
923
923
|
};
|
|
924
924
|
}, {
|
|
925
|
+
agents?: {
|
|
926
|
+
poll_interval?: number | undefined;
|
|
927
|
+
max_retries?: number | undefined;
|
|
928
|
+
checkpoint_threshold?: number | undefined;
|
|
929
|
+
llm_timeout_ms?: number | undefined;
|
|
930
|
+
llm_max_retries?: number | undefined;
|
|
931
|
+
} | undefined;
|
|
925
932
|
qa?: {
|
|
926
933
|
quality_checks?: string[] | undefined;
|
|
927
934
|
build_command?: string | undefined;
|
|
@@ -931,13 +938,6 @@ export declare const HiveConfigSchema: z.ZodObject<{
|
|
|
931
938
|
max_agents?: number | undefined;
|
|
932
939
|
} | undefined;
|
|
933
940
|
} | undefined;
|
|
934
|
-
agents?: {
|
|
935
|
-
poll_interval?: number | undefined;
|
|
936
|
-
max_retries?: number | undefined;
|
|
937
|
-
checkpoint_threshold?: number | undefined;
|
|
938
|
-
llm_timeout_ms?: number | undefined;
|
|
939
|
-
llm_max_retries?: number | undefined;
|
|
940
|
-
} | undefined;
|
|
941
941
|
scaling?: {
|
|
942
942
|
senior_capacity?: number | undefined;
|
|
943
943
|
junior_max_complexity?: number | undefined;
|
package/dist/db/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/db/client.ts"],"names":[],"mappings":"AAIA,OAAkB,EAAE,QAAQ,IAAI,aAAa,EAAE,MAAM,QAAQ,CAAC;AAG9D,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,aAAa,CAAC;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/db/client.ts"],"names":[],"mappings":"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"}
|
package/dist/db/client.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Licensed under the Hungry Ghost Hive License. See LICENSE.
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import initSqlJs from 'sql.js';
|
|
5
|
-
import { InitializationError } from '../errors/index.js';
|
|
5
|
+
import { DatabaseCorruptionError, InitializationError } from '../errors/index.js';
|
|
6
6
|
// Embedded initial migration SQL
|
|
7
7
|
const INITIAL_MIGRATION = `
|
|
8
8
|
-- Hive Orchestrator Initial Schema
|
|
@@ -150,29 +150,83 @@ async function getSqlJs() {
|
|
|
150
150
|
}
|
|
151
151
|
return SQL;
|
|
152
152
|
}
|
|
153
|
+
// Minimum file size (in bytes) that indicates a database had meaningful data.
|
|
154
|
+
// Files below this threshold are likely new or schema-only databases.
|
|
155
|
+
const CORRUPTION_CHECK_MIN_FILE_SIZE = 50 * 1024; // 50KB
|
|
156
|
+
// Core tables that should have rows in a populated database
|
|
157
|
+
const CORE_TABLES = ['teams', 'agents', 'stories'];
|
|
158
|
+
/**
|
|
159
|
+
* Validate that a loaded database is not a silently-corrupted empty copy.
|
|
160
|
+
* If the source file was large (>50KB) but the loaded DB has core tables
|
|
161
|
+
* with zero rows, that indicates sql.js silently returned an empty DB.
|
|
162
|
+
*/
|
|
163
|
+
function validateLoadedDatabase(db, fileSize) {
|
|
164
|
+
if (fileSize < CORRUPTION_CHECK_MIN_FILE_SIZE) {
|
|
165
|
+
return; // Small file — likely a new or schema-only DB, skip validation
|
|
166
|
+
}
|
|
167
|
+
// Check if any core table has data
|
|
168
|
+
for (const table of CORE_TABLES) {
|
|
169
|
+
try {
|
|
170
|
+
const result = db.exec(`SELECT COUNT(*) FROM ${table}`);
|
|
171
|
+
if (result.length > 0 && result[0].values[0][0] > 0) {
|
|
172
|
+
return; // At least one core table has data — DB looks valid
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Table doesn't exist yet — that's fine, migrations haven't run
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// File was large but all core tables are empty — likely corruption
|
|
181
|
+
throw new DatabaseCorruptionError(`Database file is ${fileSize} bytes but loaded with zero rows in core tables (${CORE_TABLES.join(', ')}). ` +
|
|
182
|
+
'This likely indicates a corrupted or partially-read database file. ' +
|
|
183
|
+
'Refusing to proceed to prevent data loss. Check the backup at hive.db.bak if available.');
|
|
184
|
+
}
|
|
153
185
|
export async function createDatabase(dbPath) {
|
|
154
186
|
const SqlJs = await getSqlJs();
|
|
155
187
|
if (!SqlJs)
|
|
156
188
|
throw new InitializationError('Failed to initialize sql.js');
|
|
157
189
|
let db;
|
|
190
|
+
const backupPath = dbPath + '.bak';
|
|
158
191
|
// Load existing database or create new one
|
|
159
192
|
if (existsSync(dbPath)) {
|
|
160
193
|
const buffer = readFileSync(dbPath);
|
|
161
|
-
|
|
194
|
+
const fileSize = statSync(dbPath).size;
|
|
195
|
+
try {
|
|
196
|
+
db = new SqlJs.Database(buffer);
|
|
197
|
+
// Verify the database is usable by running a basic command
|
|
198
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
199
|
+
db.exec('SELECT 1');
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
throw new DatabaseCorruptionError(`Failed to load database file at ${dbPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
203
|
+
}
|
|
204
|
+
// Validate the loaded DB is not silently empty from a corrupt file
|
|
205
|
+
validateLoadedDatabase(db, fileSize);
|
|
206
|
+
// Run migrations on loaded DB — wrap in try-catch to detect subtle corruption
|
|
207
|
+
try {
|
|
208
|
+
runMigrations(db);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
throw new DatabaseCorruptionError(`Database file at ${dbPath} appears corrupted (migrations failed): ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
+
}
|
|
162
213
|
}
|
|
163
214
|
else {
|
|
164
215
|
db = new SqlJs.Database();
|
|
216
|
+
// Enable foreign keys
|
|
217
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
218
|
+
// Run migrations on new DB
|
|
219
|
+
runMigrations(db);
|
|
165
220
|
}
|
|
166
|
-
// Enable foreign keys
|
|
167
|
-
db.run('PRAGMA foreign_keys = ON');
|
|
168
221
|
const save = () => {
|
|
222
|
+
// Write backup before overwriting the main database file
|
|
223
|
+
if (existsSync(dbPath)) {
|
|
224
|
+
copyFileSync(dbPath, backupPath);
|
|
225
|
+
}
|
|
169
226
|
const data = db.export();
|
|
170
227
|
const buffer = Buffer.from(data);
|
|
171
228
|
writeFileSync(dbPath, buffer);
|
|
172
229
|
};
|
|
173
|
-
// Auto-run migrations
|
|
174
|
-
runMigrations(db);
|
|
175
|
-
save();
|
|
176
230
|
const client = {
|
|
177
231
|
db,
|
|
178
232
|
close: () => {
|
package/dist/db/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/db/client.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAE7D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/db/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../src/db/client.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import initSqlJs from 'sql.js';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { DatabaseCorruptionError } from '../errors/index.js';
|
|
7
|
+
import { createDatabase } from './client.js';
|
|
8
|
+
/**
|
|
9
|
+
* Helper to create a valid SQLite database buffer with the core schema
|
|
10
|
+
* that is compatible with runMigrations().
|
|
11
|
+
*/
|
|
12
|
+
async function createValidDbBuffer(opts = {}) {
|
|
13
|
+
const SQL = await initSqlJs();
|
|
14
|
+
const db = new SQL.Database();
|
|
15
|
+
// Use a schema that matches the initial migration so runMigrations works
|
|
16
|
+
db.run(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
18
|
+
id TEXT PRIMARY KEY, repo_url TEXT NOT NULL, repo_path TEXT NOT NULL,
|
|
19
|
+
name TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
20
|
+
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
type TEXT NOT NULL CHECK (type IN ('tech_lead', 'senior', 'intermediate', 'junior', 'qa')),
|
|
24
|
+
team_id TEXT REFERENCES teams(id), tmux_session TEXT, model TEXT,
|
|
25
|
+
status TEXT DEFAULT 'idle' CHECK (status IN ('idle', 'working', 'blocked', 'terminated')),
|
|
26
|
+
current_story_id TEXT, memory_state TEXT, last_seen TIMESTAMP, worktree_path TEXT,
|
|
27
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
28
|
+
);
|
|
29
|
+
CREATE TABLE IF NOT EXISTS requirements (
|
|
30
|
+
id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT NOT NULL,
|
|
31
|
+
submitted_by TEXT DEFAULT 'human',
|
|
32
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'planning', 'planned', 'in_progress', 'completed')),
|
|
33
|
+
godmode BOOLEAN DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
34
|
+
);
|
|
35
|
+
CREATE TABLE IF NOT EXISTS stories (
|
|
36
|
+
id TEXT PRIMARY KEY, requirement_id TEXT REFERENCES requirements(id),
|
|
37
|
+
team_id TEXT REFERENCES teams(id), title TEXT NOT NULL, description TEXT NOT NULL,
|
|
38
|
+
acceptance_criteria TEXT,
|
|
39
|
+
complexity_score INTEGER CHECK (complexity_score BETWEEN 1 AND 13),
|
|
40
|
+
story_points INTEGER,
|
|
41
|
+
status TEXT DEFAULT 'draft' CHECK (status IN ('draft','estimated','planned','in_progress','review','qa','qa_failed','pr_submitted','merged')),
|
|
42
|
+
assigned_agent_id TEXT REFERENCES agents(id), branch_name TEXT, pr_url TEXT,
|
|
43
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
44
|
+
);
|
|
45
|
+
CREATE TABLE IF NOT EXISTS story_dependencies (
|
|
46
|
+
story_id TEXT REFERENCES stories(id), depends_on_story_id TEXT REFERENCES stories(id),
|
|
47
|
+
PRIMARY KEY (story_id, depends_on_story_id)
|
|
48
|
+
);
|
|
49
|
+
CREATE TABLE IF NOT EXISTS agent_logs (
|
|
50
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL REFERENCES agents(id),
|
|
51
|
+
story_id TEXT REFERENCES stories(id), event_type TEXT NOT NULL, status TEXT,
|
|
52
|
+
message TEXT, metadata TEXT, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
53
|
+
);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_agent ON agent_logs(agent_id);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_story ON agent_logs(story_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_timestamp ON agent_logs(timestamp);
|
|
57
|
+
CREATE TABLE IF NOT EXISTS escalations (
|
|
58
|
+
id TEXT PRIMARY KEY, story_id TEXT REFERENCES stories(id),
|
|
59
|
+
from_agent_id TEXT REFERENCES agents(id), to_agent_id TEXT REFERENCES agents(id),
|
|
60
|
+
reason TEXT NOT NULL,
|
|
61
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'acknowledged', 'resolved')),
|
|
62
|
+
resolution TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, resolved_at TIMESTAMP
|
|
63
|
+
);
|
|
64
|
+
CREATE TABLE IF NOT EXISTS pull_requests (
|
|
65
|
+
id TEXT PRIMARY KEY, story_id TEXT REFERENCES stories(id), team_id TEXT REFERENCES teams(id),
|
|
66
|
+
branch_name TEXT NOT NULL, github_pr_number INTEGER, github_pr_url TEXT,
|
|
67
|
+
submitted_by TEXT, reviewed_by TEXT,
|
|
68
|
+
status TEXT DEFAULT 'queued' CHECK (status IN ('queued','reviewing','approved','merged','rejected','closed')),
|
|
69
|
+
review_notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
70
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, reviewed_at TIMESTAMP
|
|
71
|
+
);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
73
|
+
id TEXT PRIMARY KEY, from_session TEXT NOT NULL, to_session TEXT NOT NULL,
|
|
74
|
+
subject TEXT, body TEXT NOT NULL, reply TEXT,
|
|
75
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'read', 'replied')),
|
|
76
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, replied_at TIMESTAMP
|
|
77
|
+
);
|
|
78
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE,
|
|
80
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
81
|
+
);
|
|
82
|
+
INSERT INTO migrations (name) VALUES ('001-initial.sql');
|
|
83
|
+
INSERT INTO migrations (name) VALUES ('002-add-agent-model.sql');
|
|
84
|
+
INSERT INTO migrations (name) VALUES ('003-fix-pull-requests.sql');
|
|
85
|
+
INSERT INTO migrations (name) VALUES ('004-add-messages.sql');
|
|
86
|
+
INSERT INTO migrations (name) VALUES ('005-add-agent-last-seen.sql');
|
|
87
|
+
INSERT INTO migrations (name) VALUES ('006-add-agent-worktree.sql');
|
|
88
|
+
INSERT INTO migrations (name) VALUES ('007-add-indexes.sql');
|
|
89
|
+
INSERT INTO migrations (name) VALUES ('008-add-godmode.sql');
|
|
90
|
+
INSERT INTO migrations (name) VALUES ('009-add-pr-sync-indexes.sql');
|
|
91
|
+
`);
|
|
92
|
+
if (opts.withData) {
|
|
93
|
+
db.run("INSERT INTO teams VALUES ('t1', 'https://example.com', '/tmp/repo', 'Test Team', datetime('now'))");
|
|
94
|
+
}
|
|
95
|
+
if (opts.withPadding) {
|
|
96
|
+
db.run('CREATE TABLE IF NOT EXISTS padding (id INTEGER PRIMARY KEY, data TEXT)');
|
|
97
|
+
for (let i = 0; i < 500; i++) {
|
|
98
|
+
db.run('INSERT INTO padding VALUES (?, ?)', [i, 'x'.repeat(100)]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const data = db.export();
|
|
102
|
+
db.close();
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
describe('createDatabase', () => {
|
|
106
|
+
let tempDir;
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
tempDir = mkdtempSync(join(tmpdir(), 'hive-db-test-'));
|
|
109
|
+
});
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
112
|
+
});
|
|
113
|
+
it('should create a new database when no file exists', async () => {
|
|
114
|
+
const dbPath = join(tempDir, 'new.db');
|
|
115
|
+
const client = await createDatabase(dbPath);
|
|
116
|
+
expect(client.db).toBeDefined();
|
|
117
|
+
// New database should not auto-save to disk
|
|
118
|
+
expect(existsSync(dbPath)).toBe(false);
|
|
119
|
+
client.close();
|
|
120
|
+
});
|
|
121
|
+
it('should load an existing valid database file', async () => {
|
|
122
|
+
const dbPath = join(tempDir, 'existing.db');
|
|
123
|
+
const data = await createValidDbBuffer({ withData: true });
|
|
124
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
125
|
+
const client = await createDatabase(dbPath);
|
|
126
|
+
const result = client.db.exec('SELECT COUNT(*) FROM teams');
|
|
127
|
+
expect(result[0].values[0][0]).toBe(1);
|
|
128
|
+
client.close();
|
|
129
|
+
});
|
|
130
|
+
it('should throw DatabaseCorruptionError when sql.js fails to parse buffer', async () => {
|
|
131
|
+
const dbPath = join(tempDir, 'corrupt.db');
|
|
132
|
+
// Write garbage data that is not valid SQLite
|
|
133
|
+
const garbageBuffer = Buffer.alloc(100);
|
|
134
|
+
garbageBuffer.write('NOT_A_SQLITE_DATABASE', 0);
|
|
135
|
+
writeFileSync(dbPath, garbageBuffer);
|
|
136
|
+
await expect(createDatabase(dbPath)).rejects.toThrow(DatabaseCorruptionError);
|
|
137
|
+
await expect(createDatabase(dbPath)).rejects.toThrow('Failed to load database file');
|
|
138
|
+
});
|
|
139
|
+
it('should detect corruption when large file loads with empty core tables', async () => {
|
|
140
|
+
const dbPath = join(tempDir, 'wiped.db');
|
|
141
|
+
// Create a large database with padding but NO data in core tables
|
|
142
|
+
const data = await createValidDbBuffer({ withData: false, withPadding: true });
|
|
143
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
144
|
+
// Verify the file is >50KB
|
|
145
|
+
expect(Buffer.from(data).length).toBeGreaterThan(50 * 1024);
|
|
146
|
+
await expect(createDatabase(dbPath)).rejects.toThrow(DatabaseCorruptionError);
|
|
147
|
+
await expect(createDatabase(dbPath)).rejects.toThrow('zero rows in core tables');
|
|
148
|
+
});
|
|
149
|
+
it('should NOT flag corruption when file is small (<50KB)', async () => {
|
|
150
|
+
const dbPath = join(tempDir, 'small-empty.db');
|
|
151
|
+
// Create an empty database (no tables yet — migrations will handle it)
|
|
152
|
+
const SQL = await initSqlJs();
|
|
153
|
+
const smallDb = new SQL.Database();
|
|
154
|
+
const data = smallDb.export();
|
|
155
|
+
smallDb.close();
|
|
156
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
157
|
+
// Verify the file is <50KB
|
|
158
|
+
expect(Buffer.from(data).length).toBeLessThan(50 * 1024);
|
|
159
|
+
// Should pass — small file skips corruption check, migrations create schema
|
|
160
|
+
const client = await createDatabase(dbPath);
|
|
161
|
+
expect(client.db).toBeDefined();
|
|
162
|
+
client.close();
|
|
163
|
+
});
|
|
164
|
+
it('should NOT flag corruption when large file has data in core tables', async () => {
|
|
165
|
+
const dbPath = join(tempDir, 'large-valid.db');
|
|
166
|
+
// Create a large database WITH data in core tables
|
|
167
|
+
const data = await createValidDbBuffer({ withData: true, withPadding: true });
|
|
168
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
169
|
+
expect(Buffer.from(data).length).toBeGreaterThan(50 * 1024);
|
|
170
|
+
// Should pass — core table has data
|
|
171
|
+
const client = await createDatabase(dbPath);
|
|
172
|
+
expect(client.db).toBeDefined();
|
|
173
|
+
client.close();
|
|
174
|
+
});
|
|
175
|
+
describe('save() backup behavior', () => {
|
|
176
|
+
it('should create a backup file before saving', async () => {
|
|
177
|
+
const dbPath = join(tempDir, 'backup-test.db');
|
|
178
|
+
const backupPath = dbPath + '.bak';
|
|
179
|
+
const client = await createDatabase(dbPath);
|
|
180
|
+
client.save(); // First save — creates the file, no prior file to back up
|
|
181
|
+
expect(existsSync(dbPath)).toBe(true);
|
|
182
|
+
// Save again — now a backup should be created from the existing file
|
|
183
|
+
client.save();
|
|
184
|
+
expect(existsSync(backupPath)).toBe(true);
|
|
185
|
+
client.close();
|
|
186
|
+
});
|
|
187
|
+
it('should preserve backup content from the previous version', async () => {
|
|
188
|
+
const dbPath = join(tempDir, 'backup-content.db');
|
|
189
|
+
const backupPath = dbPath + '.bak';
|
|
190
|
+
const client = await createDatabase(dbPath);
|
|
191
|
+
client.db.run('CREATE TABLE IF NOT EXISTS test_data (val TEXT)');
|
|
192
|
+
client.save(); // version 1
|
|
193
|
+
const firstSaveContent = readFileSync(dbPath);
|
|
194
|
+
// Modify and save again — version 2
|
|
195
|
+
client.db.run("INSERT INTO test_data VALUES ('version2')");
|
|
196
|
+
client.save();
|
|
197
|
+
// Backup should contain version 1's content
|
|
198
|
+
const backupContent = readFileSync(backupPath);
|
|
199
|
+
expect(backupContent).toEqual(firstSaveContent);
|
|
200
|
+
client.close();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe('auto-save removal', () => {
|
|
204
|
+
it('should NOT auto-save after initialization of a new database', async () => {
|
|
205
|
+
const dbPath = join(tempDir, 'no-autosave.db');
|
|
206
|
+
await createDatabase(dbPath);
|
|
207
|
+
// File should NOT exist because we removed the auto-save on init
|
|
208
|
+
expect(existsSync(dbPath)).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
it('should NOT auto-save when loading an existing database', async () => {
|
|
211
|
+
const dbPath = join(tempDir, 'no-autosave-existing.db');
|
|
212
|
+
const data = await createValidDbBuffer({ withData: true });
|
|
213
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
214
|
+
const originalContent = readFileSync(dbPath);
|
|
215
|
+
// Loading should not modify the file on disk
|
|
216
|
+
const client = await createDatabase(dbPath);
|
|
217
|
+
const afterLoadContent = readFileSync(dbPath);
|
|
218
|
+
expect(afterLoadContent).toEqual(originalContent);
|
|
219
|
+
client.close();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
//# sourceMappingURL=client.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.test.js","sourceRoot":"","sources":["../../src/db/client.test.ts"],"names":[],"mappings":"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"}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -59,6 +59,12 @@ export declare class TimeoutError extends HiveError {
|
|
|
59
59
|
export declare class NotFoundError extends HiveError {
|
|
60
60
|
constructor(message: string);
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Database corruption errors (empty DB loaded from non-empty file, parse failures)
|
|
64
|
+
*/
|
|
65
|
+
export declare class DatabaseCorruptionError extends HiveError {
|
|
66
|
+
constructor(message: string);
|
|
67
|
+
}
|
|
62
68
|
/**
|
|
63
69
|
* Helper function to convert generic errors to Hive errors
|
|
64
70
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,SAAgB,IAAI,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAqB;CAMzD;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,SAAS;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,SAAS;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,SAAS;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,SAAS;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,YAAa,SAAQ,SAAS;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,GAAE,OAAO,SAAqB,GAAG,SAAS,CAQjG"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,SAAgB,IAAI,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAqB;CAMzD;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,SAAS;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,SAAS;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,SAAS;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,SAAS;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,YAAa,SAAQ,SAAS;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,SAAS;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,GAAE,OAAO,SAAqB,GAAG,SAAS,CAQjG"}
|
package/dist/errors/index.js
CHANGED
|
@@ -92,6 +92,15 @@ export class NotFoundError extends HiveError {
|
|
|
92
92
|
Object.setPrototypeOf(this, NotFoundError.prototype);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Database corruption errors (empty DB loaded from non-empty file, parse failures)
|
|
97
|
+
*/
|
|
98
|
+
export class DatabaseCorruptionError extends HiveError {
|
|
99
|
+
constructor(message) {
|
|
100
|
+
super(message, 'DATABASE_CORRUPTION_ERROR');
|
|
101
|
+
Object.setPrototypeOf(this, DatabaseCorruptionError.prototype);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
95
104
|
/**
|
|
96
105
|
* Helper function to convert generic errors to Hive errors
|
|
97
106
|
*/
|
package/dist/errors/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import initSqlJs from 'sql.js';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { DatabaseCorruptionError } from '../errors/index.js';
|
|
7
|
+
import { createDatabase } from './client.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Helper to create a valid SQLite database buffer with the core schema
|
|
11
|
+
* that is compatible with runMigrations().
|
|
12
|
+
*/
|
|
13
|
+
async function createValidDbBuffer(
|
|
14
|
+
opts: { withData?: boolean; withPadding?: boolean } = {}
|
|
15
|
+
): Promise<Uint8Array> {
|
|
16
|
+
const SQL = await initSqlJs();
|
|
17
|
+
const db = new SQL.Database();
|
|
18
|
+
|
|
19
|
+
// Use a schema that matches the initial migration so runMigrations works
|
|
20
|
+
db.run(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
22
|
+
id TEXT PRIMARY KEY, repo_url TEXT NOT NULL, repo_path TEXT NOT NULL,
|
|
23
|
+
name TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
type TEXT NOT NULL CHECK (type IN ('tech_lead', 'senior', 'intermediate', 'junior', 'qa')),
|
|
28
|
+
team_id TEXT REFERENCES teams(id), tmux_session TEXT, model TEXT,
|
|
29
|
+
status TEXT DEFAULT 'idle' CHECK (status IN ('idle', 'working', 'blocked', 'terminated')),
|
|
30
|
+
current_story_id TEXT, memory_state TEXT, last_seen TIMESTAMP, worktree_path TEXT,
|
|
31
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
32
|
+
);
|
|
33
|
+
CREATE TABLE IF NOT EXISTS requirements (
|
|
34
|
+
id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT NOT NULL,
|
|
35
|
+
submitted_by TEXT DEFAULT 'human',
|
|
36
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'planning', 'planned', 'in_progress', 'completed')),
|
|
37
|
+
godmode BOOLEAN DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
38
|
+
);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS stories (
|
|
40
|
+
id TEXT PRIMARY KEY, requirement_id TEXT REFERENCES requirements(id),
|
|
41
|
+
team_id TEXT REFERENCES teams(id), title TEXT NOT NULL, description TEXT NOT NULL,
|
|
42
|
+
acceptance_criteria TEXT,
|
|
43
|
+
complexity_score INTEGER CHECK (complexity_score BETWEEN 1 AND 13),
|
|
44
|
+
story_points INTEGER,
|
|
45
|
+
status TEXT DEFAULT 'draft' CHECK (status IN ('draft','estimated','planned','in_progress','review','qa','qa_failed','pr_submitted','merged')),
|
|
46
|
+
assigned_agent_id TEXT REFERENCES agents(id), branch_name TEXT, pr_url TEXT,
|
|
47
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
48
|
+
);
|
|
49
|
+
CREATE TABLE IF NOT EXISTS story_dependencies (
|
|
50
|
+
story_id TEXT REFERENCES stories(id), depends_on_story_id TEXT REFERENCES stories(id),
|
|
51
|
+
PRIMARY KEY (story_id, depends_on_story_id)
|
|
52
|
+
);
|
|
53
|
+
CREATE TABLE IF NOT EXISTS agent_logs (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL REFERENCES agents(id),
|
|
55
|
+
story_id TEXT REFERENCES stories(id), event_type TEXT NOT NULL, status TEXT,
|
|
56
|
+
message TEXT, metadata TEXT, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
57
|
+
);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_agent ON agent_logs(agent_id);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_story ON agent_logs(story_id);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_timestamp ON agent_logs(timestamp);
|
|
61
|
+
CREATE TABLE IF NOT EXISTS escalations (
|
|
62
|
+
id TEXT PRIMARY KEY, story_id TEXT REFERENCES stories(id),
|
|
63
|
+
from_agent_id TEXT REFERENCES agents(id), to_agent_id TEXT REFERENCES agents(id),
|
|
64
|
+
reason TEXT NOT NULL,
|
|
65
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'acknowledged', 'resolved')),
|
|
66
|
+
resolution TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, resolved_at TIMESTAMP
|
|
67
|
+
);
|
|
68
|
+
CREATE TABLE IF NOT EXISTS pull_requests (
|
|
69
|
+
id TEXT PRIMARY KEY, story_id TEXT REFERENCES stories(id), team_id TEXT REFERENCES teams(id),
|
|
70
|
+
branch_name TEXT NOT NULL, github_pr_number INTEGER, github_pr_url TEXT,
|
|
71
|
+
submitted_by TEXT, reviewed_by TEXT,
|
|
72
|
+
status TEXT DEFAULT 'queued' CHECK (status IN ('queued','reviewing','approved','merged','rejected','closed')),
|
|
73
|
+
review_notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
74
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, reviewed_at TIMESTAMP
|
|
75
|
+
);
|
|
76
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
77
|
+
id TEXT PRIMARY KEY, from_session TEXT NOT NULL, to_session TEXT NOT NULL,
|
|
78
|
+
subject TEXT, body TEXT NOT NULL, reply TEXT,
|
|
79
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'read', 'replied')),
|
|
80
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, replied_at TIMESTAMP
|
|
81
|
+
);
|
|
82
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
83
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE,
|
|
84
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
85
|
+
);
|
|
86
|
+
INSERT INTO migrations (name) VALUES ('001-initial.sql');
|
|
87
|
+
INSERT INTO migrations (name) VALUES ('002-add-agent-model.sql');
|
|
88
|
+
INSERT INTO migrations (name) VALUES ('003-fix-pull-requests.sql');
|
|
89
|
+
INSERT INTO migrations (name) VALUES ('004-add-messages.sql');
|
|
90
|
+
INSERT INTO migrations (name) VALUES ('005-add-agent-last-seen.sql');
|
|
91
|
+
INSERT INTO migrations (name) VALUES ('006-add-agent-worktree.sql');
|
|
92
|
+
INSERT INTO migrations (name) VALUES ('007-add-indexes.sql');
|
|
93
|
+
INSERT INTO migrations (name) VALUES ('008-add-godmode.sql');
|
|
94
|
+
INSERT INTO migrations (name) VALUES ('009-add-pr-sync-indexes.sql');
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
if (opts.withData) {
|
|
98
|
+
db.run(
|
|
99
|
+
"INSERT INTO teams VALUES ('t1', 'https://example.com', '/tmp/repo', 'Test Team', datetime('now'))"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (opts.withPadding) {
|
|
104
|
+
db.run('CREATE TABLE IF NOT EXISTS padding (id INTEGER PRIMARY KEY, data TEXT)');
|
|
105
|
+
for (let i = 0; i < 500; i++) {
|
|
106
|
+
db.run('INSERT INTO padding VALUES (?, ?)', [i, 'x'.repeat(100)]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const data = db.export();
|
|
111
|
+
db.close();
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe('createDatabase', () => {
|
|
116
|
+
let tempDir: string;
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
tempDir = mkdtempSync(join(tmpdir(), 'hive-db-test-'));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should create a new database when no file exists', async () => {
|
|
127
|
+
const dbPath = join(tempDir, 'new.db');
|
|
128
|
+
const client = await createDatabase(dbPath);
|
|
129
|
+
|
|
130
|
+
expect(client.db).toBeDefined();
|
|
131
|
+
// New database should not auto-save to disk
|
|
132
|
+
expect(existsSync(dbPath)).toBe(false);
|
|
133
|
+
|
|
134
|
+
client.close();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should load an existing valid database file', async () => {
|
|
138
|
+
const dbPath = join(tempDir, 'existing.db');
|
|
139
|
+
|
|
140
|
+
const data = await createValidDbBuffer({ withData: true });
|
|
141
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
142
|
+
|
|
143
|
+
const client = await createDatabase(dbPath);
|
|
144
|
+
const result = client.db.exec('SELECT COUNT(*) FROM teams');
|
|
145
|
+
expect(result[0].values[0][0]).toBe(1);
|
|
146
|
+
|
|
147
|
+
client.close();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should throw DatabaseCorruptionError when sql.js fails to parse buffer', async () => {
|
|
151
|
+
const dbPath = join(tempDir, 'corrupt.db');
|
|
152
|
+
|
|
153
|
+
// Write garbage data that is not valid SQLite
|
|
154
|
+
const garbageBuffer = Buffer.alloc(100);
|
|
155
|
+
garbageBuffer.write('NOT_A_SQLITE_DATABASE', 0);
|
|
156
|
+
writeFileSync(dbPath, garbageBuffer);
|
|
157
|
+
|
|
158
|
+
await expect(createDatabase(dbPath)).rejects.toThrow(DatabaseCorruptionError);
|
|
159
|
+
await expect(createDatabase(dbPath)).rejects.toThrow('Failed to load database file');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should detect corruption when large file loads with empty core tables', async () => {
|
|
163
|
+
const dbPath = join(tempDir, 'wiped.db');
|
|
164
|
+
|
|
165
|
+
// Create a large database with padding but NO data in core tables
|
|
166
|
+
const data = await createValidDbBuffer({ withData: false, withPadding: true });
|
|
167
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
168
|
+
|
|
169
|
+
// Verify the file is >50KB
|
|
170
|
+
expect(Buffer.from(data).length).toBeGreaterThan(50 * 1024);
|
|
171
|
+
|
|
172
|
+
await expect(createDatabase(dbPath)).rejects.toThrow(DatabaseCorruptionError);
|
|
173
|
+
await expect(createDatabase(dbPath)).rejects.toThrow('zero rows in core tables');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should NOT flag corruption when file is small (<50KB)', async () => {
|
|
177
|
+
const dbPath = join(tempDir, 'small-empty.db');
|
|
178
|
+
|
|
179
|
+
// Create an empty database (no tables yet — migrations will handle it)
|
|
180
|
+
const SQL = await initSqlJs();
|
|
181
|
+
const smallDb = new SQL.Database();
|
|
182
|
+
const data = smallDb.export();
|
|
183
|
+
smallDb.close();
|
|
184
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
185
|
+
|
|
186
|
+
// Verify the file is <50KB
|
|
187
|
+
expect(Buffer.from(data).length).toBeLessThan(50 * 1024);
|
|
188
|
+
|
|
189
|
+
// Should pass — small file skips corruption check, migrations create schema
|
|
190
|
+
const client = await createDatabase(dbPath);
|
|
191
|
+
expect(client.db).toBeDefined();
|
|
192
|
+
client.close();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should NOT flag corruption when large file has data in core tables', async () => {
|
|
196
|
+
const dbPath = join(tempDir, 'large-valid.db');
|
|
197
|
+
|
|
198
|
+
// Create a large database WITH data in core tables
|
|
199
|
+
const data = await createValidDbBuffer({ withData: true, withPadding: true });
|
|
200
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
201
|
+
|
|
202
|
+
expect(Buffer.from(data).length).toBeGreaterThan(50 * 1024);
|
|
203
|
+
|
|
204
|
+
// Should pass — core table has data
|
|
205
|
+
const client = await createDatabase(dbPath);
|
|
206
|
+
expect(client.db).toBeDefined();
|
|
207
|
+
client.close();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('save() backup behavior', () => {
|
|
211
|
+
it('should create a backup file before saving', async () => {
|
|
212
|
+
const dbPath = join(tempDir, 'backup-test.db');
|
|
213
|
+
const backupPath = dbPath + '.bak';
|
|
214
|
+
|
|
215
|
+
const client = await createDatabase(dbPath);
|
|
216
|
+
client.save(); // First save — creates the file, no prior file to back up
|
|
217
|
+
|
|
218
|
+
expect(existsSync(dbPath)).toBe(true);
|
|
219
|
+
|
|
220
|
+
// Save again — now a backup should be created from the existing file
|
|
221
|
+
client.save();
|
|
222
|
+
expect(existsSync(backupPath)).toBe(true);
|
|
223
|
+
|
|
224
|
+
client.close();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should preserve backup content from the previous version', async () => {
|
|
228
|
+
const dbPath = join(tempDir, 'backup-content.db');
|
|
229
|
+
const backupPath = dbPath + '.bak';
|
|
230
|
+
|
|
231
|
+
const client = await createDatabase(dbPath);
|
|
232
|
+
client.db.run('CREATE TABLE IF NOT EXISTS test_data (val TEXT)');
|
|
233
|
+
client.save(); // version 1
|
|
234
|
+
|
|
235
|
+
const firstSaveContent = readFileSync(dbPath);
|
|
236
|
+
|
|
237
|
+
// Modify and save again — version 2
|
|
238
|
+
client.db.run("INSERT INTO test_data VALUES ('version2')");
|
|
239
|
+
client.save();
|
|
240
|
+
|
|
241
|
+
// Backup should contain version 1's content
|
|
242
|
+
const backupContent = readFileSync(backupPath);
|
|
243
|
+
expect(backupContent).toEqual(firstSaveContent);
|
|
244
|
+
|
|
245
|
+
client.close();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('auto-save removal', () => {
|
|
250
|
+
it('should NOT auto-save after initialization of a new database', async () => {
|
|
251
|
+
const dbPath = join(tempDir, 'no-autosave.db');
|
|
252
|
+
|
|
253
|
+
await createDatabase(dbPath);
|
|
254
|
+
|
|
255
|
+
// File should NOT exist because we removed the auto-save on init
|
|
256
|
+
expect(existsSync(dbPath)).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should NOT auto-save when loading an existing database', async () => {
|
|
260
|
+
const dbPath = join(tempDir, 'no-autosave-existing.db');
|
|
261
|
+
|
|
262
|
+
const data = await createValidDbBuffer({ withData: true });
|
|
263
|
+
writeFileSync(dbPath, Buffer.from(data));
|
|
264
|
+
|
|
265
|
+
const originalContent = readFileSync(dbPath);
|
|
266
|
+
|
|
267
|
+
// Loading should not modify the file on disk
|
|
268
|
+
const client = await createDatabase(dbPath);
|
|
269
|
+
|
|
270
|
+
const afterLoadContent = readFileSync(dbPath);
|
|
271
|
+
expect(afterLoadContent).toEqual(originalContent);
|
|
272
|
+
|
|
273
|
+
client.close();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
});
|
package/src/db/client.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Licensed under the Hungry Ghost Hive License. See LICENSE.
|
|
2
2
|
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import initSqlJs, { Database as SqlJsDatabase } from 'sql.js';
|
|
6
|
-
import { InitializationError } from '../errors/index.js';
|
|
6
|
+
import { DatabaseCorruptionError, InitializationError } from '../errors/index.js';
|
|
7
7
|
|
|
8
8
|
export interface DatabaseClient {
|
|
9
9
|
db: SqlJsDatabase;
|
|
@@ -162,33 +162,96 @@ async function getSqlJs(): Promise<typeof SQL> {
|
|
|
162
162
|
return SQL;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// Minimum file size (in bytes) that indicates a database had meaningful data.
|
|
166
|
+
// Files below this threshold are likely new or schema-only databases.
|
|
167
|
+
const CORRUPTION_CHECK_MIN_FILE_SIZE = 50 * 1024; // 50KB
|
|
168
|
+
|
|
169
|
+
// Core tables that should have rows in a populated database
|
|
170
|
+
const CORE_TABLES = ['teams', 'agents', 'stories'];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Validate that a loaded database is not a silently-corrupted empty copy.
|
|
174
|
+
* If the source file was large (>50KB) but the loaded DB has core tables
|
|
175
|
+
* with zero rows, that indicates sql.js silently returned an empty DB.
|
|
176
|
+
*/
|
|
177
|
+
function validateLoadedDatabase(db: SqlJsDatabase, fileSize: number): void {
|
|
178
|
+
if (fileSize < CORRUPTION_CHECK_MIN_FILE_SIZE) {
|
|
179
|
+
return; // Small file — likely a new or schema-only DB, skip validation
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if any core table has data
|
|
183
|
+
for (const table of CORE_TABLES) {
|
|
184
|
+
try {
|
|
185
|
+
const result = db.exec(`SELECT COUNT(*) FROM ${table}`);
|
|
186
|
+
if (result.length > 0 && (result[0].values[0][0] as number) > 0) {
|
|
187
|
+
return; // At least one core table has data — DB looks valid
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
// Table doesn't exist yet — that's fine, migrations haven't run
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// File was large but all core tables are empty — likely corruption
|
|
196
|
+
throw new DatabaseCorruptionError(
|
|
197
|
+
`Database file is ${fileSize} bytes but loaded with zero rows in core tables (${CORE_TABLES.join(', ')}). ` +
|
|
198
|
+
'This likely indicates a corrupted or partially-read database file. ' +
|
|
199
|
+
'Refusing to proceed to prevent data loss. Check the backup at hive.db.bak if available.'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
165
203
|
export async function createDatabase(dbPath: string): Promise<DatabaseClient> {
|
|
166
204
|
const SqlJs = await getSqlJs();
|
|
167
205
|
if (!SqlJs) throw new InitializationError('Failed to initialize sql.js');
|
|
168
206
|
|
|
169
207
|
let db: SqlJsDatabase;
|
|
208
|
+
const backupPath = dbPath + '.bak';
|
|
170
209
|
|
|
171
210
|
// Load existing database or create new one
|
|
172
211
|
if (existsSync(dbPath)) {
|
|
173
212
|
const buffer = readFileSync(dbPath);
|
|
174
|
-
|
|
213
|
+
const fileSize = statSync(dbPath).size;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
db = new SqlJs.Database(buffer);
|
|
217
|
+
// Verify the database is usable by running a basic command
|
|
218
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
219
|
+
db.exec('SELECT 1');
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new DatabaseCorruptionError(
|
|
222
|
+
`Failed to load database file at ${dbPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Validate the loaded DB is not silently empty from a corrupt file
|
|
227
|
+
validateLoadedDatabase(db, fileSize);
|
|
228
|
+
|
|
229
|
+
// Run migrations on loaded DB — wrap in try-catch to detect subtle corruption
|
|
230
|
+
try {
|
|
231
|
+
runMigrations(db);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw new DatabaseCorruptionError(
|
|
234
|
+
`Database file at ${dbPath} appears corrupted (migrations failed): ${error instanceof Error ? error.message : String(error)}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
175
237
|
} else {
|
|
176
238
|
db = new SqlJs.Database();
|
|
239
|
+
// Enable foreign keys
|
|
240
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
241
|
+
// Run migrations on new DB
|
|
242
|
+
runMigrations(db);
|
|
177
243
|
}
|
|
178
244
|
|
|
179
|
-
// Enable foreign keys
|
|
180
|
-
db.run('PRAGMA foreign_keys = ON');
|
|
181
|
-
|
|
182
245
|
const save = () => {
|
|
246
|
+
// Write backup before overwriting the main database file
|
|
247
|
+
if (existsSync(dbPath)) {
|
|
248
|
+
copyFileSync(dbPath, backupPath);
|
|
249
|
+
}
|
|
183
250
|
const data = db.export();
|
|
184
251
|
const buffer = Buffer.from(data);
|
|
185
252
|
writeFileSync(dbPath, buffer);
|
|
186
253
|
};
|
|
187
254
|
|
|
188
|
-
// Auto-run migrations
|
|
189
|
-
runMigrations(db);
|
|
190
|
-
save();
|
|
191
|
-
|
|
192
255
|
const client: DatabaseClient = {
|
|
193
256
|
db,
|
|
194
257
|
close: () => {
|
package/src/errors/index.ts
CHANGED
|
@@ -104,6 +104,16 @@ export class NotFoundError extends HiveError {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Database corruption errors (empty DB loaded from non-empty file, parse failures)
|
|
109
|
+
*/
|
|
110
|
+
export class DatabaseCorruptionError extends HiveError {
|
|
111
|
+
constructor(message: string) {
|
|
112
|
+
super(message, 'DATABASE_CORRUPTION_ERROR');
|
|
113
|
+
Object.setPrototypeOf(this, DatabaseCorruptionError.prototype);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
107
117
|
/**
|
|
108
118
|
* Helper function to convert generic errors to Hive errors
|
|
109
119
|
*/
|