@hasna/recordings 0.0.3

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/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@hasna/recordings",
3
+ "version": "0.0.3",
4
+ "type": "module",
5
+ "description": "Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "recordings": "dist/cli/index.js",
10
+ "recordings-mcp": "dist/mcp/index.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "bun run build:cli && bun run build:mcp && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
20
+ "build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
21
+ "build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
22
+ "build:lib": "bun build src/index.ts --target=bun --outfile=dist/index.js --external=openai",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "bun test",
25
+ "test:coverage": "bun test --coverage",
26
+ "dev:cli": "bun run src/cli/index.ts",
27
+ "dev:mcp": "bun run src/mcp/index.ts"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.12.1",
31
+ "chalk": "^5.4.1",
32
+ "commander": "^13.1.0",
33
+ "openai": "^5.1.0",
34
+ "zod": "^3.24.2"
35
+ },
36
+ "devDependencies": {
37
+ "@types/bun": "^1.2.5",
38
+ "typescript": "^5.8.2"
39
+ },
40
+ "publishConfig": {
41
+ "access": "restricted",
42
+ "registry": "https://registry.npmjs.org/"
43
+ },
44
+ "license": "MIT",
45
+ "author": "Hasna <andrei@hasna.com>"
46
+ }
@@ -0,0 +1,136 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { mkdirSync, rmSync, existsSync } from "fs";
5
+ import {
6
+ getDatabase,
7
+ closeDatabase,
8
+ resetDatabase,
9
+ } from "../db/database.js";
10
+ import { registerAgent, getAgent, listAgents } from "../db/agents.js";
11
+ import { type Database } from "bun:sqlite";
12
+
13
+ let tempDir: string;
14
+ let db: Database;
15
+
16
+ beforeEach(() => {
17
+ resetDatabase();
18
+ tempDir = join(tmpdir(), `open-recordings-test-agents-${Date.now()}-${Math.random().toString(36).slice(2)}`);
19
+ mkdirSync(tempDir, { recursive: true });
20
+ const dbPath = join(tempDir, "test.db");
21
+ db = getDatabase(dbPath);
22
+ });
23
+
24
+ afterEach(() => {
25
+ closeDatabase();
26
+ resetDatabase();
27
+ if (existsSync(tempDir)) {
28
+ rmSync(tempDir, { recursive: true, force: true });
29
+ }
30
+ });
31
+
32
+ describe("registerAgent", () => {
33
+ test("creates a new agent with name only", () => {
34
+ const agent = registerAgent("maximus", undefined, undefined, db);
35
+ expect(agent).toBeDefined();
36
+ expect(agent.name).toBe("maximus");
37
+ expect(agent.role).toBe("agent");
38
+ expect(agent.description).toBeNull();
39
+ expect(agent.metadata).toEqual({});
40
+ expect(agent.id).toHaveLength(8);
41
+ expect(agent.created_at).toBeDefined();
42
+ expect(agent.last_seen_at).toBeDefined();
43
+ });
44
+
45
+ test("creates a new agent with description and role", () => {
46
+ const agent = registerAgent("cassius", "A helper agent", "assistant", db);
47
+ expect(agent.name).toBe("cassius");
48
+ expect(agent.description).toBe("A helper agent");
49
+ expect(agent.role).toBe("assistant");
50
+ });
51
+
52
+ test("returns existing agent and updates last_seen_at (idempotent)", () => {
53
+ const first = registerAgent("aurelius", undefined, undefined, db);
54
+ // Wait a tiny bit to ensure timestamp difference
55
+ const second = registerAgent("aurelius", undefined, undefined, db);
56
+
57
+ expect(second.id).toBe(first.id);
58
+ expect(second.name).toBe("aurelius");
59
+ // last_seen_at should be updated
60
+ expect(new Date(second.last_seen_at).getTime()).toBeGreaterThanOrEqual(
61
+ new Date(first.last_seen_at).getTime()
62
+ );
63
+ });
64
+
65
+ test("does not duplicate agents on re-register", () => {
66
+ registerAgent("brutus", undefined, undefined, db);
67
+ registerAgent("brutus", undefined, undefined, db);
68
+ registerAgent("brutus", undefined, undefined, db);
69
+
70
+ const agents = listAgents(db);
71
+ const brutusAgents = agents.filter((a) => a.name === "brutus");
72
+ expect(brutusAgents).toHaveLength(1);
73
+ });
74
+ });
75
+
76
+ describe("getAgent", () => {
77
+ test("finds agent by ID", () => {
78
+ const created = registerAgent("titus", undefined, undefined, db);
79
+ const found = getAgent(created.id, db);
80
+ expect(found).toBeDefined();
81
+ expect(found!.name).toBe("titus");
82
+ });
83
+
84
+ test("finds agent by name", () => {
85
+ registerAgent("nero", "The emperor", undefined, db);
86
+ const found = getAgent("nero", db);
87
+ expect(found).toBeDefined();
88
+ expect(found!.description).toBe("The emperor");
89
+ });
90
+
91
+ test("finds agent by partial ID prefix", () => {
92
+ const created = registerAgent("cicero", undefined, undefined, db);
93
+ const prefix = created.id.substring(0, 4);
94
+ const found = getAgent(prefix, db);
95
+ expect(found).toBeDefined();
96
+ expect(found!.name).toBe("cicero");
97
+ });
98
+
99
+ test("returns null for non-existent agent", () => {
100
+ const found = getAgent("nonexistent", db);
101
+ expect(found).toBeNull();
102
+ });
103
+ });
104
+
105
+ describe("listAgents", () => {
106
+ test("returns empty array when no agents", () => {
107
+ const agents = listAgents(db);
108
+ expect(agents).toEqual([]);
109
+ });
110
+
111
+ test("returns all agents ordered by last_seen_at DESC", () => {
112
+ registerAgent("alpha", undefined, undefined, db);
113
+ registerAgent("beta", undefined, undefined, db);
114
+ registerAgent("gamma", undefined, undefined, db);
115
+
116
+ const agents = listAgents(db);
117
+ expect(agents).toHaveLength(3);
118
+ // All three agents are present
119
+ const names = agents.map((a) => a.name);
120
+ expect(names).toContain("alpha");
121
+ expect(names).toContain("beta");
122
+ expect(names).toContain("gamma");
123
+ });
124
+
125
+ test("parses metadata JSON correctly", () => {
126
+ const agent = registerAgent("delta", undefined, undefined, db);
127
+ // Manually update metadata for testing
128
+ db.query("UPDATE agents SET metadata = ? WHERE id = ?").run(
129
+ JSON.stringify({ tool: "test" }),
130
+ agent.id
131
+ );
132
+
133
+ const found = getAgent(agent.id, db);
134
+ expect(found!.metadata).toEqual({ tool: "test" });
135
+ });
136
+ });
@@ -0,0 +1,252 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs";
5
+ import { loadConfig, getDataDir, ensureDataDir, DEFAULT_CONFIG } from "../lib/config.js";
6
+
7
+ let tempDir: string;
8
+
9
+ // Save and restore env vars
10
+ const savedEnv: Record<string, string | undefined> = {};
11
+ const envKeys = [
12
+ "OPENAI_API_KEY",
13
+ "RECORDINGS_API_KEY",
14
+ "RECORDINGS_ENHANCEMENT_KEY",
15
+ "RECORDINGS_MODEL",
16
+ "RECORDINGS_ENHANCEMENT_MODEL",
17
+ "RECORDINGS_LANGUAGE",
18
+ "RECORDINGS_DB_PATH",
19
+ "RECORDINGS_AUDIO_DIR",
20
+ "RECORDINGS_MAX_SECONDS",
21
+ ];
22
+
23
+ beforeEach(() => {
24
+ tempDir = join(tmpdir(), `open-recordings-test-config-${Date.now()}-${Math.random().toString(36).slice(2)}`);
25
+ mkdirSync(tempDir, { recursive: true });
26
+
27
+ // Save env vars
28
+ for (const key of envKeys) {
29
+ savedEnv[key] = process.env[key];
30
+ delete process.env[key];
31
+ }
32
+ });
33
+
34
+ afterEach(() => {
35
+ // Restore env vars
36
+ for (const key of envKeys) {
37
+ if (savedEnv[key] !== undefined) {
38
+ process.env[key] = savedEnv[key];
39
+ } else {
40
+ delete process.env[key];
41
+ }
42
+ }
43
+
44
+ if (existsSync(tempDir)) {
45
+ rmSync(tempDir, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ describe("DEFAULT_CONFIG", () => {
50
+ test("has expected default values", () => {
51
+ expect(DEFAULT_CONFIG.transcription_model).toBe("gpt-4o-mini-transcribe");
52
+ expect(DEFAULT_CONFIG.enhancement_model).toBe("gpt-4o");
53
+ expect(DEFAULT_CONFIG.language).toBe("en");
54
+ expect(DEFAULT_CONFIG.audio_format).toBe("wav");
55
+ expect(DEFAULT_CONFIG.sample_rate).toBe(16000);
56
+ expect(DEFAULT_CONFIG.record_command).toBe("sox");
57
+ expect(DEFAULT_CONFIG.hotkey).toBe("space");
58
+ expect(DEFAULT_CONFIG.auto_enhance).toBe(true);
59
+ expect(DEFAULT_CONFIG.max_recording_seconds).toBe(300);
60
+ expect(DEFAULT_CONFIG.enhance_triggers).toContain("say it better");
61
+ expect(DEFAULT_CONFIG.enhance_triggers).toContain("rewrite this");
62
+ });
63
+ });
64
+
65
+ describe("loadConfig", () => {
66
+ test("returns defaults when no config file or env vars", () => {
67
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
68
+ expect(config.transcription_model).toBe("gpt-4o-mini-transcribe");
69
+ expect(config.enhancement_model).toBe("gpt-4o");
70
+ expect(config.language).toBe("en");
71
+ expect(config.audio_format).toBe("wav");
72
+ expect(config.auto_enhance).toBe(true);
73
+ });
74
+
75
+ test("loads config from file", () => {
76
+ const configPath = join(tempDir, "config.json");
77
+ writeFileSync(
78
+ configPath,
79
+ JSON.stringify({
80
+ transcription_model: "whisper-1",
81
+ language: "fr",
82
+ auto_enhance: false,
83
+ })
84
+ );
85
+
86
+ const config = loadConfig(configPath);
87
+ expect(config.transcription_model).toBe("whisper-1");
88
+ expect(config.language).toBe("fr");
89
+ expect(config.auto_enhance).toBe(false);
90
+ // Other defaults still present
91
+ expect(config.audio_format).toBe("wav");
92
+ });
93
+
94
+ test("ignores invalid JSON config file", () => {
95
+ const configPath = join(tempDir, "bad-config.json");
96
+ writeFileSync(configPath, "this is not json {{{");
97
+
98
+ const config = loadConfig(configPath);
99
+ // Should fall back to defaults
100
+ expect(config.transcription_model).toBe("gpt-4o-mini-transcribe");
101
+ });
102
+
103
+ test("env var OPENAI_API_KEY overrides config", () => {
104
+ process.env.OPENAI_API_KEY = "sk-env-key";
105
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
106
+ expect(config.openai_api_key).toBe("sk-env-key");
107
+ });
108
+
109
+ test("env var RECORDINGS_API_KEY overrides OPENAI_API_KEY", () => {
110
+ process.env.OPENAI_API_KEY = "sk-openai";
111
+ process.env.RECORDINGS_API_KEY = "sk-recordings";
112
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
113
+ expect(config.openai_api_key).toBe("sk-recordings");
114
+ });
115
+
116
+ test("env var RECORDINGS_ENHANCEMENT_KEY sets enhancement_api_key", () => {
117
+ process.env.RECORDINGS_ENHANCEMENT_KEY = "sk-enhance";
118
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
119
+ expect(config.enhancement_api_key).toBe("sk-enhance");
120
+ });
121
+
122
+ test("env var RECORDINGS_MODEL overrides transcription_model", () => {
123
+ process.env.RECORDINGS_MODEL = "whisper-1";
124
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
125
+ expect(config.transcription_model).toBe("whisper-1");
126
+ });
127
+
128
+ test("env var RECORDINGS_ENHANCEMENT_MODEL overrides enhancement_model", () => {
129
+ process.env.RECORDINGS_ENHANCEMENT_MODEL = "gpt-3.5-turbo";
130
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
131
+ expect(config.enhancement_model).toBe("gpt-3.5-turbo");
132
+ });
133
+
134
+ test("env var RECORDINGS_LANGUAGE overrides language", () => {
135
+ process.env.RECORDINGS_LANGUAGE = "de";
136
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
137
+ expect(config.language).toBe("de");
138
+ });
139
+
140
+ test("env var RECORDINGS_DB_PATH overrides db_path", () => {
141
+ process.env.RECORDINGS_DB_PATH = "/custom/db.sqlite";
142
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
143
+ expect(config.db_path).toBe("/custom/db.sqlite");
144
+ });
145
+
146
+ test("env var RECORDINGS_AUDIO_DIR overrides audio_dir", () => {
147
+ process.env.RECORDINGS_AUDIO_DIR = "/custom/audio";
148
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
149
+ expect(config.audio_dir).toBe("/custom/audio");
150
+ });
151
+
152
+ test("env var RECORDINGS_MAX_SECONDS overrides max_recording_seconds", () => {
153
+ process.env.RECORDINGS_MAX_SECONDS = "60";
154
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
155
+ expect(config.max_recording_seconds).toBe(60);
156
+ });
157
+
158
+ test("sets default db_path when not configured", () => {
159
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
160
+ expect(config.db_path).toBeTruthy();
161
+ expect(config.db_path).toContain("recordings.db");
162
+ });
163
+
164
+ test("sets default audio_dir when not configured", () => {
165
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
166
+ expect(config.audio_dir).toBeTruthy();
167
+ expect(config.audio_dir).toContain("audio");
168
+ });
169
+
170
+ test("enhancement_api_key falls back to openai_api_key", () => {
171
+ process.env.OPENAI_API_KEY = "sk-shared";
172
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
173
+ expect(config.enhancement_api_key).toBe("sk-shared");
174
+ });
175
+
176
+ test("file config values are overridden by env vars", () => {
177
+ const configPath = join(tempDir, "config.json");
178
+ writeFileSync(
179
+ configPath,
180
+ JSON.stringify({ language: "fr", transcription_model: "file-model" })
181
+ );
182
+ process.env.RECORDINGS_LANGUAGE = "ja";
183
+
184
+ const config = loadConfig(configPath);
185
+ expect(config.language).toBe("ja"); // env wins
186
+ expect(config.transcription_model).toBe("file-model"); // no env override for this
187
+ });
188
+ });
189
+
190
+ describe("ensureDataDir", () => {
191
+ test("creates audio_dir and db directory", () => {
192
+ const audioDir = join(tempDir, "audio-out");
193
+ const dbPath = join(tempDir, "db-dir/recordings.db");
194
+ const config = {
195
+ ...DEFAULT_CONFIG,
196
+ audio_dir: audioDir,
197
+ db_path: dbPath,
198
+ };
199
+
200
+ ensureDataDir(config);
201
+ expect(existsSync(audioDir)).toBe(true);
202
+ expect(existsSync(join(tempDir, "db-dir"))).toBe(true);
203
+ });
204
+ });
205
+
206
+ describe("getDataDir", () => {
207
+ test("returns a string", () => {
208
+ const dir = getDataDir();
209
+ expect(typeof dir).toBe("string");
210
+ expect(dir).toContain(".recordings");
211
+ });
212
+ });
213
+
214
+ describe("loadSecretKey (via loadConfig)", () => {
215
+ test("loads API key from ~/.secrets with double quotes", () => {
216
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
217
+ // The key is either loaded from ~/.secrets or empty
218
+ expect(typeof config.openai_api_key).toBe("string");
219
+ });
220
+
221
+ test("loads secret key with single quotes format", () => {
222
+ // We can't easily mock ~/.secrets, but we test the regex patterns directly
223
+ // by testing the config loading with different env overrides
224
+ // The loadSecretKey function is internal, so we test its effect indirectly
225
+ // When no env var is set and no ~/.secrets has the key, it returns ""
226
+ const config = loadConfig(join(tempDir, "nonexistent.json"));
227
+ // At minimum, the function doesn't crash
228
+ expect(config).toBeDefined();
229
+ });
230
+ });
231
+
232
+ describe("findConfigFile (via loadConfig without explicit path)", () => {
233
+ test("loads config without explicit path (uses findConfigFile)", () => {
234
+ // When no configPath is given, findConfigFile walks up from cwd
235
+ const config = loadConfig();
236
+ expect(config).toBeDefined();
237
+ expect(config.transcription_model).toBeTruthy();
238
+ });
239
+ });
240
+
241
+ describe("ensureDataDir edge cases", () => {
242
+ test("handles db_path without directory separator", () => {
243
+ const config = {
244
+ ...DEFAULT_CONFIG,
245
+ audio_dir: join(tempDir, "audio"),
246
+ db_path: "recordings.db", // no directory part
247
+ };
248
+ // Should not throw - dbDir will be empty string
249
+ ensureDataDir(config);
250
+ expect(existsSync(join(tempDir, "audio"))).toBe(true);
251
+ });
252
+ });
@@ -0,0 +1,167 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { mkdirSync, rmSync, existsSync } from "fs";
5
+ import {
6
+ getDatabase,
7
+ closeDatabase,
8
+ resetDatabase,
9
+ shortUuid,
10
+ } from "../db/database.js";
11
+
12
+ let tempDir: string;
13
+
14
+ beforeEach(() => {
15
+ resetDatabase();
16
+ tempDir = join(tmpdir(), `open-recordings-test-db-${Date.now()}-${Math.random().toString(36).slice(2)}`);
17
+ mkdirSync(tempDir, { recursive: true });
18
+ });
19
+
20
+ afterEach(() => {
21
+ closeDatabase();
22
+ resetDatabase();
23
+ if (existsSync(tempDir)) {
24
+ rmSync(tempDir, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ describe("getDatabase", () => {
29
+ test("creates database file and returns a database instance", () => {
30
+ const dbPath = join(tempDir, "test.db");
31
+ const db = getDatabase(dbPath);
32
+ expect(db).toBeDefined();
33
+ expect(existsSync(dbPath)).toBe(true);
34
+ });
35
+
36
+ test("creates parent directories if they don't exist", () => {
37
+ const dbPath = join(tempDir, "nested", "deep", "test.db");
38
+ const db = getDatabase(dbPath);
39
+ expect(db).toBeDefined();
40
+ expect(existsSync(dbPath)).toBe(true);
41
+ });
42
+
43
+ test("returns the same instance on subsequent calls (singleton)", () => {
44
+ const dbPath = join(tempDir, "test.db");
45
+ const db1 = getDatabase(dbPath);
46
+ const db2 = getDatabase(dbPath);
47
+ expect(db1).toBe(db2);
48
+ });
49
+
50
+ test("runs migrations on creation", () => {
51
+ const dbPath = join(tempDir, "test.db");
52
+ const db = getDatabase(dbPath);
53
+
54
+ // Check that the tables exist
55
+ const tables = db
56
+ .query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
57
+ .all() as { name: string }[];
58
+ const tableNames = tables.map((t) => t.name);
59
+
60
+ expect(tableNames).toContain("recordings");
61
+ expect(tableNames).toContain("agents");
62
+ expect(tableNames).toContain("projects");
63
+ expect(tableNames).toContain("recording_tags");
64
+ expect(tableNames).toContain("_migrations");
65
+ });
66
+
67
+ test("sets WAL journal mode", () => {
68
+ const dbPath = join(tempDir, "test.db");
69
+ const db = getDatabase(dbPath);
70
+ const result = db.query("PRAGMA journal_mode").get() as { journal_mode: string } | null;
71
+ expect(result?.journal_mode).toBe("wal");
72
+ });
73
+
74
+ test("enables foreign keys", () => {
75
+ const dbPath = join(tempDir, "test.db");
76
+ const db = getDatabase(dbPath);
77
+ const result = db.query("PRAGMA foreign_keys").get() as { foreign_keys: number } | null;
78
+ expect(result?.foreign_keys).toBe(1);
79
+ });
80
+
81
+ test("records migration level", () => {
82
+ const dbPath = join(tempDir, "test.db");
83
+ const db = getDatabase(dbPath);
84
+ const result = db
85
+ .query("SELECT MAX(id) as max_id FROM _migrations")
86
+ .get() as { max_id: number };
87
+ expect(result.max_id).toBe(0);
88
+ });
89
+
90
+ test("does not re-run migrations on second open", () => {
91
+ const dbPath = join(tempDir, "test.db");
92
+ const db1 = getDatabase(dbPath);
93
+ // Insert a test row
94
+ db1.query("INSERT INTO agents (id, name, created_at, last_seen_at) VALUES (?, ?, ?, ?)").run(
95
+ "test-id",
96
+ "test-agent",
97
+ new Date().toISOString(),
98
+ new Date().toISOString()
99
+ );
100
+ closeDatabase();
101
+ resetDatabase();
102
+
103
+ // Re-open
104
+ const db2 = getDatabase(dbPath);
105
+ const agent = db2.query("SELECT * FROM agents WHERE id = ?").get("test-id") as Record<string, unknown> | undefined;
106
+ expect(agent).toBeDefined();
107
+ expect(agent!["name"]).toBe("test-agent");
108
+ });
109
+ });
110
+
111
+ describe("closeDatabase", () => {
112
+ test("closes the database and allows reset", () => {
113
+ const dbPath = join(tempDir, "test.db");
114
+ getDatabase(dbPath);
115
+ closeDatabase();
116
+ resetDatabase();
117
+
118
+ // Should be able to open a new database
119
+ const dbPath2 = join(tempDir, "test2.db");
120
+ const db2 = getDatabase(dbPath2);
121
+ expect(db2).toBeDefined();
122
+ });
123
+
124
+ test("is a no-op when no database is open", () => {
125
+ // Should not throw
126
+ closeDatabase();
127
+ });
128
+ });
129
+
130
+ describe("resetDatabase", () => {
131
+ test("clears the singleton so next getDatabase creates new instance", () => {
132
+ const dbPath1 = join(tempDir, "test1.db");
133
+ const db1 = getDatabase(dbPath1);
134
+ closeDatabase();
135
+ resetDatabase();
136
+
137
+ const dbPath2 = join(tempDir, "test2.db");
138
+ const db2 = getDatabase(dbPath2);
139
+ expect(db2).not.toBe(db1);
140
+ });
141
+ });
142
+
143
+ describe("getDbPath", () => {
144
+ test("returns the db_path from config", async () => {
145
+ const { getDbPath } = await import("../db/database.js");
146
+ const path = getDbPath();
147
+ expect(typeof path).toBe("string");
148
+ expect(path).toContain("recordings.db");
149
+ });
150
+ });
151
+
152
+ describe("shortUuid", () => {
153
+ test("returns an 8-character string", () => {
154
+ const id = shortUuid();
155
+ expect(id).toHaveLength(8);
156
+ });
157
+
158
+ test("returns different values on each call", () => {
159
+ const ids = new Set(Array.from({ length: 100 }, () => shortUuid()));
160
+ expect(ids.size).toBe(100);
161
+ });
162
+
163
+ test("only contains valid UUID characters", () => {
164
+ const id = shortUuid();
165
+ expect(id).toMatch(/^[0-9a-f]{8}$/);
166
+ });
167
+ });