diff --git a/src/resources/extensions/gsd/init-wizard.ts b/src/resources/extensions/gsd/init-wizard.ts index d5f490459..6d1deb660 100644 --- a/src/resources/extensions/gsd/init-wizard.ts +++ b/src/resources/extensions/gsd/init-wizard.ts @@ -235,6 +235,20 @@ export async function showProjectInit( // ── Step 9: Bootstrap .gsd/ ──────────────────────────────────────────────── bootstrapGsdDirectory(basePath, prefs, signals); + // Initialize SQLite database so GSD starts in full-capability mode (#3880). + // Without this, isDbAvailable() returns false and GSD enters degraded + // markdown-only mode until a tool handler happens to call ensureDbOpen(). + let dbReady = false; + try { + const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js"); + dbReady = await ensureDbOpen(basePath); + } catch { + // Swallowed — warning surfaced below + } + if (!dbReady) { + ctx.ui.notify("Warning: database initialization failed — GSD will run in degraded mode until the next /gsd invocation.", "warning"); + } + // Ensure .gitignore ensureGitignore(basePath); untrackRuntimeFiles(basePath); @@ -250,6 +264,25 @@ export async function showProjectInit( // Non-fatal — codebase map generation failure should never block project init } + // Write initial STATE.md so it exists before the first /gsd invocation. + // The explicit /gsd init path (ops.ts) returns without entering showSmartEntry(), + // which would otherwise generate STATE.md at guided-flow.ts:1358. + let stateReady = false; + try { + const { deriveState } = await import("./state.js"); + const { buildStateMarkdown } = await import("./doctor.js"); + const { saveFile } = await import("./files.js"); + const { resolveGsdRootFile } = await import("./paths.js"); + const state = await deriveState(basePath); + await saveFile(resolveGsdRootFile(basePath, "STATE"), buildStateMarkdown(state)); + stateReady = true; + } catch { + // Swallowed — warning surfaced below + } + if (!stateReady) { + ctx.ui.notify("Warning: initial STATE.md generation failed — it will be created on the next /gsd invocation.", "warning"); + } + ctx.ui.notify("GSD initialized. Starting your first milestone...", "info"); return { completed: true, bootstrapped: true }; @@ -433,6 +466,7 @@ function bootstrapGsdDirectory( const gsd = gsdRoot(basePath); mkdirSync(join(gsd, "milestones"), { recursive: true }); + mkdirSync(join(gsd, "runtime"), { recursive: true }); // Write PREFERENCES.md from wizard answers const preferencesContent = buildPreferencesFile(prefs); diff --git a/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts b/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts new file mode 100644 index 000000000..bde28376c --- /dev/null +++ b/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts @@ -0,0 +1,121 @@ +/** + * GSD Init Wizard — Bootstrap completeness regression tests + * + * Regression test for #3880 — fresh install never creates gsd.db. + * + * The init wizard must create all artifacts needed for full-capability + * mode: gsd.db (via ensureDbOpen), runtime/ directory, and STATE.md + * (via deriveState + buildStateMarkdown). Without these, GSD enters + * degraded markdown-only mode on every fresh install. + * + * These are structural tests that verify the init-wizard.ts source + * contains the required calls in the correct order. + */ + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const wizardSrc = readFileSync( + join(__dirname, "..", "init-wizard.ts"), + "utf-8", +); + +describe("init-wizard bootstrap completeness (#3880)", () => { + // ── Gap 1: gsd.db must be created during init ───────────────────────── + + test("bootstrapGsdDirectory is followed by ensureDbOpen", () => { + const bootstrapIdx = wizardSrc.indexOf("bootstrapGsdDirectory(basePath"); + const ensureDbIdx = wizardSrc.indexOf("ensureDbOpen(basePath)"); + assert.ok(bootstrapIdx > -1, "bootstrapGsdDirectory call should exist"); + assert.ok(ensureDbIdx > -1, "ensureDbOpen(basePath) call should exist"); + assert.ok( + ensureDbIdx > bootstrapIdx, + "ensureDbOpen must appear after bootstrapGsdDirectory so .gsd/ exists first", + ); + }); + + test("ensureDbOpen is imported from dynamic-tools", () => { + assert.match( + wizardSrc, + /import.*dynamic-tools/, + "init-wizard should import from dynamic-tools for ensureDbOpen", + ); + }); + + // ── Gap 2: runtime/ directory must be created during init ────────────── + + test("bootstrapGsdDirectory creates runtime/ directory", () => { + // Find the bootstrapGsdDirectory function body + const fnStart = wizardSrc.indexOf("function bootstrapGsdDirectory("); + assert.ok(fnStart > -1, "bootstrapGsdDirectory function should exist"); + + // Find the next function definition to bound the search + const fnBody = wizardSrc.slice(fnStart, wizardSrc.indexOf("\nfunction ", fnStart + 1)); + + assert.match( + fnBody, + /mkdirSync\(.*"runtime"/, + 'bootstrapGsdDirectory should create "runtime" directory', + ); + }); + + // ── Gap 3: STATE.md must be written during init ──────────────────────── + + test("showProjectInit generates STATE.md after bootstrap", () => { + const bootstrapIdx = wizardSrc.indexOf("bootstrapGsdDirectory(basePath"); + const deriveIdx = wizardSrc.indexOf("deriveState(basePath)"); + const stateIdx = wizardSrc.indexOf("buildStateMarkdown(state)"); + const saveIdx = wizardSrc.indexOf('resolveGsdRootFile(basePath, "STATE")'); + + assert.ok(deriveIdx > -1, "deriveState call should exist in init-wizard"); + assert.ok(stateIdx > -1, "buildStateMarkdown call should exist in init-wizard"); + assert.ok(saveIdx > -1, "resolveGsdRootFile STATE call should exist in init-wizard"); + assert.ok( + deriveIdx > bootstrapIdx, + "deriveState must appear after bootstrapGsdDirectory", + ); + }); + + // ── Ordering: DB must be open before deriveState ─────────────────────── + + test("ensureDbOpen appears before deriveState", () => { + const ensureDbIdx = wizardSrc.indexOf("ensureDbOpen(basePath)"); + const deriveIdx = wizardSrc.indexOf("deriveState(basePath)"); + assert.ok(ensureDbIdx > -1, "ensureDbOpen should exist"); + assert.ok(deriveIdx > -1, "deriveState should exist"); + assert.ok( + ensureDbIdx < deriveIdx, + "ensureDbOpen must appear before deriveState so DB is ready for state derivation", + ); + }); + + // ── Failure visibility: user must be warned on partial bootstrap ─────── + + test("ensureDbOpen failure surfaces a warning to the user", () => { + assert.match( + wizardSrc, + /if\s*\(\s*!dbReady\s*\)/, + "init-wizard should check dbReady and warn the user on failure", + ); + // The warning must reference degraded mode so the user knows what happened + assert.match( + wizardSrc, + /degraded mode/, + "DB failure warning should mention degraded mode", + ); + }); + + test("STATE.md failure surfaces a warning to the user", () => { + assert.match( + wizardSrc, + /if\s*\(\s*!stateReady\s*\)/, + "init-wizard should check stateReady and warn the user on failure", + ); + }); +});