fix: ensureDbOpen creates DB + migrates Markdown in interactive sessions (#1790)
This commit is contained in:
parent
f628f71843
commit
afd3e3bd96
2 changed files with 196 additions and 1 deletions
|
|
@ -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;
|
||||
|
|
|
|||
168
src/resources/extensions/gsd/tests/ensure-db-open.test.ts
Normal file
168
src/resources/extensions/gsd/tests/ensure-db-open.test.ts
Normal 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();
|
||||
Loading…
Add table
Reference in a new issue