feat: build local skills registry
This commit is contained in:
250
tests/skills-registry.test.ts
Normal file
250
tests/skills-registry.test.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, sep } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
type AuditUsedSkill,
|
||||
loadSkillsRegistry,
|
||||
parseSkillsRegistry,
|
||||
toAuditUsedSkill,
|
||||
SKILL_CATEGORIES,
|
||||
} from "../lib/skills-registry";
|
||||
|
||||
function assertIncludes(values: readonly string[], value: string) {
|
||||
assert.ok(values.includes(value), `Expected ${value} in [${values.join(", ")}]`);
|
||||
}
|
||||
|
||||
function withTempProjectRegistry(
|
||||
source: string,
|
||||
run: () => Promise<void> | void,
|
||||
) {
|
||||
return mkdtemp(`${tmpdir()}${sep}`).then(async (projectRoot) => {
|
||||
const registryPath = join(projectRoot, "skills.md");
|
||||
const originalCwd = process.cwd();
|
||||
await writeFile(registryPath, source, "utf8");
|
||||
process.chdir(projectRoot);
|
||||
|
||||
try {
|
||||
await run();
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
await rm(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("parseSkillsRegistry parses valid entries, trims whitespace, and normalizes category", async () => {
|
||||
const registrySource = `
|
||||
## Design Audit
|
||||
Purpose: Evaluate layout, visual hierarchy, and CTA clarity.
|
||||
When to use: Use when a homepage is available and should be assessed for conversion quality.
|
||||
When not to use: Don't run during technical outages or non-web touchpoints.
|
||||
Required input: Homepage URL, top-level page sections, style language, brand context.
|
||||
Expected output: Prioritized improvement list with concrete design changes.
|
||||
Category: design
|
||||
Version: 2026.06
|
||||
Source: skills/design-audit.md
|
||||
`;
|
||||
|
||||
const parsed = parseSkillsRegistry(registrySource);
|
||||
|
||||
assert.equal(parsed.length, 1);
|
||||
const entry = parsed.at(0);
|
||||
assert.ok(entry);
|
||||
assert.equal(entry!.name, "Design Audit");
|
||||
assert.equal(entry!.purpose, "Evaluate layout, visual hierarchy, and CTA clarity.");
|
||||
assert.equal(entry!.category, "design");
|
||||
assert.equal(entry!.version, "2026.06");
|
||||
assert.equal(entry!.source, "skills/design-audit.md");
|
||||
});
|
||||
|
||||
test("parseSkillsRegistry accepts indented field labels", () => {
|
||||
const registrySource = `
|
||||
## Local SEO Boost
|
||||
Purpose: Evaluate visibility for local search with nearby intent.
|
||||
When to use: Use for local business pages and service locations.
|
||||
When not to use: Avoid for non-local marketing pages.
|
||||
Required input: City, address, NAP consistency.
|
||||
Expected output: Prioritized local SEO recommendations.
|
||||
Category: seo
|
||||
`;
|
||||
|
||||
const parsed = parseSkillsRegistry(registrySource);
|
||||
|
||||
assert.equal(parsed.length, 1);
|
||||
const entry = parsed.at(0);
|
||||
assert.ok(entry);
|
||||
assert.equal(entry!.name, "Local SEO Boost");
|
||||
assert.equal(entry!.purpose, "Evaluate visibility for local search with nearby intent.");
|
||||
assert.equal(entry!.category, "seo");
|
||||
});
|
||||
|
||||
test("parseSkillsRegistry throws for missing required fields", () => {
|
||||
const registrySource = `
|
||||
## UX Friction Review
|
||||
Purpose: Review interaction patterns for friction points.
|
||||
When to use: Use for lead capture and booking flows.
|
||||
When not to use: Use only when there is a user journey.
|
||||
Required input: Session flow and target action.
|
||||
Category: ux
|
||||
`;
|
||||
|
||||
assert.throws(
|
||||
() => parseSkillsRegistry(registrySource),
|
||||
/missing required field "Expected output"/i,
|
||||
);
|
||||
});
|
||||
|
||||
test("parseSkillsRegistry throws for unknown category", () => {
|
||||
const registrySource = `
|
||||
## Bad Category Example
|
||||
Purpose: Example.
|
||||
When to use: Example scenario.
|
||||
When not to use: Never for this case.
|
||||
Required input: Example data.
|
||||
Expected output: Example output.
|
||||
Category: analytics
|
||||
`;
|
||||
|
||||
assert.throws(
|
||||
() => parseSkillsRegistry(registrySource),
|
||||
/unknown category "analytics"/i,
|
||||
);
|
||||
});
|
||||
|
||||
test("parseSkillsRegistry throws for duplicate skill names", () => {
|
||||
const registrySource = `
|
||||
## Local SEO Boost
|
||||
Purpose: Strengthen local SERPs.
|
||||
When to use: Use for local service businesses.
|
||||
When not to use: Not for international-only landing pages.
|
||||
Required input: Name, address, service area.
|
||||
Expected output: Local SEO gaps and quick wins.
|
||||
Category: seo
|
||||
|
||||
## Local SEO Boost
|
||||
Purpose: Another local SEO pass.
|
||||
When to use: Use for new regions.
|
||||
When not to use: Skip for pure lead-gen pages.
|
||||
Required input: Name, address, service area.
|
||||
Expected output: Competitor baseline.
|
||||
Category: seo
|
||||
`;
|
||||
|
||||
assert.throws(
|
||||
() => parseSkillsRegistry(registrySource),
|
||||
/duplicate skill name "Local SEO Boost"/i,
|
||||
);
|
||||
});
|
||||
|
||||
test("parseSkillsRegistry accepts all configured categories", () => {
|
||||
assertIncludes(SKILL_CATEGORIES, "design");
|
||||
assertIncludes(SKILL_CATEGORIES, "ux");
|
||||
assertIncludes(SKILL_CATEGORIES, "marketing");
|
||||
assertIncludes(SKILL_CATEGORIES, "copy");
|
||||
assertIncludes(SKILL_CATEGORIES, "seo");
|
||||
assertIncludes(SKILL_CATEGORIES, "offer");
|
||||
|
||||
const registrySource = SKILL_CATEGORIES.map(
|
||||
(category) => `
|
||||
## ${category}-skill
|
||||
Purpose: Valid for ${category}.
|
||||
When to use: Use for ${category} tasks.
|
||||
When not to use: Skip when ${category} is not in scope.
|
||||
Required input: Category inputs.
|
||||
Expected output: Category-specific recommendations.
|
||||
Category: ${category}
|
||||
`,
|
||||
).join("\n\n");
|
||||
|
||||
const parsed = parseSkillsRegistry(registrySource);
|
||||
assert.equal(parsed.length, SKILL_CATEGORIES.length);
|
||||
for (const category of SKILL_CATEGORIES) {
|
||||
const match = parsed.find((entry) => entry.name === `${category}-skill`);
|
||||
assert.ok(match, `Expected parsed entry for ${category}`);
|
||||
assert.equal(match.category, category);
|
||||
}
|
||||
});
|
||||
|
||||
test("loadSkillsRegistry reads skills.md from process.cwd() by default", async () => {
|
||||
await withTempProjectRegistry(
|
||||
`
|
||||
## Offer Writing
|
||||
Purpose: Build offer-focused copy for outreach.
|
||||
When to use: Use before drafting proposals.
|
||||
When not to use: Avoid when no offer exists.
|
||||
Required input: Offer structure and pricing envelope.
|
||||
Expected output: Offer draft and pricing emphasis.
|
||||
Category: offer
|
||||
`,
|
||||
async () => {
|
||||
const parsed = await loadSkillsRegistry();
|
||||
const parsedEntry = parsed.find((entry) => entry.name === "Offer Writing");
|
||||
|
||||
assert.ok(parsedEntry);
|
||||
assert.equal(parsedEntry.category, "offer");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("loadSkillsRegistry accepts an explicit registry path", async () => {
|
||||
const projectRoot = await mkdtemp(`${tmpdir()}${sep}`);
|
||||
const registryPath = join(projectRoot, "seed-skills.md");
|
||||
|
||||
await writeFile(
|
||||
registryPath,
|
||||
`
|
||||
## Design Audit
|
||||
Purpose: Validate design quality for local business pages.
|
||||
When to use: Use for a quick visual prioritization pass.
|
||||
When not to use: Skip when no public page exists.
|
||||
Required input: Homepage URL and target conversion goal.
|
||||
Expected output: Ranked design actions with confidence.
|
||||
Category: design
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
try {
|
||||
const parsed = await loadSkillsRegistry(registryPath);
|
||||
assert.equal(parsed.at(0)?.name, "Design Audit");
|
||||
} finally {
|
||||
await rm(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("toAuditUsedSkill returns only required audit-facing fields", () => {
|
||||
const skill = {
|
||||
name: "Copy Clarity",
|
||||
purpose: "Reduce complexity and improve readability.",
|
||||
whenToUse: "When existing copy is verbose.",
|
||||
whenNotToUse: "Skip if website is plain text-only.",
|
||||
requiredInput: "Page sections and CTAs.",
|
||||
expectedOutput: "A concise writing pass.",
|
||||
category: "copy",
|
||||
version: "1.0",
|
||||
source: "skills/copy-clarity.md",
|
||||
} satisfies {
|
||||
name: string;
|
||||
purpose: string;
|
||||
whenToUse: string;
|
||||
whenNotToUse: string;
|
||||
requiredInput: string;
|
||||
expectedOutput: string;
|
||||
category: "copy";
|
||||
version: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
const auditUsed = toAuditUsedSkill(skill);
|
||||
const expected: AuditUsedSkill = {
|
||||
name: "Copy Clarity",
|
||||
category: "copy",
|
||||
version: "1.0",
|
||||
source: "skills/copy-clarity.md",
|
||||
};
|
||||
|
||||
assert.deepEqual(auditUsed, expected);
|
||||
});
|
||||
Reference in New Issue
Block a user