251 lines
7.6 KiB
TypeScript
251 lines
7.6 KiB
TypeScript
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);
|
|
});
|