UNIQUE(item_id, chunk_seq) + indices on item_id and sha1_hex. 1 new db test (127 total). No producer wired up yet — that comes with the Plan 04b integration into the live recorder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
5.5 KiB
TypeScript
138 lines
5.5 KiB
TypeScript
import { test, expect, describe, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { openDatabase, type DbHandle } from './db';
|
|
|
|
let tmpDir: string;
|
|
let db: DbHandle | null = null;
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'db-test-'));
|
|
});
|
|
afterEach(() => {
|
|
try { db?.close(); } catch { /* ignore */ }
|
|
db = null;
|
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
});
|
|
|
|
describe('openDatabase', () => {
|
|
test('creates a new file', () => {
|
|
const target = path.join(tmpDir, 'a.db');
|
|
db = openDatabase(target);
|
|
expect(fs.existsSync(target)).toBe(true);
|
|
expect(typeof db.run).toBe('function');
|
|
expect(typeof db.get).toBe('function');
|
|
expect(typeof db.all).toBe('function');
|
|
expect(typeof db.close).toBe('function');
|
|
expect(typeof db.transaction).toBe('function');
|
|
expect(typeof db.runBatch).toBe('function');
|
|
});
|
|
|
|
test('schema_meta row exists with schema_version=5', () => {
|
|
db = openDatabase(path.join(tmpDir, 'b.db'));
|
|
const row = db.get<{ value: string }>('SELECT value FROM schema_meta WHERE key = ?', ['schema_version']);
|
|
expect(row?.value).toBe('5');
|
|
});
|
|
|
|
test('WAL mode active', () => {
|
|
db = openDatabase(path.join(tmpDir, 'c.db'));
|
|
const row = db.get<{ journal_mode: string }>('PRAGMA journal_mode');
|
|
expect(row?.journal_mode).toBe('wal');
|
|
});
|
|
|
|
test('idempotent open: existing file keeps schema_version=5', () => {
|
|
const target = path.join(tmpDir, 'd.db');
|
|
db = openDatabase(target);
|
|
db.close();
|
|
db = openDatabase(target);
|
|
const row = db.get<{ value: string }>('SELECT value FROM schema_meta WHERE key = ?', ['schema_version']);
|
|
expect(row?.value).toBe('5');
|
|
});
|
|
|
|
test('run + get + all roundtrip on downloaded_vods', () => {
|
|
db = openDatabase(path.join(tmpDir, 'e.db'));
|
|
db.run('INSERT INTO downloaded_vods(vod_id) VALUES (?)', ['1234']);
|
|
db.run('INSERT INTO downloaded_vods(vod_id) VALUES (?)', ['5678']);
|
|
const one = db.get<{ vod_id: string }>('SELECT vod_id FROM downloaded_vods WHERE vod_id = ?', ['1234']);
|
|
expect(one?.vod_id).toBe('1234');
|
|
const all = db.all<{ vod_id: string }>('SELECT vod_id FROM downloaded_vods ORDER BY vod_id');
|
|
expect(all.map(r => r.vod_id)).toEqual(['1234', '5678']);
|
|
});
|
|
|
|
test('transaction commits as bracket', () => {
|
|
db = openDatabase(path.join(tmpDir, 'f.db'));
|
|
const handle = db;
|
|
const inserted = handle.transaction(() => {
|
|
handle.run('INSERT INTO downloaded_vods(vod_id) VALUES (?)', ['t1']);
|
|
handle.run('INSERT INTO downloaded_vods(vod_id) VALUES (?)', ['t2']);
|
|
return 2;
|
|
});
|
|
expect(inserted).toBe(2);
|
|
const c = handle.get<{ c: number }>('SELECT COUNT(*) AS c FROM downloaded_vods');
|
|
expect(c?.c).toBe(2);
|
|
});
|
|
|
|
test('chunk_index table accepts insert + UNIQUE(item_id, chunk_seq)', () => {
|
|
db = openDatabase(path.join(tmpDir, 'chunk.db'));
|
|
db.run(
|
|
'INSERT INTO chunk_index(item_id, chunk_seq, sha1_hex, bytes) VALUES (?, ?, ?, ?)',
|
|
['item1', 0, 'abc123', 1024]
|
|
);
|
|
const handle = db;
|
|
expect(() => {
|
|
handle.run(
|
|
'INSERT INTO chunk_index(item_id, chunk_seq, sha1_hex, bytes) VALUES (?, ?, ?, ?)',
|
|
['item1', 0, 'different', 2048]
|
|
);
|
|
}).toThrow(); // UNIQUE violation
|
|
const rows = handle.all<{ sha1_hex: string }>('SELECT sha1_hex FROM chunk_index WHERE item_id = ?', ['item1']);
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].sha1_hex).toBe('abc123');
|
|
});
|
|
|
|
test('oauth_accounts table exists and accepts insert', () => {
|
|
db = openDatabase(path.join(tmpDir, 'oauth.db'));
|
|
db.run(
|
|
`INSERT INTO oauth_accounts(provider, twitch_user_id, login, encrypted_access_token)
|
|
VALUES (?, ?, ?, ?)`,
|
|
['twitch', 'user-123', 'alice', 'ciphertext-blob']
|
|
);
|
|
const row = db.get<{ login: string; provider: string }>(
|
|
'SELECT login, provider FROM oauth_accounts WHERE twitch_user_id = ?',
|
|
['user-123']
|
|
);
|
|
expect(row?.login).toBe('alice');
|
|
expect(row?.provider).toBe('twitch');
|
|
});
|
|
|
|
test('oauth_accounts UNIQUE(provider, twitch_user_id) enforced', () => {
|
|
db = openDatabase(path.join(tmpDir, 'oauth-unique.db'));
|
|
db.run(
|
|
`INSERT INTO oauth_accounts(provider, twitch_user_id, login, encrypted_access_token)
|
|
VALUES (?, ?, ?, ?)`,
|
|
['twitch', 'u1', 'a', 'x']
|
|
);
|
|
const handle = db;
|
|
expect(() => {
|
|
handle.run(
|
|
`INSERT INTO oauth_accounts(provider, twitch_user_id, login, encrypted_access_token)
|
|
VALUES (?, ?, ?, ?)`,
|
|
['twitch', 'u1', 'b', 'y']
|
|
);
|
|
}).toThrow();
|
|
});
|
|
|
|
test('transaction rolls back on throw', () => {
|
|
db = openDatabase(path.join(tmpDir, 'g.db'));
|
|
const handle = db;
|
|
expect(() => {
|
|
handle.transaction(() => {
|
|
handle.run('INSERT INTO downloaded_vods(vod_id) VALUES (?)', ['x1']);
|
|
throw new Error('boom');
|
|
});
|
|
}).toThrow('boom');
|
|
const c = handle.get<{ c: number }>('SELECT COUNT(*) AS c FROM downloaded_vods');
|
|
expect(c?.c).toBe(0);
|
|
});
|
|
});
|