From b6358c1c14212b32743a1e6a7a66604df813e321 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 05:39:38 +0200 Subject: [PATCH] test: commit current vitest fixes --- .../extensions/sf/milestone-quality.ts | 1 + .../sf/tests/tool-call-loop-guard.test.ts | 386 +++++----- .../sf/tests/workflow-templates.test.ts | 697 +++++++++--------- src/tests/create-sf-extension-paths.test.ts | 16 +- src/tests/search-tavily.test.ts | 21 +- 5 files changed, 552 insertions(+), 569 deletions(-) diff --git a/src/resources/extensions/sf/milestone-quality.ts b/src/resources/extensions/sf/milestone-quality.ts index 8f82fe9ca..c02f803fd 100644 --- a/src/resources/extensions/sf/milestone-quality.ts +++ b/src/resources/extensions/sf/milestone-quality.ts @@ -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; } diff --git a/src/resources/extensions/sf/tests/tool-call-loop-guard.test.ts b/src/resources/extensions/sf/tests/tool-call-loop-guard.test.ts index 7382a1beb..09b5e2fe6 100644 --- a/src/resources/extensions/sf/tests/tool-call-loop-guard.test.ts +++ b/src/resources/extensions/sf/tests/tool-call-loop-guard.test.ts @@ -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", + ); + }); +}); diff --git a/src/resources/extensions/sf/tests/workflow-templates.test.ts b/src/resources/extensions/sf/tests/workflow-templates.test.ts index b14e21822..5cee11097 100644 --- a/src/resources/extensions/sf/tests/workflow-templates.test.ts +++ b/src/resources/extensions/sf/tests/workflow-templates.test.ts @@ -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"); + }); + }); +}); diff --git a/src/tests/create-sf-extension-paths.test.ts b/src/tests/create-sf-extension-paths.test.ts index 001d22a14..61e9b4da1 100644 --- a/src/tests/create-sf-extension-paths.test.ts +++ b/src/tests/create-sf-extension-paths.test.ts @@ -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 diff --git a/src/tests/search-tavily.test.ts b/src/tests/search-tavily.test.ts index 382bc32c6..684f286f6 100644 --- a/src/tests/search-tavily.test.ts +++ b/src/tests/search-tavily.test.ts @@ -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 = {}; + 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();