From d49a16db6e7a290c8cdf159c41796c955df10426 Mon Sep 17 00:00:00 2001 From: jeffusion Date: Thu, 26 Mar 2026 22:05:28 +0800 Subject: [PATCH] test(db): add self-healing tests for missing repository prompt table - Test runtime self-healing when repository_review_prompts table is dropped - Test migration layer self-healing for inconsistent DB state - Verify repository listing remains functional during schema recovery Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) --- ...ion-repository-review-prompts-heal.test.ts | 75 +++++++++++++++++++ .../repository-review-prompt-repo.test.ts | 28 ++++++- 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/db/__tests__/migration-repository-review-prompts-heal.test.ts diff --git a/src/db/__tests__/migration-repository-review-prompts-heal.test.ts b/src/db/__tests__/migration-repository-review-prompts-heal.test.ts new file mode 100644 index 0000000..cac23d1 --- /dev/null +++ b/src/db/__tests__/migration-repository-review-prompts-heal.test.ts @@ -0,0 +1,75 @@ +import { Database } from 'bun:sqlite'; +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; +import { randomUUID } from 'node:crypto'; +import { existsSync, mkdirSync, unlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { closeDatabase, getDatabase, initDatabase } from '../database'; + +function createInconsistentMigrationState(dbPath: string): void { + const db = new Database(dbPath); + db.exec('PRAGMA foreign_keys = ON'); + db.exec(` + CREATE TABLE IF NOT EXISTS _migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + `); + + db.query('INSERT INTO _migrations (version, name) VALUES (?, ?)').run( + 1, + 'init_llm_provider_schema' + ); + db.query('INSERT INTO _migrations (version, name) VALUES (?, ?)').run( + 2, + 'remove_legacy_review_mode' + ); + db.query('INSERT INTO _migrations (version, name) VALUES (?, ?)').run( + 3, + 'add_repository_review_prompts' + ); + + db.close(); +} + +describe('migration self-heal for repository review prompts', () => { + let dbPath: string; + const savedDbPath = process.env.DATABASE_PATH; + + beforeEach(() => { + const tmpDir = join(tmpdir(), `db-migration-heal-${randomUUID()}`); + mkdirSync(tmpDir, { recursive: true }); + dbPath = join(tmpDir, 'test.db'); + process.env.DATABASE_PATH = dbPath; + createInconsistentMigrationState(dbPath); + }); + + afterEach(() => { + closeDatabase(); + if (savedDbPath === undefined) { + Reflect.deleteProperty(process.env, 'DATABASE_PATH'); + } else { + process.env.DATABASE_PATH = savedDbPath; + } + + if (existsSync(dbPath)) unlinkSync(dbPath); + if (existsSync(`${dbPath}-wal`)) unlinkSync(`${dbPath}-wal`); + if (existsSync(`${dbPath}-shm`)) unlinkSync(`${dbPath}-shm`); + }); + + test('rebuilds missing repository_review_prompts table even when migration 3 is marked applied', () => { + initDatabase(); + const db = getDatabase(); + + const tableRow = db + .query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") + .get('repository_review_prompts') as { name: string } | null; + expect(tableRow?.name).toBe('repository_review_prompts'); + + const migrationCountRow = db + .query('SELECT COUNT(*) AS count FROM _migrations WHERE version = ?') + .get(3) as { count: number } | null; + expect(migrationCountRow?.count).toBe(1); + }); +}); diff --git a/src/db/__tests__/repository-review-prompt-repo.test.ts b/src/db/__tests__/repository-review-prompt-repo.test.ts index cc82e38..600134a 100644 --- a/src/db/__tests__/repository-review-prompt-repo.test.ts +++ b/src/db/__tests__/repository-review-prompt-repo.test.ts @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'; import { existsSync, mkdirSync, unlinkSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { closeDatabase, initDatabase } from '../database'; +import { closeDatabase, getDatabase, initDatabase } from '../database'; import { repositoryReviewPromptRepo } from '../repositories/repository-review-prompt-repo'; describe('repository-review-prompt-repo', () => { @@ -64,4 +64,30 @@ describe('repository-review-prompt-repo', () => { 'acme/b': 'prompt-b', }); }); + + test('self-heals missing prompt table and keeps repository listing readable', () => { + const db = getDatabase(); + db.exec('DROP TABLE repository_review_prompts'); + + const map = repositoryReviewPromptRepo.listProjectPrompts(['acme/a']); + expect(map).toEqual({}); + + repositoryReviewPromptRepo.setProjectPrompt('acme', 'a', 'prompt-a'); + expect(repositoryReviewPromptRepo.getProjectPrompt('acme', 'a')).toBe('prompt-a'); + }); + + test('self-heals missing prompt table for direct prompt write path', () => { + const db = getDatabase(); + db.exec('DROP TABLE repository_review_prompts'); + + const row = repositoryReviewPromptRepo.setProjectPrompt( + 'acme', + 'direct-write', + 'prompt-direct' + ); + expect(row.project_prompt).toBe('prompt-direct'); + expect(repositoryReviewPromptRepo.getProjectPrompt('acme', 'direct-write')).toBe( + 'prompt-direct' + ); + }); });