import Database from 'better-sqlite3'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => { vi.restoreAllMocks(); await fixture.cleanup(); }); it('initializes every schema on first open and runs no migration SQL on later opens', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const { databaseVersion } = await import('../../src/lib/server/db/migrations.js'); const handle = db(); expect(handle.pragma('user_version', { simple: true })).toBe(databaseVersion); for (const table of [ 'servers', 'native_instances', 'ec_hosts', 'auth_users', 'game_file_versions' ]) expect(handle.prepare('SELECT name FROM sqlite_master WHERE name=?').get(table)).toEqual({ name: table }); handle.exec( "INSERT INTO servers(name,last_reply_snippet) VALUES('saved','sanitized diagnostic')" ); const schemaVersion = handle.pragma('schema_version', { simple: true }); closeDb(); const exec = vi.spyOn(Database.prototype, 'exec'); const prepare = vi.spyOn(Database.prototype, 'prepare'); const reopened = db(); expect(exec).not.toHaveBeenCalled(); expect(prepare).not.toHaveBeenCalled(); expect(reopened.pragma('schema_version', { simple: true })).toBe(schemaVersion); expect(reopened.prepare('SELECT name,last_reply_snippet FROM servers').get()).toEqual({ name: 'saved', last_reply_snippet: 'sanitized diagnostic' }); expect(reopened.pragma('foreign_key_check')).toEqual([]); }); it('adopts a fully migrated unversioned database without replaying destructive backfills', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const { databaseVersion } = await import('../../src/lib/server/db/migrations.js'); const handle = db(); handle.exec(`INSERT INTO servers(name,desired_state,last_reply_snippet) VALUES('saved','stopped','keep'); INSERT INTO file_blobs(sha256,size,content) VALUES('hash',1,X'01'); INSERT INTO scripts(id,name,target_rel_path) VALUES(1,'saved','test.gsc'); INSERT INTO script_versions(id,script_id,version,sha256,size,content_hash) VALUES(1,1,1,'hash',1,'hash'); INSERT INTO betas(id,script_id,script_version_id,trigger_kind,state) VALUES(1,1,1,'bundle','running'); INSERT INTO beta_test_plans(beta_id,plan,outcome) VALUES(1,'{"requirements":[]}','active');`); // All pre-versioning releases used 0; their sentinels are compatibility inputs. handle.pragma('user_version = 0'); closeDb(); expect(db().pragma('user_version', { simple: true })).toBe(databaseVersion); expect(db().prepare('SELECT desired_state,last_reply_snippet FROM servers').get()).toEqual({ desired_state: 'stopped', last_reply_snippet: 'keep' }); expect(db().prepare('SELECT state FROM betas').get()).toEqual({ state: 'running' }); expect(db().prepare('SELECT plan FROM beta_test_plans').get()).toEqual({ plan: '{"requirements":[]}' }); expect(db().pragma('foreign_key_check')).toEqual([]); }); it('closes a failed startup handle and resumes from the last committed version', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const { migrations, databaseVersion } = await import('../../src/lib/server/db/migrations.js'); const hosts = migrations.find( (migration) => migration.name === 'hosts and saved command files' )!; const run = hosts.run; const failure = vi.spyOn(hosts, 'run').mockImplementation((handle) => { run(handle); throw new Error('interrupted host upgrade'); }); expect(db).toThrow('interrupted host upgrade'); const inspect = new Database(fixture.dbPath); try { expect(inspect.pragma('user_version', { simple: true })).toBe(hosts.version - 1); expect( inspect.prepare("SELECT 1 FROM sqlite_master WHERE name='ec_hosts'").get() ).toBeUndefined(); } finally { inspect.close(); } failure.mockRestore(); const completed = migrations .slice(0, hosts.version - 1) .map((migration) => vi.spyOn(migration, 'run')); expect(db().pragma('user_version', { simple: true })).toBe(databaseVersion); for (const migration of completed) expect(migration).not.toHaveBeenCalled(); expect(db().pragma('foreign_key_check')).toEqual([]); }); it('keeps the legacy snapshot safety guard and rolls back an unsafe first upgrade', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const handle = db(); handle.exec(`INSERT INTO file_blobs(sha256,size,content) VALUES('hash',1,X'01'); INSERT INTO scripts(id,name,target_rel_path) VALUES(1,'saved','test.gsc'); INSERT INTO script_versions(id,script_id,version,sha256,size,content_hash) VALUES(1,1,1,'hash',1,'hash'); INSERT INTO betas(id,script_id,script_version_id,trigger_kind,state) VALUES(1,1,1,'bundle','running'); DELETE FROM settings WHERE key='snapshots_v1';`); handle.pragma('user_version = 0'); closeDb(); expect(db).toThrow('close active beta 1 before upgrading'); const inspect = new Database(fixture.dbPath); try { expect(inspect.pragma('user_version', { simple: true })).toBe(0); expect(inspect.prepare('SELECT state FROM betas').get()).toEqual({ state: 'running' }); // The join backfill precedes the guard, but must roll back with it. expect(inspect.prepare('SELECT * FROM beta_scripts').all()).toEqual([]); expect(inspect.pragma('foreign_key_check')).toEqual([]); } finally { inspect.close(); } }); it('rejects a database from a newer application without changing its version or schema', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const { databaseVersion } = await import('../../src/lib/server/db/migrations.js'); db().pragma(`user_version = ${databaseVersion + 1}`); const schemaVersion = db().pragma('schema_version', { simple: true }); closeDb(); expect(db).toThrow('not supported by this application'); const inspect = new Database(fixture.dbPath); try { expect(inspect.pragma('user_version', { simple: true })).toBe(databaseVersion + 1); expect(inspect.pragma('schema_version', { simple: true })).toBe(schemaVersion); } finally { inspect.close(); } }); it('adds policy report invalidation when upgrading a database at version 12', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const handle = db(); for (const table of ['autoscaling_policies', 'settings']) for (const action of ['INSERT', 'UPDATE', 'DELETE']) handle.exec(`DROP TRIGGER report_metrics_${table}_${action}`); handle.pragma('user_version = 12'); closeDb(); const upgraded = db(); const revision = () => ( upgraded .prepare("SELECT revision FROM report_revisions WHERE topic='metrics'") .get() as { revision: number } ).revision; const before = revision(); upgraded .prepare( "INSERT INTO settings(key,value) VALUES('autoscaling_paused','1') ON CONFLICT(key) DO UPDATE SET value='1'" ) .run(); expect(revision()).toBeGreaterThan(before); const paused = revision(); upgraded.prepare("INSERT INTO settings(key,value) VALUES('unrelated-setting','1')").run(); expect(revision()).toBe(paused); upgraded.prepare("DELETE FROM settings WHERE key='autoscaling_paused'").run(); expect(revision()).toBeGreaterThan(paused); });