fix: bind escalate command to project db

This commit is contained in:
Mikael Hugo 2026-05-07 06:37:21 +02:00
parent 87362f27fc
commit 2aed04608c
2 changed files with 104 additions and 0 deletions

View file

@ -7,6 +7,7 @@
// — apply user choice, clear flag, allow loop to continue
//
// All operations run against the active project's DB (process.cwd()-rooted).
import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
import { readEscalationArtifact, resolveEscalation } from "./escalation.js";
import {
getActiveMilestoneFromDb,
@ -32,6 +33,7 @@ function parseSliceTask(spec) {
return { sliceId: m[1], taskId: m[2] };
}
export async function handleEscalate(args, ctx) {
await ensureDbOpen(process.cwd());
if (!isDbAvailable()) {
ctx.ui.notify("SF database is not available. Run /sf doctor.", "error");
return;

View file

@ -0,0 +1,102 @@
/**
* commands-escalate-db.test.mjs `/sf escalate` database binding coverage.
*
* Purpose: prove escalation commands read the current project SQLite DB instead
* of reusing a previously-open DB from another project.
*/
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import { handleEscalate } from "../commands-escalate.js";
import {
closeDatabase,
insertMilestone,
insertSlice,
insertTask,
openDatabase,
setTaskEscalationPending,
} from "../sf-db.js";
const tmpRoots = [];
const originalCwd = process.cwd();
afterEach(() => {
process.chdir(originalCwd);
closeDatabase();
for (const root of tmpRoots.splice(0)) {
rmSync(root, { recursive: true, force: true });
}
});
function makeProject() {
const root = mkdtempSync(join(tmpdir(), "sf-escalate-db-"));
mkdirSync(join(root, ".sf", "runtime"), { recursive: true });
tmpRoots.push(root);
return root;
}
function makeCtx() {
const notifications = [];
return {
ctx: {
ui: {
notify(message, level = "info") {
notifications.push({ message, level });
},
},
},
notifications,
};
}
function seedEscalation(project) {
openDatabase(join(project, ".sf", "sf.db"));
insertMilestone({ id: "M001", title: "First milestone", status: "active" });
insertSlice({
milestoneId: "M001",
id: "S01",
title: "First slice",
status: "pending",
});
insertTask({
milestoneId: "M001",
sliceId: "S01",
id: "T01",
title: "Needs decision",
status: "pending",
});
const artifactPath = join(
project,
".sf",
"runtime",
"M001-S01-T01-ESCALATION.json",
);
writeFileSync(
artifactPath,
JSON.stringify({
question: "Use path A or B?",
options: [{ id: "a", label: "Path A" }],
recommendation: "a",
recommendationRationale: "lowest risk",
}),
"utf-8",
);
setTaskEscalationPending("M001", "S01", "T01", artifactPath);
}
test("handleEscalate_when_process_switches_projects_opens_current_project_db", async () => {
const first = makeProject();
const second = makeProject();
seedEscalation(first);
process.chdir(second);
const { ctx, notifications } = makeCtx();
await handleEscalate("list", ctx);
assert.equal(notifications.length, 1);
assert.equal(notifications[0].level, "info");
assert.match(notifications[0].message, /No active milestone/);
assert.doesNotMatch(notifications[0].message, /Use path A or B/);
});