feat: build local skills registry
This commit is contained in:
231
tests/audit-skills-schema.test.ts
Normal file
231
tests/audit-skills-schema.test.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import ts from "typescript";
|
||||
|
||||
const schemaPath = join(process.cwd(), "convex", "schema.ts");
|
||||
const auditsPath = join(process.cwd(), "convex", "audits.ts");
|
||||
const schemaSource = readFileSync(schemaPath, "utf8");
|
||||
const auditsSource = readFileSync(auditsPath, "utf8");
|
||||
|
||||
const sourceFile = ts.createSourceFile(
|
||||
"audits.ts",
|
||||
auditsSource,
|
||||
ts.ScriptTarget.ES2022,
|
||||
true,
|
||||
);
|
||||
|
||||
function extractTableSection(tableName: string) {
|
||||
const marker = `${tableName}: defineTable({`;
|
||||
const markerIndex = schemaSource.indexOf(marker);
|
||||
assert.notEqual(
|
||||
markerIndex,
|
||||
-1,
|
||||
`Expected schema table definition for ${tableName}.`,
|
||||
);
|
||||
|
||||
const objectStart = schemaSource.indexOf("{", markerIndex);
|
||||
let depth = 0;
|
||||
let objectEnd = -1;
|
||||
|
||||
for (let index = objectStart; index < schemaSource.length; index += 1) {
|
||||
if (schemaSource[index] === "{") {
|
||||
depth += 1;
|
||||
} else if (schemaSource[index] === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
objectEnd = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.notEqual(
|
||||
objectEnd,
|
||||
-1,
|
||||
`Could not parse schema object for ${tableName}.`,
|
||||
);
|
||||
const objectBlock = schemaSource.slice(objectStart, objectEnd + 1);
|
||||
|
||||
return { objectBlock };
|
||||
}
|
||||
|
||||
function extractExportSource(name: string) {
|
||||
const marker = `export const ${name} = `;
|
||||
const declarationIndex = auditsSource.indexOf(marker);
|
||||
assert.notEqual(
|
||||
declarationIndex,
|
||||
-1,
|
||||
`Expected declaration for ${name}.`,
|
||||
);
|
||||
|
||||
const openBraceIndex = auditsSource.indexOf("{", declarationIndex);
|
||||
let depth = 0;
|
||||
let end = -1;
|
||||
|
||||
for (let index = openBraceIndex; index < auditsSource.length; index += 1) {
|
||||
const char = auditsSource[index];
|
||||
if (char === "{") {
|
||||
depth += 1;
|
||||
} else if (char === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.notEqual(
|
||||
end,
|
||||
-1,
|
||||
`Expected balanced braces for export ${name}.`,
|
||||
);
|
||||
return auditsSource.slice(openBraceIndex, end + 1);
|
||||
}
|
||||
|
||||
function extractFieldSection(source: string, fieldName: string, nextFieldName: string) {
|
||||
const match = source.match(
|
||||
new RegExp(
|
||||
`${fieldName}:\\s*v\\.optional\\([\\s\\S]*?(?=\\s*${nextFieldName}:)`,
|
||||
),
|
||||
);
|
||||
|
||||
assert.notEqual(
|
||||
match,
|
||||
null,
|
||||
`Expected ${fieldName} field with expected object structure in schema.`,
|
||||
);
|
||||
|
||||
return match![0];
|
||||
}
|
||||
|
||||
function hasPattern(source: string, pattern: RegExp, message: string) {
|
||||
assert.equal(pattern.test(source), true, message);
|
||||
}
|
||||
|
||||
test("audits schema stores compact usedSkills metadata", () => {
|
||||
const { objectBlock } = extractTableSection("audits");
|
||||
const usedSkillsSection = extractFieldSection(
|
||||
objectBlock,
|
||||
"usedSkills",
|
||||
"skillSummaries",
|
||||
);
|
||||
const skillSummariesSection = extractFieldSection(
|
||||
objectBlock,
|
||||
"skillSummaries",
|
||||
"multimodalSummary",
|
||||
);
|
||||
|
||||
hasPattern(usedSkillsSection, /usedSkills:\s*v\.optional\(/, "usedSkills should be optional.");
|
||||
hasPattern(
|
||||
usedSkillsSection,
|
||||
/name:\s*v\.string\(\)/,
|
||||
"usedSkills.name should be string.",
|
||||
);
|
||||
hasPattern(
|
||||
usedSkillsSection,
|
||||
/category:\s*v\.string\(\)/,
|
||||
"usedSkills.category should be string.",
|
||||
);
|
||||
hasPattern(
|
||||
usedSkillsSection,
|
||||
/version:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
|
||||
"usedSkills.version should be optional string.",
|
||||
);
|
||||
hasPattern(
|
||||
usedSkillsSection,
|
||||
/source:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
|
||||
"usedSkills.source should be optional string.",
|
||||
);
|
||||
hasPattern(
|
||||
usedSkillsSection,
|
||||
/v\.array\(/,
|
||||
"usedSkills should be an optional array of objects.",
|
||||
);
|
||||
hasPattern(
|
||||
usedSkillsSection,
|
||||
/v\.object\(/,
|
||||
"usedSkills should be defined with v.object fields.",
|
||||
);
|
||||
|
||||
hasPattern(skillSummariesSection, /skillSummaries:/, "skillSummaries should still exist.");
|
||||
hasPattern(
|
||||
skillSummariesSection,
|
||||
/name:\s*v\.string\(\)/,
|
||||
"skillSummaries.name should stay string.",
|
||||
);
|
||||
hasPattern(
|
||||
skillSummariesSection,
|
||||
/purpose:\s*v\.string\(\)/,
|
||||
"skillSummaries.purpose should stay string.",
|
||||
);
|
||||
hasPattern(
|
||||
skillSummariesSection,
|
||||
/summary:\s*v\.string\(\)/,
|
||||
"skillSummaries.summary should stay string.",
|
||||
);
|
||||
});
|
||||
|
||||
test("audits.create accepts usedSkills validator and persists metadata payloads", () => {
|
||||
const createSource = extractExportSource("create");
|
||||
|
||||
hasPattern(
|
||||
auditsSource,
|
||||
/const usedSkillsValidator\s*=\s*v\.array\(/,
|
||||
"audits.ts should define a reusable usedSkillsValidator.",
|
||||
);
|
||||
hasPattern(
|
||||
auditsSource,
|
||||
/v\.object\([\s\S]*?name:\s*v\.string\(\)[\s\S]*?category:\s*v\.string\(\)[\s\S]*?version:\s*v\.optional\(\s*v\.string\(\)\s*\)[\s\S]*?source:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
|
||||
"audits.ts should define a reusable usedSkillsValidator.",
|
||||
);
|
||||
|
||||
hasPattern(
|
||||
createSource,
|
||||
/usedSkills:\s*v\.optional\(usedSkillsValidator\)/,
|
||||
"create args should include optional usedSkills field.",
|
||||
);
|
||||
hasPattern(
|
||||
createSource,
|
||||
/ctx\.db\.insert\(\s*["']audits["'][\s\S]*?args[\s\S]*\}/,
|
||||
"create should persist audit payload from args (so usedSkills is stored when provided).",
|
||||
);
|
||||
});
|
||||
|
||||
test("audits.getDetail returns audit + lead context with null-safe lead lookup", () => {
|
||||
const getDetailSource = extractExportSource("getDetail");
|
||||
|
||||
hasPattern(
|
||||
getDetailSource,
|
||||
/args:\s*{[\s\S]*id:\s*v\.id\(["']audits["']\)[\s\S]*}/,
|
||||
"getDetail should require id argument for audits.",
|
||||
);
|
||||
hasPattern(
|
||||
getDetailSource,
|
||||
/const\s+audit\s*=\s*await\s+ctx\.db\.get\s*\(\s*args\.id\s*\)/,
|
||||
"getDetail should load audit by id.",
|
||||
);
|
||||
hasPattern(
|
||||
getDetailSource,
|
||||
/if\s*\(\s*!audit\s*\)\s*{\s*return null;\s*}/,
|
||||
"getDetail should return null when audit is missing.",
|
||||
);
|
||||
hasPattern(
|
||||
getDetailSource,
|
||||
/const\s+lead\s*=\s*await\s+ctx\.db\.get\s*\(\s*audit\.leadId\s*\)/,
|
||||
"getDetail should load lead by leadId from the audit.",
|
||||
);
|
||||
hasPattern(
|
||||
getDetailSource,
|
||||
/return\s*{\s*audit,\s*lead\s*}/,
|
||||
"getDetail should return { audit, lead }.",
|
||||
);
|
||||
hasPattern(
|
||||
sourceFile.getFullText(),
|
||||
/export const getDetail = query\(/,
|
||||
"audits.ts should export a getDetail query.",
|
||||
);
|
||||
});
|
||||
162
tests/audit-skills-ui.test.ts
Normal file
162
tests/audit-skills-ui.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const source = async (relativePath: string) => {
|
||||
return await readFile(
|
||||
join(process.cwd(), ...relativePath.split("/")),
|
||||
"utf8",
|
||||
);
|
||||
};
|
||||
|
||||
test("audits dashboard page uses a dedicated board component", async () => {
|
||||
const dashboardPageSource = await source("app/dashboard/audits/page.tsx");
|
||||
|
||||
assert.doesNotMatch(
|
||||
dashboardPageSource,
|
||||
/DashboardPlaceholderPage/i,
|
||||
"Dashboard audits route should not render the placeholder page.",
|
||||
);
|
||||
assert.match(
|
||||
dashboardPageSource,
|
||||
/<AuditsBoard \/>/,
|
||||
"Audits board should be mounted from route page.",
|
||||
);
|
||||
assert.match(
|
||||
dashboardPageSource,
|
||||
/"@\/components\/audits\/audits-board"/,
|
||||
"Audits board should be imported from components.",
|
||||
);
|
||||
});
|
||||
|
||||
test("audits board renders compact list with convex list query and core columns", async () => {
|
||||
const boardSource = await source("components/audits/audits-board.tsx");
|
||||
|
||||
assert.match(
|
||||
boardSource,
|
||||
/\"use client\"/,
|
||||
"AuditsBoard must be a Client Component for useQuery.",
|
||||
);
|
||||
assert.match(
|
||||
boardSource,
|
||||
/useQuery\s*\(\s*api\.audits\.list,\s*\{\s*limit:\s*100\s*\}\s*\)/,
|
||||
"AuditsBoard should call api.audits.list with { limit: 100 }.",
|
||||
);
|
||||
assert.match(
|
||||
boardSource,
|
||||
/sort\(\(\s*a,\s*b\s*\)\s*=>\s*b\.createdAt\s*-\s*a\.createdAt\)/,
|
||||
"Audits should be sorted newest first.",
|
||||
);
|
||||
assert.match(boardSource, /Loading|lädt|Lade/i);
|
||||
assert.match(boardSource, /Keine Audits|keine Audits/i);
|
||||
assert.match(boardSource, /Slug/);
|
||||
assert.match(boardSource, /Domain/);
|
||||
assert.match(boardSource, /Status/);
|
||||
assert.match(boardSource, /Seiten/);
|
||||
assert.match(
|
||||
boardSource,
|
||||
/href=\{`\/dashboard\/audits\/\$\{audit\._id\}`\}/,
|
||||
"Each audit row should link to /dashboard/audits/{id}.",
|
||||
);
|
||||
});
|
||||
|
||||
test("audit detail component uses getDetail query and renders skills overview section", async () => {
|
||||
const detailSource = await source("components/audits/audit-detail.tsx");
|
||||
|
||||
assert.match(
|
||||
detailSource,
|
||||
/\"use client\"/,
|
||||
"AuditDetail must be client-side for Convex query calls.",
|
||||
);
|
||||
assert.match(
|
||||
detailSource,
|
||||
/api\.audits[\s\S]{0,80}getDetail/,
|
||||
"AuditDetail should use api.audits.getDetail query.",
|
||||
);
|
||||
assert.match(
|
||||
detailSource,
|
||||
/useQuery\(\s*api\.audits\.getDetail,\s*\{/,
|
||||
"AuditDetail should call useQuery with api.audits.getDetail directly.",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
detailSource,
|
||||
/const\s+auditDetailQueryRef/,
|
||||
"AuditDetail should not use a cast-based query fallback variable.",
|
||||
);
|
||||
assert.match(
|
||||
detailSource,
|
||||
/const\s+audit\s*=\s*result\?\.audit;/,
|
||||
"AuditDetail should destructure audit from result.audit.",
|
||||
);
|
||||
assert.match(
|
||||
detailSource,
|
||||
/const\s+lead\s*=\s*result\?\.lead;/,
|
||||
"AuditDetail should destructure lead from result.lead.",
|
||||
);
|
||||
assert.match(
|
||||
detailSource,
|
||||
/leadSummary\(\s*lead\s*\)/,
|
||||
"AuditDetail should pass lead into leadSummary from result.lead.",
|
||||
);
|
||||
assert.match(
|
||||
detailSource,
|
||||
/usedSkills/,
|
||||
"AuditDetail should inspect usedSkills for overview rendering.",
|
||||
);
|
||||
assert.match(
|
||||
detailSource,
|
||||
/Keine Skills gespeichert/,
|
||||
"AuditDetail should show fallback text when no skills are saved.",
|
||||
);
|
||||
assert.match(
|
||||
detailSource,
|
||||
/Verwendete Skills/,
|
||||
"AuditDetail should render Verwendete Skills heading.",
|
||||
);
|
||||
assert.match(
|
||||
detailSource,
|
||||
/Lead|lead/,
|
||||
"AuditDetail should surface lead context when available.",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
detailSource,
|
||||
/<p[^>]*>\s*\{leadSummary\(\s*lead\|[\s\S]*?\)\s*\}\s*<\/p>/,
|
||||
"Lead summary should not wrap leadSummary output in a nested <p>.",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
detailSource,
|
||||
/<p[^>]*>\s*\{leadSummary\(\s*audit\.lead\)\s*\}\s*<\/p>/,
|
||||
"Lead summary should not wrap leadSummary output in a nested <p>.",
|
||||
);
|
||||
});
|
||||
|
||||
test("audits detail route passes id to AuditDetail via Promise params", async () => {
|
||||
const pageSource = await source("app/dashboard/audits/[id]/page.tsx");
|
||||
|
||||
assert.match(
|
||||
pageSource,
|
||||
/params:\s*Promise<\{\s*id:\s*string\s*\}>/,
|
||||
"Audit detail route should accept params as Promise in Next.js 16 style.",
|
||||
);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/const \{\s*id\s*\}\s*=\s*await params/,
|
||||
"Audit detail route should unwrap Promise params.",
|
||||
);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/<AuditDetail\s+id=/,
|
||||
"Audit detail route should pass id prop into AuditDetail.",
|
||||
);
|
||||
});
|
||||
|
||||
test("public audit page does not expose used skills", async () => {
|
||||
const publicAuditSource = await source("app/audit/[slug]/page.tsx");
|
||||
|
||||
assert.doesNotMatch(
|
||||
publicAuditSource,
|
||||
/Verwendete Skills|usedSkills/i,
|
||||
"Public audit page must not show used skills.",
|
||||
);
|
||||
});
|
||||
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