fix: ensureDbOpen creates DB + migrates Markdown in interactive sessions (#1790)

This commit is contained in:
deseltrus 2026-03-21 18:38:43 +01:00 committed by GitHub
parent f628f71843
commit afd3e3bd96
2 changed files with 196 additions and 1 deletions

View file

@ -10,10 +10,37 @@ export async function ensureDbOpen(): Promise<boolean> {
try {
const db = await import("../gsd-db.js");
if (db.isDbAvailable()) return true;
const dbPath = join(process.cwd(), ".gsd", "gsd.db");
const basePath = process.cwd();
const gsdDir = join(basePath, ".gsd");
const dbPath = join(gsdDir, "gsd.db");
// Open existing DB file
if (existsSync(dbPath)) {
return db.openDatabase(dbPath);
}
// No DB file — create + migrate from Markdown if .gsd/ has content
if (existsSync(gsdDir)) {
const hasDecisions = existsSync(join(gsdDir, "DECISIONS.md"));
const hasRequirements = existsSync(join(gsdDir, "REQUIREMENTS.md"));
const hasMilestones = existsSync(join(gsdDir, "milestones"));
if (hasDecisions || hasRequirements || hasMilestones) {
const opened = db.openDatabase(dbPath);
if (opened) {
try {
const { migrateFromMarkdown } = await import("../md-importer.js");
migrateFromMarkdown(basePath);
} catch (err) {
process.stderr.write(
`gsd-db: ensureDbOpen auto-migration failed: ${(err as Error).message}\n`,
);
}
}
return opened;
}
}
return false;
} catch {
return false;

View file

@ -0,0 +1,168 @@
// ensureDbOpen — Tests that the lazy DB opener creates + migrates the database
// when .gsd/ exists with Markdown content but no gsd.db file.
//
// This covers the bug where interactive (non-auto) sessions got
// "GSD database is not available" because ensureDbOpen only opened
// existing DB files but never created them.
import { createTestContext } from './test-helpers.ts';
import * as path from 'node:path';
import * as os from 'node:os';
import * as fs from 'node:fs';
import { closeDatabase, isDbAvailable, getDecisionById } from '../gsd-db.ts';
const { assertEq, assertTrue, report } = createTestContext();
function makeTmpDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ensure-db-'));
return dir;
}
function cleanupDir(dir: string): void {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch { /* swallow */ }
}
// ═══════════════════════════════════════════════════════════════════════════
// ensureDbOpen creates DB + migrates when .gsd/ has Markdown
// ═══════════════════════════════════════════════════════════════════════════
console.log('\n── ensureDbOpen: creates DB from Markdown ──');
{
const tmpDir = makeTmpDir();
const gsdDir = path.join(tmpDir, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
// Write a minimal DECISIONS.md so migration has content
const decisionsContent = `# Decisions
| # | When | Scope | Decision | Choice | Rationale | Revisable |
|---|------|-------|----------|--------|-----------|-----------|
| D001 | M001 | architecture | Use SQLite | SQLite | Sync API | Yes |
`;
fs.writeFileSync(path.join(gsdDir, 'DECISIONS.md'), decisionsContent);
// Verify no DB file exists yet
const dbPath = path.join(gsdDir, 'gsd.db');
assertTrue(!fs.existsSync(dbPath), 'DB file should not exist before ensureDbOpen');
// Close any previously open DB
try { closeDatabase(); } catch { /* ok */ }
// Override process.cwd to point at tmpDir for ensureDbOpen
const origCwd = process.cwd;
process.cwd = () => tmpDir;
try {
// Dynamic import to get the freshest version
const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
const result = await ensureDbOpen();
assertTrue(result === true, 'ensureDbOpen should return true when .gsd/ has Markdown');
assertTrue(fs.existsSync(dbPath), 'DB file should be created after ensureDbOpen');
assertTrue(isDbAvailable(), 'DB should be available after ensureDbOpen');
// Verify that Markdown migration actually ran
const decision = getDecisionById('D001');
assertTrue(decision !== null, 'D001 should be migrated from DECISIONS.md');
if (decision) {
assertEq(decision.scope, 'architecture', 'Migrated decision scope should match');
assertEq(decision.choice, 'SQLite', 'Migrated decision choice should match');
}
} finally {
process.cwd = origCwd;
closeDatabase();
cleanupDir(tmpDir);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// ensureDbOpen returns false when no .gsd/ exists
// ═══════════════════════════════════════════════════════════════════════════
console.log('\n── ensureDbOpen: no .gsd/ returns false ──');
{
const tmpDir = makeTmpDir();
// No .gsd/ directory at all
try { closeDatabase(); } catch { /* ok */ }
const origCwd = process.cwd;
process.cwd = () => tmpDir;
try {
const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
const result = await ensureDbOpen();
assertTrue(result === false, 'ensureDbOpen should return false when no .gsd/ exists');
assertTrue(!isDbAvailable(), 'DB should not be available');
} finally {
process.cwd = origCwd;
cleanupDir(tmpDir);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// ensureDbOpen opens existing DB without re-migration
// ═══════════════════════════════════════════════════════════════════════════
console.log('\n── ensureDbOpen: opens existing DB ──');
{
const tmpDir = makeTmpDir();
const gsdDir = path.join(tmpDir, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
// Create a DB file first
const dbPath = path.join(gsdDir, 'gsd.db');
const { openDatabase } = await import('../gsd-db.ts');
openDatabase(dbPath);
closeDatabase();
assertTrue(fs.existsSync(dbPath), 'DB file should exist from manual create');
const origCwd = process.cwd;
process.cwd = () => tmpDir;
try {
const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
const result = await ensureDbOpen();
assertTrue(result === true, 'ensureDbOpen should open existing DB');
assertTrue(isDbAvailable(), 'DB should be available');
} finally {
process.cwd = origCwd;
closeDatabase();
cleanupDir(tmpDir);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// ensureDbOpen returns false for empty .gsd/ (no Markdown, no DB)
// ═══════════════════════════════════════════════════════════════════════════
console.log('\n── ensureDbOpen: empty .gsd/ returns false ──');
{
const tmpDir = makeTmpDir();
fs.mkdirSync(path.join(tmpDir, '.gsd'), { recursive: true });
// .gsd/ exists but no DECISIONS.md, REQUIREMENTS.md, or milestones/
try { closeDatabase(); } catch { /* ok */ }
const origCwd = process.cwd;
process.cwd = () => tmpDir;
try {
const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
const result = await ensureDbOpen();
assertTrue(result === false, 'ensureDbOpen should return false for empty .gsd/');
} finally {
process.cwd = origCwd;
cleanupDir(tmpDir);
}
}
// ═══════════════════════════════════════════════════════════════════════════
report();