Merge pull request #3904 from jeremymcs/fix/init-bootstrap-completeness
fix(gsd): create gsd.db, runtime/, and STATE.md during init
This commit is contained in:
commit
35568d0543
2 changed files with 155 additions and 0 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue