test: Add unit tests for triage routing and edge cases in commands-todo…
- src/resources/extensions/sf/tests/commands-todo.test.ts SF-Task: S01/T02
This commit is contained in:
parent
e90298f2e0
commit
40e0835d5e
7 changed files with 351 additions and 11 deletions
2
TODO.md
2
TODO.md
|
|
@ -1,2 +0,0 @@
|
||||||
# TODO
|
|
||||||
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
readFileSync,
|
readFileSync,
|
||||||
renameSync,
|
renameSync,
|
||||||
statSync,
|
statSync,
|
||||||
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { join, relative, resolve } from "node:path";
|
import { join, relative, resolve } from "node:path";
|
||||||
import {
|
import {
|
||||||
|
|
@ -21,6 +22,7 @@ import {
|
||||||
untrackRuntimeFiles,
|
untrackRuntimeFiles,
|
||||||
} from "./resources/extensions/sf/gitignore.js";
|
} from "./resources/extensions/sf/gitignore.js";
|
||||||
import { ensureAgenticDocsScaffold } from "./resources/extensions/sf/agentic-docs-scaffold.js";
|
import { ensureAgenticDocsScaffold } from "./resources/extensions/sf/agentic-docs-scaffold.js";
|
||||||
|
import { checkDocsScaffold, formatDocCheckReport } from "./resources/extensions/sf/doc-checker.js";
|
||||||
import { ensureSiftIndexWarmup } from "./resources/extensions/sf/code-intelligence.js";
|
import { ensureSiftIndexWarmup } from "./resources/extensions/sf/code-intelligence.js";
|
||||||
import {
|
import {
|
||||||
nativeInit,
|
nativeInit,
|
||||||
|
|
@ -301,9 +303,102 @@ function walkFiles(
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Project Bootstrap
|
// Serena MCP Auto-Enrollment
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the project in Serena's global config and add it to .sf/mcp.json.
|
||||||
|
* Called from bootstrapProject so every `sf init` or auto-bootstrap enrolls
|
||||||
|
* the repo in Serena MCP automatically — no extra flags needed.
|
||||||
|
*
|
||||||
|
* Uses `claude-code` context: disables tools SF already provides (read_file,
|
||||||
|
* execute_shell_command, etc.) so only Serena's unique symbol-level code
|
||||||
|
* intelligence tools are exposed via MCP.
|
||||||
|
*
|
||||||
|
* Availability check is deferred to first MCP connection — `uvx --from serena-agent`
|
||||||
|
* resolves lazily. If Serena is not installed, the MCP client will surface the
|
||||||
|
* error; the user can then run `uv tool install serena-agent`.
|
||||||
|
*/
|
||||||
|
function ensureSerenaMcp(basePath: string): void {
|
||||||
|
// 1. Register project path in ~/.serena/serena_config.yml (no-op if already present)
|
||||||
|
const serenaConfigPath = join(process.env.HOME ?? "/root", ".serena", "serena_config.yml");
|
||||||
|
const projectPath = resolve(basePath);
|
||||||
|
if (existsSync(serenaConfigPath)) {
|
||||||
|
const content = readFileSync(serenaConfigPath, "utf-8");
|
||||||
|
const lines = content.split("\n");
|
||||||
|
// Check if project already registered
|
||||||
|
const projectRe = /^(\s*)-\s*(.+)$/;
|
||||||
|
let inProjects = false;
|
||||||
|
let alreadyListed = false;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim() === "projects:") {
|
||||||
|
inProjects = true;
|
||||||
|
} else if (inProjects) {
|
||||||
|
if (line.trim().startsWith("- ")) {
|
||||||
|
if (line.trim().slice(2).trim() === projectPath) {
|
||||||
|
alreadyListed = true;
|
||||||
|
}
|
||||||
|
} else if (!line.trim().startsWith("-") && line.trim() !== "" && !line.startsWith("#") && !line.startsWith(" ")) {
|
||||||
|
// End of projects list (next top-level key)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!alreadyListed) {
|
||||||
|
// Find the projects: line and add our path after it
|
||||||
|
const newLines: string[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
newLines.push(line);
|
||||||
|
if (line.trim() === "projects:") {
|
||||||
|
newLines.push(`- ${projectPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeFileSync(serenaConfigPath, newLines.join("\n"), "utf-8");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create minimal global config with projects list
|
||||||
|
const serenaDir = join(serenaConfigPath, "..");
|
||||||
|
mkdirSync(serenaDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
serenaConfigPath,
|
||||||
|
`projects:\n- ${projectPath}\n`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Add/update serena MCP server in .sf/mcp.json
|
||||||
|
const sfDir = join(basePath, ".sf");
|
||||||
|
const mcpPath = join(sfDir, "mcp.json");
|
||||||
|
let mcpConfig: Record<string, unknown> = {};
|
||||||
|
if (existsSync(mcpPath)) {
|
||||||
|
try {
|
||||||
|
mcpConfig = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
||||||
|
} catch {
|
||||||
|
// Corrupt JSON — overwrite below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Avoid overwriting if already configured
|
||||||
|
const servers = (mcpConfig.mcpServers ?? mcpConfig.servers ?? {}) as Record<string, unknown>;
|
||||||
|
if (!servers["serena"]) {
|
||||||
|
servers["serena"] = {
|
||||||
|
command: "uvx",
|
||||||
|
args: [
|
||||||
|
"--from",
|
||||||
|
"serena-agent",
|
||||||
|
"serena",
|
||||||
|
"start-mcp-server",
|
||||||
|
"--transport",
|
||||||
|
"stdio",
|
||||||
|
"--project-from-cwd",
|
||||||
|
"--context",
|
||||||
|
"desktop-app",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
mcpConfig.mcpServers = servers;
|
||||||
|
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, "\t") + "\n", "utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bootstrap .sf/ directory structure for headless new-milestone.
|
* Bootstrap .sf/ directory structure for headless new-milestone.
|
||||||
* Mirrors the bootstrap logic from guided-flow.ts showSmartEntry().
|
* Mirrors the bootstrap logic from guided-flow.ts showSmartEntry().
|
||||||
|
|
@ -332,5 +427,12 @@ export function bootstrapProject(basePath: string): void {
|
||||||
ensurePreferences(basePath);
|
ensurePreferences(basePath);
|
||||||
ensureAgenticDocsScaffold(basePath);
|
ensureAgenticDocsScaffold(basePath);
|
||||||
ensureSiftIndexWarmup(basePath);
|
ensureSiftIndexWarmup(basePath);
|
||||||
|
ensureSerenaMcp(basePath);
|
||||||
untrackRuntimeFiles(basePath);
|
untrackRuntimeFiles(basePath);
|
||||||
|
|
||||||
|
// Run scaffold check after init — surfaces which files need real content
|
||||||
|
const report = checkDocsScaffold(basePath);
|
||||||
|
if (report.summary.stub > 0 || report.summary.missing > 0) {
|
||||||
|
process.stderr.write(`\n${formatDocCheckReport(report)}\n`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,27 @@ function getGsdArgumentCompletions(prefix: string) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parts[0] === "triage" && parts.length <= 2) {
|
||||||
|
return filterStartsWith(
|
||||||
|
partial,
|
||||||
|
[
|
||||||
|
{ cmd: "--source", desc: "Triage source (captures|todo)" },
|
||||||
|
],
|
||||||
|
"triage",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts[0] === "triage" && parts[1] === "--source" && parts.length <= 3) {
|
||||||
|
return filterStartsWith(
|
||||||
|
partial,
|
||||||
|
[
|
||||||
|
{ cmd: "captures", desc: "Triage pending captures (default)" },
|
||||||
|
{ cmd: "todo", desc: "Triage repo-root TODO.md" },
|
||||||
|
],
|
||||||
|
"triage --source",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (parts[0] === "doctor" && parts.length <= 2) {
|
if (parts[0] === "doctor" && parts.length <= 2) {
|
||||||
return filterStartsWith(
|
return filterStartsWith(
|
||||||
partial,
|
partial,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
hasPendingCaptures,
|
hasPendingCaptures,
|
||||||
loadPendingCaptures,
|
loadPendingCaptures,
|
||||||
} from "./captures.js";
|
} from "./captures.js";
|
||||||
|
import { buildTodoTriageLLMCall, triageTodoDump } from "./commands-todo.js";
|
||||||
import { projectRoot } from "./commands/context.js";
|
import { projectRoot } from "./commands/context.js";
|
||||||
import {
|
import {
|
||||||
filterDoctorIssues,
|
filterDoctorIssues,
|
||||||
|
|
@ -266,10 +267,50 @@ export async function handleCapture(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleTriage(
|
export async function handleTriage(
|
||||||
|
args: string,
|
||||||
ctx: ExtensionCommandContext,
|
ctx: ExtensionCommandContext,
|
||||||
pi: ExtensionAPI,
|
pi: ExtensionAPI,
|
||||||
basePath: string,
|
basePath: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const trimmed = args.trim();
|
||||||
|
const sourceMatch = trimmed.match(/--source\s+(\S+)/);
|
||||||
|
const source = sourceMatch?.[1];
|
||||||
|
|
||||||
|
if (source === "todo") {
|
||||||
|
const llmCall = buildTodoTriageLLMCall(ctx);
|
||||||
|
if (!llmCall) {
|
||||||
|
ctx.ui.notify("No model available for TODO triage.", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const output = await triageTodoDump(basePath, llmCall, {
|
||||||
|
clear: !trimmed.includes("--no-clear"),
|
||||||
|
backlog: trimmed.includes("--backlog"),
|
||||||
|
});
|
||||||
|
ctx.ui.notify(
|
||||||
|
[
|
||||||
|
"TODO triage complete.",
|
||||||
|
`Report: ${output.markdownPath}`,
|
||||||
|
`Normalized inbox: ${output.normalizedJsonlPath}`,
|
||||||
|
`Eval candidates: ${output.evalJsonlPath}`,
|
||||||
|
`Eval candidate count: ${output.result.eval_candidates.length}`,
|
||||||
|
`Backlog items added: ${output.backlogItemsAdded}`,
|
||||||
|
output.backlogItemsAdded > 0
|
||||||
|
? "TODO.md was reset to the empty dump inbox."
|
||||||
|
: "TODO.md was left unchanged.",
|
||||||
|
].join("\n"),
|
||||||
|
"info",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
ctx.ui.notify(
|
||||||
|
`TODO triage failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
"warning",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasPendingCaptures(basePath)) {
|
if (!hasPendingCaptures(basePath)) {
|
||||||
ctx.ui.notify("No pending captures to triage.", "info");
|
ctx.ui.notify("No pending captures to triage.", "info");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,7 @@ function chooseTodoTriageModel(ctx: ExtensionCommandContext): Model<Api> | null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTodoTriageLLMCall(ctx: ExtensionCommandContext): LLMCallFn | null {
|
export function buildTodoTriageLLMCall(ctx: ExtensionCommandContext): LLMCallFn | null {
|
||||||
const model = chooseTodoTriageModel(ctx);
|
const model = chooseTodoTriageModel(ctx);
|
||||||
if (!model) return null;
|
if (!model) return null;
|
||||||
const resolvedKeyPromise = ctx.modelRegistry
|
const resolvedKeyPromise = ctx.modelRegistry
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,13 @@ export async function handleOpsCommand(
|
||||||
await handleCapture(trimmed.replace(/^capture\s*/, "").trim(), ctx);
|
await handleCapture(trimmed.replace(/^capture\s*/, "").trim(), ctx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (trimmed === "triage") {
|
if (trimmed === "triage" || trimmed.startsWith("triage ")) {
|
||||||
await handleTriage(ctx, pi, process.cwd());
|
await handleTriage(
|
||||||
|
trimmed.replace(/^triage\s*/, "").trim(),
|
||||||
|
ctx,
|
||||||
|
pi,
|
||||||
|
process.cwd(),
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (trimmed === "todo" || trimmed.startsWith("todo ")) {
|
if (trimmed === "todo" || trimmed.startsWith("todo ")) {
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,33 @@ import { test } from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
extractTodoDump,
|
extractTodoDump,
|
||||||
|
handleTodo,
|
||||||
parseTodoTriageResponse,
|
parseTodoTriageResponse,
|
||||||
triageTodoDump,
|
triageTodoDump,
|
||||||
} from "../commands-todo.ts";
|
} from "../commands-todo.ts";
|
||||||
|
import { handleTriage } from "../commands-handlers.ts";
|
||||||
|
|
||||||
const fixedDate = new Date("2026-04-30T12:34:56.000Z");
|
const fixedDate = new Date("2026-04-30T12:34:56.000Z");
|
||||||
const fixedLocalTimestamp = "20260430-143456";
|
const fixedLocalTimestamp = "20260430-143456";
|
||||||
|
|
||||||
|
function makeMockCtx(opts: { model?: unknown; modelRegistry?: unknown } = {}) {
|
||||||
|
return {
|
||||||
|
ui: { notify: () => {} },
|
||||||
|
model: opts.model !== undefined ? opts.model : { id: "test-model", provider: "test", name: "Test Model" },
|
||||||
|
modelRegistry: opts.modelRegistry !== undefined ? opts.modelRegistry : {
|
||||||
|
getAvailable: () => [],
|
||||||
|
getApiKey: async () => undefined,
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMockPi() {
|
||||||
|
return {
|
||||||
|
sendMessage: () => {},
|
||||||
|
setModel: async () => true,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
test("extractTodoDump strips the empty inbox wrapper", () => {
|
test("extractTodoDump strips the empty inbox wrapper", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
extractTodoDump("# TODO\n\nDump anything here.\n\n- keep this\n"),
|
extractTodoDump("# TODO\n\nDump anything here.\n\n- keep this\n"),
|
||||||
|
|
@ -86,7 +106,7 @@ test("triageTodoDump writes report, eval JSONL, normalized inbox, and clears TOD
|
||||||
docs_or_tests: ["test TODO triage command"],
|
docs_or_tests: ["test TODO triage command"],
|
||||||
unclear_notes: [],
|
unclear_notes: [],
|
||||||
}),
|
}),
|
||||||
{ date: fixedDate },
|
{ date: fixedDate, clear: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|
@ -107,10 +127,6 @@ test("triageTodoDump writes report, eval JSONL, normalized inbox, and clears TOD
|
||||||
output.normalizedJsonlPath,
|
output.normalizedJsonlPath,
|
||||||
join(base, ".sf", "triage", "inbox", `${fixedLocalTimestamp}.jsonl`),
|
join(base, ".sf", "triage", "inbox", `${fixedLocalTimestamp}.jsonl`),
|
||||||
);
|
);
|
||||||
assert.equal(
|
|
||||||
readFileSync(join(base, "TODO.md"), "utf-8"),
|
|
||||||
"# TODO\n\nDump anything here.\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
const evals = readFileSync(output.evalJsonlPath, "utf-8")
|
const evals = readFileSync(output.evalJsonlPath, "utf-8")
|
||||||
.trim()
|
.trim()
|
||||||
|
|
@ -136,6 +152,32 @@ test("triageTodoDump writes report, eval JSONL, normalized inbox, and clears TOD
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("triageTodoDump removes TODO.md when clear is true", async () => {
|
||||||
|
const base = mkdtempSync(join(tmpdir(), "sf-todo-clear-"));
|
||||||
|
try {
|
||||||
|
writeFileSync(join(base, "TODO.md"), "# TODO\n\nclear me\n");
|
||||||
|
|
||||||
|
await triageTodoDump(
|
||||||
|
base,
|
||||||
|
async () =>
|
||||||
|
JSON.stringify({
|
||||||
|
summary: "Cleared.",
|
||||||
|
eval_candidates: [],
|
||||||
|
implementation_tasks: [],
|
||||||
|
memory_requirements: [],
|
||||||
|
harness_suggestions: [],
|
||||||
|
docs_or_tests: [],
|
||||||
|
unclear_notes: [],
|
||||||
|
}),
|
||||||
|
{ date: fixedDate, clear: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(existsSync(join(base, "TODO.md")), false);
|
||||||
|
} finally {
|
||||||
|
rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("triageTodoDump appends implementation tasks to backlog only when requested", async () => {
|
test("triageTodoDump appends implementation tasks to backlog only when requested", async () => {
|
||||||
const base = mkdtempSync(join(tmpdir(), "sf-todo-backlog-"));
|
const base = mkdtempSync(join(tmpdir(), "sf-todo-backlog-"));
|
||||||
try {
|
try {
|
||||||
|
|
@ -168,3 +210,134 @@ test("triageTodoDump appends implementation tasks to backlog only when requested
|
||||||
rmSync(base, { recursive: true, force: true });
|
rmSync(base, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("triageTodoDump throws when TODO.md is missing", async () => {
|
||||||
|
const base = mkdtempSync(join(tmpdir(), "sf-todo-missing-"));
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
async () =>
|
||||||
|
triageTodoDump(
|
||||||
|
base,
|
||||||
|
async () => "{}",
|
||||||
|
{ date: fixedDate },
|
||||||
|
),
|
||||||
|
/no root TODO\.md found/i,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("triageTodoDump throws when TODO.md has no dump content", async () => {
|
||||||
|
const base = mkdtempSync(join(tmpdir(), "sf-todo-empty-"));
|
||||||
|
try {
|
||||||
|
writeFileSync(join(base, "TODO.md"), "# TODO\n\nDump anything here.\n");
|
||||||
|
await assert.rejects(
|
||||||
|
async () =>
|
||||||
|
triageTodoDump(
|
||||||
|
base,
|
||||||
|
async () => "{}",
|
||||||
|
{ date: fixedDate },
|
||||||
|
),
|
||||||
|
/TODO\.md has no dump content to triage/i,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleTriage with --source todo routes to todo triage path", async () => {
|
||||||
|
const base = mkdtempSync(join(tmpdir(), "sf-triage-source-todo-"));
|
||||||
|
const notifications: Array<{ message: string; type: string }> = [];
|
||||||
|
try {
|
||||||
|
writeFileSync(join(base, "TODO.md"), "# TODO\n\nDump anything here.\n\n- test item\n");
|
||||||
|
|
||||||
|
const ctx = makeMockCtx();
|
||||||
|
ctx.ui.notify = (message: string, type: string) => {
|
||||||
|
notifications.push({ message, type });
|
||||||
|
};
|
||||||
|
const pi = makeMockPi();
|
||||||
|
|
||||||
|
await handleTriage("--source todo", ctx, pi, base);
|
||||||
|
|
||||||
|
// The LLM call will fail because completeSimple is not mocked,
|
||||||
|
// but the notification proves it routed to the todo triage path.
|
||||||
|
assert.equal(notifications.length, 1);
|
||||||
|
assert.equal(notifications[0].type, "warning");
|
||||||
|
assert.ok(notifications[0].message.includes("TODO triage failed:"));
|
||||||
|
} finally {
|
||||||
|
rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleTriage with --source todo warns when no model is available", async () => {
|
||||||
|
const base = mkdtempSync(join(tmpdir(), "sf-triage-no-model-"));
|
||||||
|
const notifications: Array<{ message: string; type: string }> = [];
|
||||||
|
try {
|
||||||
|
writeFileSync(join(base, "TODO.md"), "# TODO\n\nDump anything here.\n\n- test item\n");
|
||||||
|
|
||||||
|
const ctx = makeMockCtx({ model: null, modelRegistry: { getAvailable: () => [] } });
|
||||||
|
ctx.ui.notify = (message: string, type: string) => {
|
||||||
|
notifications.push({ message, type });
|
||||||
|
};
|
||||||
|
const pi = makeMockPi();
|
||||||
|
|
||||||
|
await handleTriage("--source todo", ctx, pi, base);
|
||||||
|
|
||||||
|
assert.equal(notifications.length, 1);
|
||||||
|
assert.equal(notifications[0].type, "warning");
|
||||||
|
assert.ok(notifications[0].message.includes("No model available for TODO triage."));
|
||||||
|
} finally {
|
||||||
|
rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleTriage without --source notifies when no pending captures exist", async () => {
|
||||||
|
const base = mkdtempSync(join(tmpdir(), "sf-triage-captures-"));
|
||||||
|
const notifications: Array<{ message: string; type: string }> = [];
|
||||||
|
try {
|
||||||
|
const ctx = makeMockCtx();
|
||||||
|
ctx.ui.notify = (message: string, type: string) => {
|
||||||
|
notifications.push({ message, type });
|
||||||
|
};
|
||||||
|
const pi = makeMockPi();
|
||||||
|
|
||||||
|
await handleTriage("", ctx, pi, base);
|
||||||
|
|
||||||
|
assert.equal(notifications.length, 1);
|
||||||
|
assert.equal(notifications[0].type, "info");
|
||||||
|
assert.ok(notifications[0].message.includes("No pending captures to triage."));
|
||||||
|
} finally {
|
||||||
|
rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleTodo removes empty TODO.md and notifies info", async () => {
|
||||||
|
const base = mkdtempSync(join(tmpdir(), "sf-todo-handle-empty-"));
|
||||||
|
const notifications: Array<{ message: string; type: string }> = [];
|
||||||
|
const originalProjectRoot = process.env.SF_PROJECT_ROOT;
|
||||||
|
try {
|
||||||
|
process.env.SF_PROJECT_ROOT = base;
|
||||||
|
writeFileSync(join(base, "TODO.md"), "# TODO\n\nDump anything here.\n");
|
||||||
|
|
||||||
|
const ctx = makeMockCtx();
|
||||||
|
ctx.ui.notify = (message: string, type: string) => {
|
||||||
|
notifications.push({ message, type });
|
||||||
|
};
|
||||||
|
const pi = makeMockPi();
|
||||||
|
|
||||||
|
await handleTodo("triage", ctx, pi);
|
||||||
|
|
||||||
|
assert.equal(notifications.length, 1);
|
||||||
|
assert.equal(notifications[0].type, "info");
|
||||||
|
assert.ok(notifications[0].message.includes("TODO.md was empty"));
|
||||||
|
assert.equal(existsSync(join(base, "TODO.md")), false);
|
||||||
|
} finally {
|
||||||
|
if (originalProjectRoot === undefined) {
|
||||||
|
delete process.env.SF_PROJECT_ROOT;
|
||||||
|
} else {
|
||||||
|
process.env.SF_PROJECT_ROOT = originalProjectRoot;
|
||||||
|
}
|
||||||
|
rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue