test: commit current vitest fixes
This commit is contained in:
parent
0e769dbf13
commit
b6358c1c14
5 changed files with 552 additions and 569 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue