diff --git a/src/resources/extensions/sf/doctor-engine-checks.js b/src/resources/extensions/sf/doctor-engine-checks.js index 2c2302616..44e1070b8 100644 --- a/src/resources/extensions/sf/doctor-engine-checks.js +++ b/src/resources/extensions/sf/doctor-engine-checks.js @@ -1,7 +1,12 @@ import { existsSync, readdirSync, renameSync, rmSync, statSync } from "node:fs"; import { join } from "node:path"; import { milestonesDir, resolveMilestoneFile } from "./paths.js"; -import { _getAdapter, getAllMilestones, isDbAvailable } from "./sf-db.js"; +import { + _getAdapter, + getAllMilestones, + isDbAvailable, + openDatabase, +} from "./sf-db.js"; import { deriveState } from "./state.js"; import { readEvents } from "./workflow-events.js"; import { renderAllProjections } from "./workflow-projections.js"; @@ -28,18 +33,17 @@ function normalizeLegacyDirectory( const sourcePath = join(parentDir, entry); const targetPath = join(parentDir, bareId); const conflict = existsSync(targetPath); + if (conflict) return; issues.push({ - severity: conflict ? "warning" : "info", + severity: "info", code: "legacy_plan_slug_directory", scope: "project", unitId: `${unitPrefix}/${entry}`, - message: conflict - ? `Legacy plan directory ${sourcePath} should be renamed to ${targetPath}, but the target already exists. Merge manually before running doctor fix.` - : `Legacy plan directory ${sourcePath} should be renamed to bare ID directory ${targetPath}.`, + message: `Legacy plan directory ${sourcePath} should be renamed to bare ID directory ${targetPath}.`, file: sourcePath, - fixable: !conflict, + fixable: true, }); - if (conflict || !shouldFix?.("legacy_plan_slug_directory")) return; + if (!shouldFix?.("legacy_plan_slug_directory")) return; try { renameSync(sourcePath, targetPath); fixesApplied.push( @@ -105,6 +109,14 @@ export async function checkEngineHealth( shouldFix, ) { const dbPath = join(basePath, ".sf", "sf.db"); + if (!isDbAvailable() && existsSync(dbPath)) { + try { + openDatabase(dbPath); + } catch { + // Report below. Doctor must not throw just because the engine DB is + // locked, corrupt, or unavailable in this process. + } + } if (!isDbAvailable() && existsSync(dbPath)) { issues.push({ severity: "warning", diff --git a/src/resources/extensions/sf/doctor-environment.js b/src/resources/extensions/sf/doctor-environment.js index 08d16fb34..3457df093 100644 --- a/src/resources/extensions/sf/doctor-environment.js +++ b/src/resources/extensions/sf/doctor-environment.js @@ -543,7 +543,7 @@ function checkGitRemote(basePath) { const remote = tryExec("git remote get-url origin", basePath); if (!remote) return null; // Quick connectivity check with short timeout - const result = tryExec("git ls-remote --exit-code -h origin HEAD", basePath); + const result = tryExec("git ls-remote --exit-code origin HEAD", basePath); if (result === null) { return { name: "git_remote", diff --git a/src/resources/extensions/sf/doctor-runtime-checks.js b/src/resources/extensions/sf/doctor-runtime-checks.js index b1bc86b4e..131607bdd 100644 --- a/src/resources/extensions/sf/doctor-runtime-checks.js +++ b/src/resources/extensions/sf/doctor-runtime-checks.js @@ -704,8 +704,6 @@ function formatBucketCountParts(counts) { parts.push(`${counts.upgradable} pending upgrade`); if (counts["editing-drift"] && counts["editing-drift"] > 0) parts.push(`${counts["editing-drift"]} editing-drift`); - if (counts.untracked && counts.untracked > 0) - parts.push(`${counts.untracked} untracked`); const pendingCount = (counts.missing ?? 0) + (counts.upgradable ?? 0); return { parts, pendingCount }; } @@ -727,10 +725,7 @@ export function checkScaffoldFreshness(basePath) { } const counts = report.countsByBucket; const actionable = - counts.missing + - counts.upgradable + - counts["editing-drift"] + - counts.untracked; + counts.missing + counts.upgradable + counts["editing-drift"]; if (actionable === 0) return null; const { parts, pendingCount } = formatBucketCountParts(counts); const summary = parts.join(", "); diff --git a/src/resources/extensions/sf/tests/agentic-docs-scaffold.test.mjs b/src/resources/extensions/sf/tests/agentic-docs-scaffold.test.mjs index a1c988f5f..8e42149ee 100644 --- a/src/resources/extensions/sf/tests/agentic-docs-scaffold.test.mjs +++ b/src/resources/extensions/sf/tests/agentic-docs-scaffold.test.mjs @@ -9,7 +9,11 @@ import { import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, test } from "vitest"; -import { ensureAgenticDocsScaffold } from "../agentic-docs-scaffold.js"; +import { + ensureAgenticDocsScaffold, + SCAFFOLD_FILES, +} from "../agentic-docs-scaffold.js"; +import { checkScaffoldFreshness } from "../doctor-runtime-checks.js"; import { detectScaffoldDrift } from "../scaffold-drift.js"; import { extractMarker, stampScaffoldFile } from "../scaffold-versioning.js"; @@ -131,3 +135,14 @@ All lines should start with \`OK:\` for the bootstrap spec to pass. assert.equal(existsSync(join(root, "harness")), false); assert.equal(existsSync(join(root, ".sf/harness/specs/bootstrap.md")), true); }); + +test("checkScaffoldFreshness_when_only_markerless_scaffold_exists_does_not_warn", () => { + const root = makeProject(); + for (const file of SCAFFOLD_FILES) { + const target = join(root, file.path); + mkdirSync(dirname(target), { recursive: true }); + writeFileSync(target, file.content, "utf-8"); + } + + assert.equal(checkScaffoldFreshness(root), null); +}); diff --git a/src/resources/extensions/sf/tests/doctor-plan-dir-normalization.test.mjs b/src/resources/extensions/sf/tests/doctor-plan-dir-normalization.test.mjs index 5ef8e18c7..0f2d70612 100644 --- a/src/resources/extensions/sf/tests/doctor-plan-dir-normalization.test.mjs +++ b/src/resources/extensions/sf/tests/doctor-plan-dir-normalization.test.mjs @@ -9,7 +9,11 @@ import { import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, test } from "vitest"; -import { normalizeLegacyPlanSlugDirectories } from "../doctor-engine-checks.js"; +import { + checkEngineHealth, + normalizeLegacyPlanSlugDirectories, +} from "../doctor-engine-checks.js"; +import { closeDatabase, openDatabase } from "../sf-db.js"; const tmpDirs = []; @@ -21,6 +25,7 @@ function makeProject() { } afterEach(() => { + closeDatabase(); while (tmpDirs.length > 0) { const dir = tmpDirs.pop(); if (dir) rmSync(dir, { recursive: true, force: true }); @@ -96,15 +101,31 @@ describe("doctor plan directory normalization", () => { (code) => code === "legacy_plan_slug_directory", ); - const issue = issues.find( - (candidate) => candidate.code === "legacy_plan_slug_directory", + assert.equal( + issues.some( + (candidate) => candidate.code === "legacy_plan_slug_directory", + ), + false, ); - assert.equal(issue?.fixable, false); - assert.match(issue?.message ?? "", /target already exists/); assert.equal( existsSync(join(project, ".sf", "milestones", "M001-long-name")), true, ); assert.deepEqual(fixesApplied, []); }); + + test("checkEngineHealth_when_db_file_can_open_does_not_report_unavailable", async () => { + const project = makeProject(); + const dbPath = join(project, ".sf", "sf.db"); + assert.equal(openDatabase(dbPath), true); + closeDatabase(); + const issues = []; + + await checkEngineHealth(project, issues, [], () => false); + + assert.equal( + issues.some((issue) => issue.code === "db_unavailable"), + false, + ); + }); });