test: commit current vitest fixes

This commit is contained in:
Mikael Hugo 2026-05-02 05:39:38 +02:00
parent 0e769dbf13
commit b6358c1c14
5 changed files with 552 additions and 569 deletions

View file

@ -44,6 +44,7 @@ function normalizeVisionMeetingRoute(
if (!firstLine) return undefined;
const cleaned = firstLine.toLowerCase().replace(/[`*_~]/g, " ");
const match = cleaned.match(/\b(planning|researching|discussing)\b/);
console.log(`[sf:debug] normalizeVisionMeetingRoute: firstLine="${firstLine}" match="${match?.[1]}"`);
return match?.[1] as VisionMeetingRoute | undefined;
}

View file

@ -3,6 +3,7 @@
// Verifies that identical consecutive tool calls are detected and blocked
// after exceeding the threshold, and that the guard resets properly.
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import {
checkToolCallLoop,
@ -11,233 +12,190 @@ import {
resetToolCallLoopGuard,
} from "../bootstrap/tool-call-loop-guard.ts";
// ═══════════════════════════════════════════════════════════════════════════
// Allows first N calls, blocks after threshold
// ═══════════════════════════════════════════════════════════════════════════
describe("tool-call loop guard", () => {
it("allows first N calls and blocks after threshold", () => {
resetToolCallLoopGuard();
console.log("\n── Loop guard: blocks after threshold ──");
for (let i = 1; i <= 4; i++) {
const result = checkToolCallLoop("web_search", { query: "same query" });
assert.strictEqual(result.block, false, `Call ${i} should be allowed`);
assert.strictEqual(
result.count,
i,
`Count should be ${i} after call ${i}`,
);
}
{
resetToolCallLoopGuard();
// First 4 identical calls should be allowed (threshold is 4)
for (let i = 1; i <= 4; i++) {
const result = checkToolCallLoop("web_search", { query: "same query" });
assert.ok(result.block === false, `Call ${i} should be allowed`);
assert.deepStrictEqual(
result.count,
i,
`Count should be ${i} after call ${i}`,
);
}
// 5th identical call should be blocked
const blocked = checkToolCallLoop("web_search", { query: "same query" });
assert.ok(blocked.block === true, "5th identical call should be blocked");
assert.ok(
blocked.reason!.includes("web_search"),
"Reason should mention tool name",
);
assert.ok(blocked.reason!.includes("5"), "Reason should mention count");
}
// ═══════════════════════════════════════════════════════════════════════════
// Different tool calls reset the streak
// ═══════════════════════════════════════════════════════════════════════════
console.log("\n── Loop guard: different calls reset streak ──");
{
resetToolCallLoopGuard();
checkToolCallLoop("web_search", { query: "query A" });
checkToolCallLoop("web_search", { query: "query A" });
checkToolCallLoop("web_search", { query: "query A" });
assert.deepStrictEqual(
getToolCallLoopCount(),
3,
"Count should be 3 after 3 identical calls",
);
// A different call resets the streak
const different = checkToolCallLoop("bash", { command: "ls" });
assert.ok(different.block === false, "Different tool call should be allowed");
assert.deepStrictEqual(
getToolCallLoopCount(),
1,
"Count should reset to 1 after different call",
);
// Same tool but different args also resets
checkToolCallLoop("web_search", { query: "query A" });
checkToolCallLoop("web_search", { query: "query B" }); // different args
assert.deepStrictEqual(
getToolCallLoopCount(),
1,
"Different args should reset count",
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Reset clears the guard
// ═══════════════════════════════════════════════════════════════════════════
console.log("\n── Loop guard: reset clears state ──");
{
resetToolCallLoopGuard();
checkToolCallLoop("web_search", { query: "q" });
checkToolCallLoop("web_search", { query: "q" });
checkToolCallLoop("web_search", { query: "q" });
assert.deepStrictEqual(
getToolCallLoopCount(),
3,
"Count should be 3 before reset",
);
resetToolCallLoopGuard();
assert.deepStrictEqual(
getToolCallLoopCount(),
0,
"Count should be 0 after reset",
);
// After reset, the same call starts fresh
const result = checkToolCallLoop("web_search", { query: "q" });
assert.ok(result.block === false, "Call after reset should be allowed");
assert.deepStrictEqual(
getToolCallLoopCount(),
1,
"Count should be 1 after first call post-reset",
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Disable makes guard permissive
// ═══════════════════════════════════════════════════════════════════════════
console.log("\n── Loop guard: disable allows everything ──");
disableToolCallLoopGuard();
for (let i = 0; i < 10; i++) {
const result = checkToolCallLoop("web_search", { query: "same" });
assert.ok(
result.block === false,
`Call ${i + 1} should be allowed when disabled`,
);
}
// Re-enable via reset
resetToolCallLoopGuard();
checkToolCallLoop("web_search", { query: "q" });
assert.deepStrictEqual(
getToolCallLoopCount(),
1,
"Guard should be active again after reset",
);
// ═══════════════════════════════════════════════════════════════════════════
// Arg order doesn't affect hash
// ═══════════════════════════════════════════════════════════════════════════
console.log("\n── Loop guard: arg order is normalized ──");
{
resetToolCallLoopGuard();
checkToolCallLoop("web_search", { query: "test", limit: 5 });
const result = checkToolCallLoop("web_search", { limit: 5, query: "test" }); // same args, different order
assert.ok(
result.block === false,
"Same args in different order should count as consecutive",
);
assert.deepStrictEqual(
getToolCallLoopCount(),
2,
"Should detect as same call regardless of key order",
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Nested/array arguments produce distinct hashes
// ═══════════════════════════════════════════════════════════════════════════
console.log("\n── Loop guard: nested args are not stripped ──");
{
resetToolCallLoopGuard();
// Simulate ask_user_questions-style calls with different nested content
for (let i = 1; i <= 5; i++) {
const result = checkToolCallLoop("ask_user_questions", {
questions: [{ id: `q${i}`, question: `Question ${i}?` }],
});
const blocked = checkToolCallLoop("web_search", { query: "same query" });
assert.strictEqual(blocked.block, true, "5th identical call should be blocked");
assert.ok(
result.block === false,
`Nested call ${i} with unique content should be allowed`,
blocked.reason!.includes("web_search"),
"Reason should mention tool name",
);
assert.deepStrictEqual(
assert.ok(blocked.reason!.includes("5"), "Reason should mention count");
});
it("different tool calls reset the streak", () => {
resetToolCallLoopGuard();
checkToolCallLoop("web_search", { query: "query A" });
checkToolCallLoop("web_search", { query: "query A" });
checkToolCallLoop("web_search", { query: "query A" });
assert.strictEqual(
getToolCallLoopCount(),
3,
"Count should be 3 after 3 identical calls",
);
const different = checkToolCallLoop("bash", { command: "ls" });
assert.strictEqual(different.block, false, "Different tool call should be allowed");
assert.strictEqual(
getToolCallLoopCount(),
1,
`Each unique nested call should reset count to 1`,
"Count should reset to 1 after different call",
);
}
// Truly identical nested calls should still be detected.
// ask_user_questions has a strict threshold of 1, so the 2nd identical call is blocked.
resetToolCallLoopGuard();
const first = checkToolCallLoop("ask_user_questions", {
questions: [{ id: "same", question: "Same?" }],
checkToolCallLoop("web_search", { query: "query A" });
checkToolCallLoop("web_search", { query: "query B" });
assert.strictEqual(
getToolCallLoopCount(),
1,
"Different args should reset count",
);
});
assert.ok(
first.block === false,
"First ask_user_questions call should be allowed",
);
const blocked = checkToolCallLoop("ask_user_questions", {
questions: [{ id: "same", question: "Same?" }],
});
assert.ok(
blocked.block === true,
"2nd identical ask_user_questions call should be blocked (strict threshold)",
);
// Non-strict tools still allow up to 4 identical calls
resetToolCallLoopGuard();
for (let i = 1; i <= 4; i++) {
const r = checkToolCallLoop("web_search", {
it("reset clears the guard", () => {
resetToolCallLoopGuard();
checkToolCallLoop("web_search", { query: "q" });
checkToolCallLoop("web_search", { query: "q" });
checkToolCallLoop("web_search", { query: "q" });
assert.strictEqual(
getToolCallLoopCount(),
3,
"Count should be 3 before reset",
);
resetToolCallLoopGuard();
assert.strictEqual(
getToolCallLoopCount(),
0,
"Count should be 0 after reset",
);
const result = checkToolCallLoop("web_search", { query: "q" });
assert.strictEqual(result.block, false, "Call after reset should be allowed");
assert.strictEqual(
getToolCallLoopCount(),
1,
"Count should be 1 after first call post-reset",
);
});
it("disable makes guard permissive", () => {
disableToolCallLoopGuard();
for (let i = 0; i < 10; i++) {
const result = checkToolCallLoop("web_search", { query: "same" });
assert.strictEqual(
result.block,
false,
`Call ${i + 1} should be allowed when disabled`,
);
}
resetToolCallLoopGuard();
checkToolCallLoop("web_search", { query: "q" });
assert.strictEqual(
getToolCallLoopCount(),
1,
"Guard should be active again after reset",
);
});
it("arg order does not affect hash", () => {
resetToolCallLoopGuard();
checkToolCallLoop("web_search", { query: "test", limit: 5 });
const result = checkToolCallLoop("web_search", { limit: 5, query: "test" });
assert.strictEqual(
result.block,
false,
"Same args in different order should count as consecutive",
);
assert.strictEqual(
getToolCallLoopCount(),
2,
"Should detect as same call regardless of key order",
);
});
it("nested/array arguments produce distinct hashes", () => {
resetToolCallLoopGuard();
for (let i = 1; i <= 5; i++) {
const result = checkToolCallLoop("ask_user_questions", {
questions: [{ id: `q${i}`, question: `Question ${i}?` }],
});
assert.strictEqual(
result.block,
false,
`Nested call ${i} with unique content should be allowed`,
);
assert.strictEqual(
getToolCallLoopCount(),
1,
`Each unique nested call should reset count to 1`,
);
}
resetToolCallLoopGuard();
const first = checkToolCallLoop("ask_user_questions", {
questions: [{ id: "same", question: "Same?" }],
});
assert.ok(
r.block === false,
`web_search call ${i} should be allowed (normal threshold)`,
assert.strictEqual(
first.block,
false,
"First ask_user_questions call should be allowed",
);
const blocked = checkToolCallLoop("ask_user_questions", {
questions: [{ id: "same", question: "Same?" }],
});
assert.strictEqual(
blocked.block,
true,
"2nd identical ask_user_questions call should be blocked (strict threshold)",
);
resetToolCallLoopGuard();
for (let i = 1; i <= 4; i++) {
const r = checkToolCallLoop("web_search", {
questions: [{ id: "same", question: "Same?" }],
});
assert.strictEqual(
r.block,
false,
`web_search call ${i} should be allowed (normal threshold)`,
);
}
const blockedNormal = checkToolCallLoop("web_search", {
questions: [{ id: "same", question: "Same?" }],
});
assert.strictEqual(
blockedNormal.block,
true,
"5th identical web_search call should be blocked",
);
}
const blockedNormal = checkToolCallLoop("web_search", {
questions: [{ id: "same", question: "Same?" }],
});
assert.ok(
blockedNormal.block === true,
"5th identical web_search call should be blocked",
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Nested object key order is normalized
// ═══════════════════════════════════════════════════════════════════════════
it("nested object key order is normalized", () => {
resetToolCallLoopGuard();
console.log("\n── Loop guard: nested key order is normalized ──");
{
resetToolCallLoopGuard();
checkToolCallLoop("tool", { outer: { b: 2, a: 1 } });
const _result = checkToolCallLoop("tool", { outer: { a: 1, b: 2 } });
assert.deepStrictEqual(
getToolCallLoopCount(),
2,
"Same nested args in different key order should match",
);
}
// ═══════════════════════════════════════════════════════════════════════════
checkToolCallLoop("tool", { outer: { b: 2, a: 1 } });
checkToolCallLoop("tool", { outer: { a: 1, b: 2 } });
assert.strictEqual(
getToolCallLoopCount(),
2,
"Same nested args in different key order should match",
);
});
});

View file

@ -2,6 +2,7 @@
//
// Tests registry loading, template resolution, auto-detection, and listing.
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import {
autoDetect,
@ -15,369 +16,397 @@ import {
workflowTemplateCommandDefinitions,
} from "../workflow-templates.ts";
// ═══════════════════════════════════════════════════════════════════════════
// Registry Loading
// ═══════════════════════════════════════════════════════════════════════════
describe("workflow-templates", () => {
describe("Registry Loading", () => {
it("should load registry with correct schema and templates", () => {
const registry = loadRegistry();
assert.ok(registry !== null, "Registry should load");
assert.deepStrictEqual(
registry.schemaVersion,
1,
"Registry schemaVersion should be 1",
);
assert.ok(
Object.keys(registry.templates).length >= 8,
"Should have at least 8 templates",
);
console.log("\n── Registry Loading ──");
// Verify required template keys exist
const expectedIds = [
"full-project",
"bugfix",
"small-feature",
"refactor",
"spike",
"hotfix",
"security-audit",
"dep-upgrade",
"product-plan",
"product-tracking",
];
for (const id of expectedIds) {
assert.ok(
id in registry.templates,
`Template "${id}" should exist in registry`,
);
}
{
const registry = loadRegistry();
assert.ok(registry !== null, "Registry should load");
assert.deepStrictEqual(
registry.schemaVersion,
1,
"Registry schemaVersion should be 1",
);
assert.ok(
Object.keys(registry.templates).length >= 8,
"Should have at least 8 templates",
);
// Verify each template has required fields
for (const [id, entry] of Object.entries(registry.templates)) {
assert.ok(
typeof entry.name === "string" && entry.name.length > 0,
`${id}: name should be non-empty string`,
);
assert.ok(
typeof entry.description === "string" &&
entry.description.length > 0,
`${id}: description should be non-empty`,
);
assert.ok(
typeof entry.file === "string" && entry.file.endsWith(".md"),
`${id}: file should be a .md path`,
);
assert.ok(
Array.isArray(entry.phases) && entry.phases.length > 0,
`${id}: phases should be non-empty array`,
);
assert.ok(
Array.isArray(entry.triggers) && entry.triggers.length > 0,
`${id}: triggers should be non-empty array`,
);
assert.ok(
loadWorkflowTemplate(id) !== null,
`${id}: registered template file should load`,
);
}
});
});
// Verify required template keys exist
const expectedIds = [
"full-project",
"bugfix",
"small-feature",
"refactor",
"spike",
"hotfix",
"security-audit",
"dep-upgrade",
"product-plan",
"product-tracking",
];
for (const id of expectedIds) {
assert.ok(
id in registry.templates,
`Template "${id}" should exist in registry`,
);
}
describe("Resolve by Name", () => {
it("should resolve exact match", () => {
const bugfix = resolveByName("bugfix");
assert.ok(bugfix !== null, 'Should resolve "bugfix"');
assert.deepStrictEqual(bugfix!.id, "bugfix", 'ID should be "bugfix"');
assert.deepStrictEqual(
bugfix!.confidence,
"exact",
"Exact name should have exact confidence",
);
});
// Verify each template has required fields
for (const [id, entry] of Object.entries(registry.templates)) {
assert.ok(
typeof entry.name === "string" && entry.name.length > 0,
`${id}: name should be non-empty string`,
);
assert.ok(
typeof entry.description === "string" && entry.description.length > 0,
`${id}: description should be non-empty`,
);
assert.ok(
typeof entry.file === "string" && entry.file.endsWith(".md"),
`${id}: file should be a .md path`,
);
assert.ok(
Array.isArray(entry.phases) && entry.phases.length > 0,
`${id}: phases should be non-empty array`,
);
assert.ok(
Array.isArray(entry.triggers) && entry.triggers.length > 0,
`${id}: triggers should be non-empty array`,
);
assert.ok(
loadWorkflowTemplate(id) !== null,
`${id}: registered template file should load`,
);
}
}
it("should resolve case-insensitive name match", () => {
const spike = resolveByName("Research Spike");
assert.ok(spike !== null, 'Should resolve "Research Spike" by name');
assert.deepStrictEqual(spike!.id, "spike", "Should resolve to spike");
});
// ═══════════════════════════════════════════════════════════════════════════
// Resolve by Name
// ═══════════════════════════════════════════════════════════════════════════
it("should resolve aliases", () => {
const bug = resolveByName("bug");
assert.ok(bug !== null, 'Should resolve "bug" alias');
assert.deepStrictEqual(
bug!.id,
"bugfix",
'Alias "bug" should map to bugfix',
);
console.log("\n── Resolve by Name ──");
const feat = resolveByName("feat");
assert.ok(feat !== null, 'Should resolve "feat" alias');
assert.deepStrictEqual(
feat!.id,
"small-feature",
'Alias "feat" should map to small-feature',
);
{
// Exact match
const bugfix = resolveByName("bugfix");
assert.ok(bugfix !== null, 'Should resolve "bugfix"');
assert.deepStrictEqual(bugfix!.id, "bugfix", 'ID should be "bugfix"');
assert.deepStrictEqual(
bugfix!.confidence,
"exact",
"Exact name should have exact confidence",
);
const deps = resolveByName("deps");
assert.ok(deps !== null, 'Should resolve "deps" alias');
assert.deepStrictEqual(
deps!.id,
"dep-upgrade",
'Alias "deps" should map to dep-upgrade',
);
// Case-insensitive name match
const spike = resolveByName("Research Spike");
assert.ok(spike !== null, 'Should resolve "Research Spike" by name');
assert.deepStrictEqual(spike!.id, "spike", "Should resolve to spike");
const telemetry = resolveByName("telemetry");
assert.ok(telemetry !== null, 'Should resolve "telemetry" alias');
assert.deepStrictEqual(
telemetry!.id,
"product-tracking",
'Alias "telemetry" should map to product-tracking',
);
// Alias match
const bug = resolveByName("bug");
assert.ok(bug !== null, 'Should resolve "bug" alias');
assert.deepStrictEqual(bug!.id, "bugfix", 'Alias "bug" should map to bugfix');
const product = resolveByName("product");
assert.ok(product !== null, 'Should resolve "product" alias');
assert.deepStrictEqual(
product!.id,
"product-plan",
'Alias "product" should map to product-plan',
);
});
const feat = resolveByName("feat");
assert.ok(feat !== null, 'Should resolve "feat" alias');
assert.deepStrictEqual(
feat!.id,
"small-feature",
'Alias "feat" should map to small-feature',
);
it("should return null for unknown template", () => {
const missing = resolveByName("nonexistent-template");
assert.ok(missing === null, "Should return null for unknown template");
});
});
const deps = resolveByName("deps");
assert.ok(deps !== null, 'Should resolve "deps" alias');
assert.deepStrictEqual(
deps!.id,
"dep-upgrade",
'Alias "deps" should map to dep-upgrade',
);
describe("Auto-Detection", () => {
it("should detect bugfix from 'fix' keyword", () => {
const fixMatches = autoDetect("fix the login button");
assert.ok(
fixMatches.length > 0,
'Should detect matches for "fix the login button"',
);
assert.ok(
fixMatches.some((m) => m.id === "bugfix"),
"Should include bugfix in matches",
);
});
const telemetry = resolveByName("telemetry");
assert.ok(telemetry !== null, 'Should resolve "telemetry" alias');
assert.deepStrictEqual(
telemetry!.id,
"product-tracking",
'Alias "telemetry" should map to product-tracking',
);
it("should detect spike from 'research' keyword", () => {
const researchMatches = autoDetect("research authentication libraries");
assert.ok(
researchMatches.length > 0,
'Should detect matches for "research"',
);
assert.ok(
researchMatches.some((m) => m.id === "spike"),
"Should include spike in matches",
);
});
const product = resolveByName("product");
assert.ok(product !== null, 'Should resolve "product" alias');
assert.deepStrictEqual(
product!.id,
"product-plan",
'Alias "product" should map to product-plan',
);
it("should detect hotfix from 'urgent' keyword", () => {
const urgentMatches = autoDetect("urgent production is down");
assert.ok(
urgentMatches.length > 0,
'Should detect matches for "urgent"',
);
assert.ok(
urgentMatches.some((m) => m.id === "hotfix"),
"Should include hotfix in matches",
);
});
// No match
const missing = resolveByName("nonexistent-template");
assert.ok(missing === null, "Should return null for unknown template");
}
it("should detect dep-upgrade from 'upgrade' keyword", () => {
const upgradeMatches = autoDetect("upgrade react to v19");
assert.ok(
upgradeMatches.length > 0,
'Should detect matches for "upgrade"',
);
assert.ok(
upgradeMatches.some((m) => m.id === "dep-upgrade"),
"Should include dep-upgrade in matches",
);
});
// ═══════════════════════════════════════════════════════════════════════════
// Auto-Detection
// ═══════════════════════════════════════════════════════════════════════════
it("should detect product-tracking from product analytics phrasing", () => {
const trackingMatches = autoDetect(
"create product analytics tracking plan",
);
assert.ok(
trackingMatches.length > 0,
'Should detect matches for "product analytics tracking plan"',
);
assert.ok(
trackingMatches.some((m) => m.id === "product-tracking"),
"Should include product-tracking in matches",
);
});
console.log("\n── Auto-Detection ──");
it("should detect product-plan from planning phrasing", () => {
const productPlanMatches = autoDetect(
"plan the product we need to develop",
);
assert.ok(
productPlanMatches.length > 0,
'Should detect matches for "plan the product we need to develop"',
);
assert.ok(
productPlanMatches.some((m) => m.id === "product-plan"),
"Should include product-plan in matches",
);
});
{
// Should detect bugfix from "fix" keyword
const fixMatches = autoDetect("fix the login button");
assert.ok(
fixMatches.length > 0,
'Should detect matches for "fix the login button"',
);
assert.ok(
fixMatches.some((m) => m.id === "bugfix"),
"Should include bugfix in matches",
);
it("should score multi-word triggers higher", () => {
const projectMatches = autoDetect("create a new project from scratch");
const projectMatch = projectMatches.find((m) => m.id === "full-project");
assert.ok(
projectMatch !== undefined,
'Should detect full-project for "from scratch"',
);
});
// Should detect spike from "research" keyword
const researchMatches = autoDetect("research authentication libraries");
assert.ok(researchMatches.length > 0, 'Should detect matches for "research"');
assert.ok(
researchMatches.some((m) => m.id === "spike"),
"Should include spike in matches",
);
it("should return no matches for empty input", () => {
const emptyMatches = autoDetect("");
assert.deepStrictEqual(
emptyMatches.length,
0,
"Empty input should return no matches",
);
});
});
// Should detect hotfix from "urgent" keyword
const urgentMatches = autoDetect("urgent production is down");
assert.ok(urgentMatches.length > 0, 'Should detect matches for "urgent"');
assert.ok(
urgentMatches.some((m) => m.id === "hotfix"),
"Should include hotfix in matches",
);
describe("List Templates", () => {
it("should list templates with header and usage hint", () => {
const output = listTemplates();
assert.ok(output.includes("Workflow Templates"), "Should have header");
assert.ok(output.includes("bugfix"), "Should list bugfix");
assert.ok(output.includes("spike"), "Should list spike");
assert.ok(output.includes("hotfix"), "Should list hotfix");
assert.ok(
output.includes("product-tracking"),
"Should list product-tracking",
);
assert.ok(output.includes("product-plan"), "Should list product-plan");
assert.ok(output.includes("/sf start"), "Should include usage hint");
});
});
// Should detect dep-upgrade from "upgrade" keyword
const upgradeMatches = autoDetect("upgrade react to v19");
assert.ok(upgradeMatches.length > 0, 'Should detect matches for "upgrade"');
assert.ok(
upgradeMatches.some((m) => m.id === "dep-upgrade"),
"Should include dep-upgrade in matches",
);
describe("Start Usage and Completions", () => {
it("should include all templates in command definitions and usage", () => {
const registry = loadRegistry();
const commandDefs = workflowTemplateCommandDefinitions();
const commandIds = commandDefs.map((entry) => entry.cmd);
for (const id of Object.keys(registry.templates)) {
assert.ok(
commandIds.includes(id),
`Start completions should include ${id}`,
);
}
// Should detect product-tracking from product analytics phrasing
const trackingMatches = autoDetect("create product analytics tracking plan");
assert.ok(
trackingMatches.length > 0,
'Should detect matches for "product analytics tracking plan"',
);
assert.ok(
trackingMatches.some((m) => m.id === "product-tracking"),
"Should include product-tracking in matches",
);
const usage = formatStartUsage();
assert.ok(
usage.includes("product-plan"),
"Usage should include product-plan",
);
assert.ok(
usage.includes("/sf workflow run"),
"Usage should distinguish YAML workflow definitions",
);
});
});
// Should detect product-plan from planning phrasing
const productPlanMatches = autoDetect("plan the product we need to develop");
assert.ok(
productPlanMatches.length > 0,
'Should detect matches for "plan the product we need to develop"',
);
assert.ok(
productPlanMatches.some((m) => m.id === "product-plan"),
"Should include product-plan in matches",
);
describe("Template Info", () => {
it("should return info for known templates", () => {
const info = getTemplateInfo("bugfix");
assert.ok(info !== null, "Should return info for bugfix");
assert.ok(info!.includes("Bug Fix"), "Should include template name");
assert.ok(info!.includes("triage"), "Should include phase names");
assert.ok(info!.includes("Triggers"), "Should include triggers section");
});
// Multi-word triggers should score higher
const projectMatches = autoDetect("create a new project from scratch");
const projectMatch = projectMatches.find((m) => m.id === "full-project");
assert.ok(
projectMatch !== undefined,
'Should detect full-project for "from scratch"',
);
it("should return null for unknown template", () => {
const missing = getTemplateInfo("nonexistent");
assert.ok(missing === null, "Should return null for unknown template");
});
});
// Empty input should return no matches
const emptyMatches = autoDetect("");
assert.deepStrictEqual(
emptyMatches.length,
0,
"Empty input should return no matches",
);
}
describe("Load Workflow Template Content", () => {
it("should load bugfix template content", () => {
const content = loadWorkflowTemplate("bugfix");
assert.ok(content !== null, "Should load bugfix template");
assert.ok(
content!.includes("Bugfix Workflow"),
"Should contain workflow title",
);
assert.ok(
content!.includes("Phase 1: Triage"),
"Should contain triage phase",
);
assert.ok(
content!.includes("Phase 4: Ship"),
"Should contain ship phase",
);
});
// ═══════════════════════════════════════════════════════════════════════════
// List Templates
// ═══════════════════════════════════════════════════════════════════════════
it("should load hotfix template content", () => {
const hotfixContent = loadWorkflowTemplate("hotfix");
assert.ok(hotfixContent !== null, "Should load hotfix template");
assert.ok(
hotfixContent!.includes("Hotfix Workflow"),
"Should contain hotfix title",
);
});
console.log("\n── List Templates ──");
it("should load product-tracking template content", () => {
const productTrackingContent = loadWorkflowTemplate("product-tracking");
assert.ok(
productTrackingContent !== null,
"Should load product-tracking template",
);
assert.ok(
productTrackingContent!.includes("Product Tracking Workflow"),
"Should contain product-tracking title",
);
assert.ok(
productTrackingContent!.includes("Phase 5: Implement Tracking"),
"Should contain implementation phase",
);
});
{
const output = listTemplates();
assert.ok(output.includes("Workflow Templates"), "Should have header");
assert.ok(output.includes("bugfix"), "Should list bugfix");
assert.ok(output.includes("spike"), "Should list spike");
assert.ok(output.includes("hotfix"), "Should list hotfix");
assert.ok(
output.includes("product-tracking"),
"Should list product-tracking",
);
assert.ok(output.includes("product-plan"), "Should list product-plan");
assert.ok(output.includes("/sf start"), "Should include usage hint");
}
it("should load product-plan template content", () => {
const productPlanContent = loadWorkflowTemplate("product-plan");
assert.ok(productPlanContent !== null, "Should load product-plan template");
assert.ok(
productPlanContent!.includes("Product Plan Workflow"),
"Should contain product-plan title",
);
assert.ok(
productPlanContent!.includes("Phase 3: Plan Implementation Slices"),
"Should contain implementation slice planning phase",
);
});
// ═══════════════════════════════════════════════════════════════════════════
// Start Usage and Completions
// ═══════════════════════════════════════════════════════════════════════════
it("should return null for unknown template", () => {
const missingContent = loadWorkflowTemplate("nonexistent");
assert.ok(
missingContent === null,
"Should return null for unknown template",
);
});
});
console.log("\n── Start Usage and Completions ──");
describe("Milestone Scaffolding", () => {
it("should scaffold bugfix milestone slices", () => {
const bugfixSlices = scaffoldMilestoneSlices("bugfix");
assert.ok(bugfixSlices !== null, "Should scaffold bugfix milestone slices");
assert.equal(
bugfixSlices!.length,
3,
"Bugfix scaffold should create 3 slices",
);
assert.equal(bugfixSlices![0].sliceId, "S01");
assert.ok(bugfixSlices![1].depends.includes("S01"));
});
{
const registry = loadRegistry();
const commandDefs = workflowTemplateCommandDefinitions();
const commandIds = commandDefs.map((entry) => entry.cmd);
for (const id of Object.keys(registry.templates)) {
assert.ok(commandIds.includes(id), `Start completions should include ${id}`);
}
it("should scaffold via alias", () => {
const featureSlices = scaffoldMilestoneSlices("feat");
assert.ok(featureSlices !== null, "Should scaffold via alias");
assert.equal(
featureSlices![0].title,
"Define the user-facing contract",
);
});
const usage = formatStartUsage();
assert.ok(usage.includes("product-plan"), "Usage should include product-plan");
assert.ok(
usage.includes("/sf workflow run"),
"Usage should distinguish YAML workflow definitions",
);
}
it("should scaffold product-plan milestone slices", () => {
const productPlanSlices = scaffoldMilestoneSlices("product-plan");
assert.ok(
productPlanSlices !== null,
"Should scaffold product-plan milestone slices",
);
assert.equal(
productPlanSlices!.length,
4,
"Product-plan scaffold should create 4 slices",
);
assert.equal(
productPlanSlices![0].title,
"Model the product and value flow",
);
assert.ok(productPlanSlices![2].depends.includes("S02"));
});
// ═══════════════════════════════════════════════════════════════════════════
// Template Info
// ═══════════════════════════════════════════════════════════════════════════
console.log("\n── Template Info ──");
{
const info = getTemplateInfo("bugfix");
assert.ok(info !== null, "Should return info for bugfix");
assert.ok(info!.includes("Bug Fix"), "Should include template name");
assert.ok(info!.includes("triage"), "Should include phase names");
assert.ok(info!.includes("Triggers"), "Should include triggers section");
const missing = getTemplateInfo("nonexistent");
assert.ok(missing === null, "Should return null for unknown template");
}
// ═══════════════════════════════════════════════════════════════════════════
// Load Workflow Template Content
// ═══════════════════════════════════════════════════════════════════════════
console.log("\n── Load Workflow Template ──");
{
const content = loadWorkflowTemplate("bugfix");
assert.ok(content !== null, "Should load bugfix template");
assert.ok(
content!.includes("Bugfix Workflow"),
"Should contain workflow title",
);
assert.ok(
content!.includes("Phase 1: Triage"),
"Should contain triage phase",
);
assert.ok(content!.includes("Phase 4: Ship"), "Should contain ship phase");
const hotfixContent = loadWorkflowTemplate("hotfix");
assert.ok(hotfixContent !== null, "Should load hotfix template");
assert.ok(
hotfixContent!.includes("Hotfix Workflow"),
"Should contain hotfix title",
);
const productTrackingContent = loadWorkflowTemplate("product-tracking");
assert.ok(
productTrackingContent !== null,
"Should load product-tracking template",
);
assert.ok(
productTrackingContent!.includes("Product Tracking Workflow"),
"Should contain product-tracking title",
);
assert.ok(
productTrackingContent!.includes("Phase 5: Implement Tracking"),
"Should contain implementation phase",
);
const productPlanContent = loadWorkflowTemplate("product-plan");
assert.ok(productPlanContent !== null, "Should load product-plan template");
assert.ok(
productPlanContent!.includes("Product Plan Workflow"),
"Should contain product-plan title",
);
assert.ok(
productPlanContent!.includes("Phase 3: Plan Implementation Slices"),
"Should contain implementation slice planning phase",
);
const missingContent = loadWorkflowTemplate("nonexistent");
assert.ok(missingContent === null, "Should return null for unknown template");
}
// ═══════════════════════════════════════════════════════════════════════════
// Milestone Scaffolding
// ═══════════════════════════════════════════════════════════════════════════
console.log("\n── Milestone Scaffolding ──");
{
const bugfixSlices = scaffoldMilestoneSlices("bugfix");
assert.ok(bugfixSlices !== null, "Should scaffold bugfix milestone slices");
assert.equal(
bugfixSlices!.length,
3,
"Bugfix scaffold should create 3 slices",
);
assert.equal(bugfixSlices![0].sliceId, "S01");
assert.ok(bugfixSlices![1].depends.includes("S01"));
const featureSlices = scaffoldMilestoneSlices("feat");
assert.ok(featureSlices !== null, "Should scaffold via alias");
assert.equal(featureSlices![0].title, "Define the user-facing contract");
const productPlanSlices = scaffoldMilestoneSlices("product-plan");
assert.ok(
productPlanSlices !== null,
"Should scaffold product-plan milestone slices",
);
assert.equal(
productPlanSlices!.length,
4,
"Product-plan scaffold should create 4 slices",
);
assert.equal(productPlanSlices![0].title, "Model the product and value flow");
assert.ok(productPlanSlices![2].depends.includes("S02"));
const missingScaffold = scaffoldMilestoneSlices("nonexistent");
assert.equal(missingScaffold, null, "Unknown template should not scaffold");
}
// ═══════════════════════════════════════════════════════════════════════════
it("should return null for unknown template", () => {
const missingScaffold = scaffoldMilestoneSlices("nonexistent");
assert.equal(missingScaffold, null, "Unknown template should not scaffold");
});
});
});

View file

@ -12,7 +12,7 @@
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { test } from 'vitest';
import { test, describe } from 'vitest';
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -38,15 +38,12 @@ const docsToCheck: { file: string; label: string }[] = [
{ file: "workflows/debug-extension.md", label: "debug-extension.md" },
];
test("create-sf-extension docs use ~/.pi/agent/extensions/ for community extensions", async (t) => {
if (!skillDirExists) {
return; // skip: "create-sf-extension skill not present in this repo";
return;
}
describe("create-sf-extension docs use ~/.pi/agent/extensions/ for community extensions", () => {
for (const { file, label } of docsToCheck) {
test(
`${label} references ~/.pi/agent/extensions/ for global extensions`,
() => {
if (!skillDirExists) return;
const content = readSkillFile(file);
// The doc should reference ~/.pi/agent/extensions/ (community path)
@ -59,15 +56,12 @@ test("create-sf-extension docs use ~/.pi/agent/extensions/ for community extensi
}
});
test("create-sf-extension docs do NOT direct users to install in ~/.sf/agent/extensions/", async (t) => {
if (!skillDirExists) {
return; // skip: "create-sf-extension skill not present in this repo";
return;
}
describe("create-sf-extension docs do NOT direct users to install in ~/.sf/agent/extensions/", () => {
for (const { file, label } of docsToCheck) {
test(
`${label} does not tell users to place extensions in ~/.sf/agent/extensions/`,
() => {
if (!skillDirExists) return;
const content = readSkillFile(file);
// ~/.sf/agent/extensions/ should only appear in context that clearly marks

View file

@ -209,18 +209,19 @@ test("resolveSearchProvider returns 'brave' when only BRAVE_API_KEY is set", (t)
assert.equal(provider, "brave");
});
test("resolveSearchProvider returns null when neither key is set", (t) => {
const origTavily = process.env.TAVILY_API_KEY;
const origBrave = process.env.BRAVE_API_KEY;
delete process.env.TAVILY_API_KEY;
delete process.env.BRAVE_API_KEY;
test("resolveSearchProvider returns null when neither key is set", () => {
const keys = ["TAVILY_API_KEY", "BRAVE_API_KEY", "MINIMAX_API_KEY", "MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY", "SERPER_API_KEY", "EXA_API_KEY", "OLLAMA_API_KEY"];
const originals: Record<string, string | undefined> = {};
for (const k of keys) {
originals[k] = process.env[k];
delete process.env[k];
}
afterEach(() => {
if (origTavily !== undefined) process.env.TAVILY_API_KEY = origTavily;
else delete process.env.BRAVE_API_KEY;
if (origBrave !== undefined) process.env.BRAVE_API_KEY = origBrave;
else delete process.env.BRAVE_API_KEY;
for (const k of keys) {
if (originals[k] !== undefined) process.env[k] = originals[k];
else delete process.env[k];
}
});
const provider = resolveSearchProvider();