mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
* skill: drop quality tiers, keep the real brand-craft guardrails Codex's craft/brand pass introduced fast/ship/showpiece "quality bars" plus brand-specific build gates, asset ledgers, sub-agent review, and self-graded fallback labels. In practice those tiers became escape hatches rather than craft pressure: the final output should always be 10/10, and the real decision points are splashiness and maximalism, not quality. Removed: - All quality-bar / showpiece / fast / ship framing in shape.md and craft.md - Standalone Brand Direction (#4) and Asset Requirements (#10) sections in shape's brief; renumbered back to 1-10 - The Brand hard rules section in brand.md (folded its real prohibitions into the existing Imagery and Brand bans sections) - Brand-specific build-gate item, mock-fidelity bullet, production-bar bullet, present-step bullet in craft.md - Asset ledger ceremony in craft Step 4 - Review-only sub-agents and "self-reviewed fallback, not independently validated" machinery in craft.md and polish.md - The For brand surfaces, assess hard failures subsection in polish.md and the brand checklist row - tests/brand-showpiece-reference.test.mjs (and its package.json wiring) Kept (the real nuggets): - Asset-substitution prohibition: image-led briefs ship real/generated assets or canvas/SVG/WebGL, not generic CSS panels, cards, bullets, or copy - Repeated tiny uppercase tracked kicker labels as a brand ban - Detector/QA output is defect evidence only, never proof of quality - "What visual assets are real content here?" discovery question - Inspect each major section individually for brand and long-form work - repeated-section-kickers detection rule + fixture - CLI improvements (JSON to stdout, -json/-fast aliases, severity field) - critique.md: npx impeccable detect --json fix Harness output dirs refreshed via bun run build. Full test suite (186) passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * skill: strip gate ceremony; require shape pause; allow compact briefs The setup gate table and IMPECCABLE_PREFLIGHT banner pushed every craft run through ritual restatement (PRODUCT.md → original prompt → round 1 → round 2 → 70-line "confirmed brief" → critique → summary, all saying the same thing). Replaced with imperative prose that still demands the same work but skips the user-facing telemetry. Specifically: SKILL.md - Drop the Setup gate table and IMPECCABLE_PREFLIGHT banner. - Keep the imperative steps explicitly: load context, identify register and load brand.md or product.md, AND load the matching command reference (craft.md / shape.md / etc.) when a sub-command is invoked. The command-reference step is non-negotiable; without craft.md loaded the agent skips the shape-and-confirm pause. craft.md - Drop the Build Gate / Craft Contract formal sections; replace with one paragraph stating prerequisites. - Step 1 explicitly requires ending the response after presenting the shape output; the user must confirm before any code lands. Allows a compact 3-5 bullet brief when the prompt + PRODUCT.md already pin direction (full 10-section structure reserved for genuinely ambiguous tasks). - Step 3 image gate skips silently when image generation isn't natively available; no user-facing announcement. - Step 6 explicitly legitimizes "first pass clean, shipping" as a valid endpoint and bans inventing fake defects to demonstrate iteration. shape.md - Cap discovery at 1 round by default; second round only when first leaves material gaps. - Adds an "assert-then-confirm, not menu-with-escape" rule: when PRODUCT.md and the prompt make one option obvious, name it and ask for confirm or override instead of enumerating "Restrained / Committed / Or something else?" as a real choice. - Phase 2 brief has two forms now: compact (default for clear briefs) and full structured (genuinely ambiguous). Open Questions can't double as leading-with-Recommend; if you'd write "Recommend: X", decide X. - Image gate same as craft.md. Validated end-to-end with a Haiku skill-on observability run: agent loads craft.md plus the brief's recommended implementation refs, pauses for one productive question (accent color, trace fidelity, CTA), and ships an artifact with zero side-tab violations vs. the original v1 baseline. Cost trades up modestly for that quality. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * craft.md Step 6: Reading the screenshot is the inspection, not taking it A v4 eval run took 4 targeted screenshots (hero, mobile, tablet, query-section) and then never Read any of them back. The agent treated browser_screenshot itself as "I inspected" and shipped without the multimodal feedback loop ever closing. Detector caught the resulting slop (5+ side-tab violations) on adjacent runs that did the same thing. Step 6 now spells out the pattern explicitly: take the screenshot, then Read the resulting PNG so its image content enters the conversation as multimodal input, then critique what you actually see in the image. With a check: "if your critique could have been written without looking at the image, you didn't look at the image." Validated with v5b: agent took 6 screenshots, Read all 6 back, and shipped with zero detector findings (vs the previous greenfield runs that hit 1-12 findings each). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * craft + brand: framework foundation, build-pipeline respect, image verification Three closely-linked additions surfaced by an eval-harness session investigating why the agent always shipped flat single-file HTML and zero imagery on greenfield brand briefs. 1. craft.md gains a new Step 0 "Project Foundation" before Shape. Detects existing framework / component library / icon set and uses what's there. Greenfield: ask the user via AskUserQuestion with sensible defaults framed by the brief (Astro for content/ brand sites, SvelteKit/Next/Nuxt for app surfaces, single index.html only for one-shot demos). Skipping the framework decision and writing flat HTML "to satisfy the spec" produces work that reads as a 2018 prototype regardless of visual quality. 2. craft.md Step 5 production bar gains two bullets: - Respect the build pipeline. Edit source files and run the project's `npm run build`; do not write to build/ / dist/ / .next/ directly with cat/heredoc/Bash redirects. Bypassing the pipeline skips asset hashing, image optimization, code splitting, and CSS extraction. - Verify external image URLs before referencing them. Use an image-search MCP, web-fetch tool, or browser if available; guessed photo IDs ship as broken-image placeholders. 3. brand.md "Imagery" section: - Generalizes the Unsplash URL guidance to "verify URLs before referencing them" with a hierarchy: image-search MCP > web-fetch > confidence-restricted manual selection > fewer photos. - Tightens the tech/dev-tool exception. Old line "zero imagery can be correct" gave models a permission slip. New framing keeps the underlying truth (typography + code + diagrams primarily carry voice) but raises the floor: imagery still earns its place when it serves the brief, and skipping it requires naming the typographic/diagrammatic move that's carrying the visual weight instead. "Zero imagery is the failure mode of laziness, not restraint." Eval-harness corpus that prompted this: 19/19 brand landing tasks shipped 0 images each, including ones where Opus had taste enough to break the dev-tool color default lane. The skill needs to teach both halves of the decision; the harness shouldn't have to nudge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * detector: body-text-viewport-edge rule + OKLCH/var-resolution + anchor-inherit FP fixes New rule: body-text-viewport-edge flags body paragraphs that render flush against the left/right viewport edges (no container padding). Tested via the new tests/fixtures/antipatterns/body-text-viewport-edge.html fixture (3 flag cases, 5 pass cases) and the test in detect-antipatterns-browser. False-positive class fixes — all jsdom-mode only (real browsers resolve the cascade correctly so these gates stay inert there). Five related gaps that compounded into ~14× spurious contrast findings on Tailwind v4 pages with OKLCH color tokens: • OKLCH parser. jsdom returns the literal "oklch(...)" string from getComputedStyle; the detector now converts to sRGB via Björn Ottosson's matrices. Handles Tailwind v4's compact minified form "oklch(21.5%.02 50)" (no space after %). • var() resolution. resolveBackground + checkElementColors now accept the existing customPropMap and parse `var(--color-paper)` etc. as proper RGB via the new parseColorResolved helper. • bg-color before bg-image. The old order bailed on any gradient ancestor before checking for a solid background-color underneath, causing the body's decorative paper-grain gradient to be measured against instead of the page's actual `bg-paper` cream. • body/html-level gradient → white fallback. When the only opaque ancestor we can read is body/html with a gradient overlay (and jsdom can't decompose `background: var(--paper) gradient` to extract the solid color), return white instead of falling through to resolveGradientStops — which was picking up paper-grain noise colors and using them as the bg. • Anchor-inherit workaround for jsdom :link UA specificity. Tailwind v4's preflight declares `a { color: inherit }` (0,0,1). jsdom's UA stylesheet has `:link { color: blue }` at (0,1,1) and wins the cascade. Real Chrome wraps :link in :where() (0,0,0) so the page rule wins. When the page declares the inherit rule AND we see jsdom's default `rgb(0,0,238)` on an anchor, walk to the nearest non-anchor ancestor and use its color. • Alpha-fallback safety gate. When text has alpha<1 AND we couldn't find an opaque ancestor (effectiveBg null), skip the contrast finding. Covers any remaining FP class the deeper fixes miss. Verified end-to-end against an Opus iter-1 artifact on Tailwind v4 with 14 cream/cream FPs + 2 blue-link UA FPs before; 0 findings after, while the color.html fixture's 12 real low-contrast cases continue to flag (verified via direct detectHtml calls). cli/engine/detect-antipatterns-browser.js is the generated browser distribution — regenerated from .mjs via scripts/build-browser-detector.js (no manual edits to the generated file). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * craft.md: tighten verbose passages, de-codex Step 6, cut redundancies Cumulative reduction: 218 → 155 lines (-29%). Step 0: drop the "Why this matters" paragraph at the end. The body of Step 0 already makes the framework-pick point; the paragraph just re-explains it with extra rhetoric. Step 1: replace the 4-sentence "you must end your response" block with a single line. The original said the same thing three different ways. Step 3: trim the conditional / defensive scaffolding (Purpose subsection, "do not skip because the eventual UI is semantic..." paragraph, duplicated approval-loop guidance). Mock fidelity inventory preserved. Step 4: drop the "keep UI text semantic" sentence; it duplicates Step 5's "Semantic first" rule. The rasterized-vs-semantic decision rule stays. Step 5: tighten each production-bar bullet to bold-lead + specifics format. All 15 rules preserved (real content, mock ingredients, semantic first, spacing/alignment, typography, state coverage, interaction quality, icon set, build pipeline, image URL verification, optimized imagery, premium motion, maintainability, technical cleanliness, ask-when-uncertain). Step 6: rewrite around "look at what you built like a designer would — your eyes are whatever the harness gives you." Drops Codex-specific "In Codex, use browser-use" bias. Drops the verbose 3-step Read pattern (condensed to one sentence). Drops the 1-8 numbered checklist (replaced by a tight paragraph). Keeps the load-bearing rules: read the PNG, don't fabricate iteration, mock fidelity reference, exit bar = studio defensibility. Step 7: drop the closing "Iterate based on feedback. Good design is rarely right on the first pass" preachy filler. All em-dashes converted to semicolons / colons / periods to satisfy the skill prose validator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build: native subagent pipeline + Codex-only asset producer Adds an agent cross-compile pipeline alongside the existing skill pipeline. Sources live at skill/agents/*.md; providers that declare agentFormat (codex-toml, claude-md) emit native subagent files. An optional providers: <list> field on an agent gates which harnesses get a copy; default (no field) ships everywhere. The impeccable-asset-producer agent is opt-in to Codex only. It's useful for Codex's native image generation path and is untested elsewhere; Claude has no native image gen anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * brand: inverse-test + cultural-symbol palette guardrail Two additions to the brand register reference: - Inverse slop test: describe the page the way a competitor would describe theirs. If that sentence fits the modal landing page in the category, restart. - Palette guardrail: when a cultural-symbol palette is the obvious pull, reach past it. Let cultural reading come from typography, imagery, and copy. Harness mirrors regenerated; some also catch up to the image- verification paragraph frome3ad2efthat hadn't been re-synced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PRODUCT.md: widen audience beyond developers Designers, product managers, and engineers all use AI coding tools and want better design output. Keeping the audience narrow to "frontend and full-stack developers" understates who the skill is actually for. Also retitles "developer" to "user/builder" in the purpose statement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * site + build: bump rule count to 29, strip changelog from detector check Two changes: - site/pages/index.astro: three live mentions of "28 rules / checks" bumped to 29 after the body-text-viewport-edge rule landed inb9bf496. - scripts/build.js: the detection-count validator was reading the unstripped content, so historical counts inside changelog entries (e.g. "28 rules" from an older release note) were flagging against the current detector total. The command-count check already strips the changelog ul; the detection check now does the same. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: align hero-eyebrow-chip fixture with relaxed rule gatesb9bf496intentionally relaxed two gates in checkHeroEyebrow: - removed the heading-size ≥ 48px anchor (modern hero h1s use clamp/vw/var that jsdom can't resolve) - raised the eyebrow text ceiling from 30 to 60 chars Two fixture cases that satisfied the negative side of the old gates now match the rule: - "Body-Sized Heading Below Eyebrow" — 24px h1 with tracked-caps label above. Per the rule's stated intent ("a tiny tan label directly above any h1 is the antipattern regardless of how big the h1 ends up"), this is a flag. - "Long Uppercase Sentence Above Hero" — 46-char tracked-caps label is under the new 60-char ceiling, so still eyebrow-shaped. Both cases moved from the should-pass column to should-flag, with case descriptions rewritten to explain the gate they exercise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
325 lines
13 KiB
JavaScript
325 lines
13 KiB
JavaScript
import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import * as utils from '../scripts/lib/utils.js';
|
|
import * as transformers from '../scripts/lib/transformers/index.js';
|
|
|
|
const TEST_DIR = path.join(process.cwd(), 'test-tmp-build');
|
|
|
|
describe('build orchestration', () => {
|
|
beforeEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should call readSourceFiles with root directory', () => {
|
|
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
|
skills: []
|
|
});
|
|
|
|
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
|
|
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
|
|
const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {});
|
|
const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {});
|
|
|
|
// Simulate the build process
|
|
const ROOT_DIR = TEST_DIR;
|
|
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
|
|
|
const { skills } = utils.readSourceFiles(ROOT_DIR);
|
|
const patterns = utils.readPatterns(ROOT_DIR);
|
|
transformers.transformCursor(skills, DIST_DIR, patterns);
|
|
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
|
transformers.transformGemini(skills, DIST_DIR, patterns);
|
|
transformers.transformCodex(skills, DIST_DIR, patterns);
|
|
|
|
expect(readSourceFilesSpy).toHaveBeenCalledWith(ROOT_DIR);
|
|
|
|
readSourceFilesSpy.mockRestore();
|
|
transformCursorSpy.mockRestore();
|
|
transformClaudeCodeSpy.mockRestore();
|
|
transformGeminiSpy.mockRestore();
|
|
transformCodexSpy.mockRestore();
|
|
});
|
|
|
|
test('should call all transformers with correct arguments', () => {
|
|
const skills = [
|
|
{ name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Skill body 1' }
|
|
];
|
|
const patterns = { patterns: [], antipatterns: [] };
|
|
|
|
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
|
skills
|
|
});
|
|
const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue(patterns);
|
|
|
|
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
|
|
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
|
|
const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {});
|
|
const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {});
|
|
|
|
const ROOT_DIR = TEST_DIR;
|
|
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
|
|
|
const sourceFiles = utils.readSourceFiles(ROOT_DIR);
|
|
const patternData = utils.readPatterns(ROOT_DIR);
|
|
transformers.transformCursor(sourceFiles.skills, DIST_DIR, patternData);
|
|
transformers.transformClaudeCode(sourceFiles.skills, DIST_DIR, patternData);
|
|
transformers.transformGemini(sourceFiles.skills, DIST_DIR, patternData);
|
|
transformers.transformCodex(sourceFiles.skills, DIST_DIR, patternData);
|
|
|
|
expect(transformCursorSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
|
|
expect(transformClaudeCodeSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
|
|
expect(transformGeminiSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
|
|
expect(transformCodexSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
|
|
|
|
readSourceFilesSpy.mockRestore();
|
|
readPatternsSpy.mockRestore();
|
|
transformCursorSpy.mockRestore();
|
|
transformClaudeCodeSpy.mockRestore();
|
|
transformGeminiSpy.mockRestore();
|
|
transformCodexSpy.mockRestore();
|
|
});
|
|
|
|
test('should handle empty source files', () => {
|
|
const patterns = { patterns: [], antipatterns: [] };
|
|
|
|
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
|
skills: []
|
|
});
|
|
const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue(patterns);
|
|
|
|
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
|
|
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
|
|
const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {});
|
|
const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {});
|
|
|
|
const ROOT_DIR = TEST_DIR;
|
|
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
|
|
|
const { skills } = utils.readSourceFiles(ROOT_DIR);
|
|
const patternData = utils.readPatterns(ROOT_DIR);
|
|
transformers.transformCursor(skills, DIST_DIR, patternData);
|
|
transformers.transformClaudeCode(skills, DIST_DIR, patternData);
|
|
transformers.transformGemini(skills, DIST_DIR, patternData);
|
|
transformers.transformCodex(skills, DIST_DIR, patternData);
|
|
|
|
expect(transformCursorSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
|
|
expect(transformClaudeCodeSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
|
|
expect(transformGeminiSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
|
|
expect(transformCodexSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
|
|
|
|
readSourceFilesSpy.mockRestore();
|
|
readPatternsSpy.mockRestore();
|
|
transformCursorSpy.mockRestore();
|
|
transformClaudeCodeSpy.mockRestore();
|
|
transformGeminiSpy.mockRestore();
|
|
transformCodexSpy.mockRestore();
|
|
});
|
|
|
|
test('integration: full build creates all expected outputs', () => {
|
|
// Create test source files
|
|
const skillContent = `---
|
|
name: test-skill
|
|
description: A test skill
|
|
license: MIT
|
|
---
|
|
|
|
This is a test skill body.`;
|
|
|
|
const skillDir = path.join(TEST_DIR, 'skill');
|
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
|
|
// Run the build process
|
|
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
|
const { skills } = utils.readSourceFiles(TEST_DIR);
|
|
const patterns = utils.readPatterns(TEST_DIR);
|
|
|
|
transformers.transformCursor(skills, DIST_DIR, patterns);
|
|
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
|
transformers.transformGemini(skills, DIST_DIR, patterns);
|
|
transformers.transformCodex(skills, DIST_DIR, patterns);
|
|
|
|
// Verify Cursor outputs
|
|
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/.cursor/skills/test-skill/SKILL.md'))).toBe(true);
|
|
|
|
// Verify Claude Code outputs
|
|
expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/.claude/skills/test-skill/SKILL.md'))).toBe(true);
|
|
|
|
// Verify Gemini outputs
|
|
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/.gemini/skills/test-skill/SKILL.md'))).toBe(true);
|
|
|
|
// Verify Codex outputs
|
|
expect(fs.existsSync(path.join(DIST_DIR, 'codex/.codex/skills/test-skill/SKILL.md'))).toBe(true);
|
|
});
|
|
|
|
test('integration: emits native subagent files for Codex and Claude Code', () => {
|
|
const skillContent = `---
|
|
name: test-skill
|
|
description: A test skill
|
|
---
|
|
|
|
This is a test skill body.`;
|
|
|
|
const agentContent = `---
|
|
name: asset-producer
|
|
codex-name: asset_producer
|
|
description: Produces assets from approved crops
|
|
tools: Read, Write
|
|
model: inherit
|
|
effort: medium
|
|
max-turns: 8
|
|
nickname-candidates:
|
|
- Asset Plate
|
|
---
|
|
|
|
Do not redesign the approved crop.`;
|
|
|
|
const skillDir = path.join(TEST_DIR, 'skill');
|
|
fs.mkdirSync(path.join(skillDir, 'agents'), { recursive: true });
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
fs.writeFileSync(path.join(skillDir, 'agents/asset-producer.md'), agentContent);
|
|
|
|
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
|
const { skills } = utils.readSourceFiles(TEST_DIR);
|
|
const patterns = utils.readPatterns(TEST_DIR);
|
|
|
|
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
|
transformers.transformCodex(skills, DIST_DIR, patterns);
|
|
|
|
const claudeAgentPath = path.join(DIST_DIR, 'claude-code/.claude/agents/asset-producer.md');
|
|
const codexAgentPath = path.join(DIST_DIR, 'codex/.codex/agents/asset_producer.toml');
|
|
|
|
expect(fs.existsSync(claudeAgentPath)).toBe(true);
|
|
expect(fs.existsSync(codexAgentPath)).toBe(true);
|
|
|
|
const claudeAgent = fs.readFileSync(claudeAgentPath, 'utf-8');
|
|
expect(claudeAgent).toContain('name: asset-producer');
|
|
expect(claudeAgent).toContain('tools: Read, Write');
|
|
expect(claudeAgent).toContain('maxTurns: 8');
|
|
|
|
const codexAgent = fs.readFileSync(codexAgentPath, 'utf-8');
|
|
expect(codexAgent).toContain('name = "asset_producer"');
|
|
expect(codexAgent).toContain('model_reasoning_effort = "medium"');
|
|
expect(codexAgent).toContain('nickname_candidates = ["Asset Plate"]');
|
|
expect(codexAgent).toContain('developer_instructions =');
|
|
});
|
|
|
|
test('integration: verify transformations are correct', () => {
|
|
const skillContent = `---
|
|
name: audit
|
|
description: Run technical quality checks
|
|
user-invocable: true
|
|
argument-hint: "[TARGET=<value>]"
|
|
---
|
|
|
|
Please audit {{target}} for technical quality. Ask {{model}} for help.`;
|
|
|
|
const skillDir = path.join(TEST_DIR, 'skill');
|
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
|
|
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
|
const { skills } = utils.readSourceFiles(TEST_DIR);
|
|
const patterns = utils.readPatterns(TEST_DIR);
|
|
|
|
transformers.transformCursor(skills, DIST_DIR, patterns);
|
|
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
|
transformers.transformGemini(skills, DIST_DIR, patterns);
|
|
transformers.transformCodex(skills, DIST_DIR, patterns);
|
|
|
|
// Verify Cursor: full frontmatter with user-invocable
|
|
const cursorContent = fs.readFileSync(path.join(DIST_DIR, 'cursor/.cursor/skills/audit/SKILL.md'), 'utf-8');
|
|
expect(cursorContent).toContain('---');
|
|
expect(cursorContent).toContain('name: audit');
|
|
expect(cursorContent).toContain('{{target}}');
|
|
expect(cursorContent).toContain('the model');
|
|
|
|
// Verify Claude Code: full frontmatter with user-invocable and argument-hint
|
|
const claudeContent = fs.readFileSync(path.join(DIST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8');
|
|
expect(claudeContent).toContain('---');
|
|
expect(claudeContent).toContain('name: audit');
|
|
expect(claudeContent).toContain('user-invocable: true');
|
|
expect(claudeContent).toContain('{{target}}');
|
|
expect(claudeContent).toContain('Claude');
|
|
|
|
// Verify Gemini: skill in skills directory
|
|
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'))).toBe(true);
|
|
const geminiContent = fs.readFileSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'), 'utf-8');
|
|
expect(geminiContent).toContain('{{target}}'); // No body transform, placeholder preserved
|
|
expect(geminiContent).toContain('Gemini');
|
|
|
|
// Verify Codex: skill in skills directory
|
|
expect(fs.existsSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'))).toBe(true);
|
|
const codexContent = fs.readFileSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'), 'utf-8');
|
|
expect(codexContent).toContain('{{target}}'); // No body transform, placeholder preserved
|
|
expect(codexContent).toContain('GPT');
|
|
});
|
|
|
|
test('should call transformers in correct order', () => {
|
|
const callOrder = [];
|
|
|
|
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
|
skills: []
|
|
});
|
|
const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue({ patterns: [], antipatterns: [] });
|
|
|
|
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {
|
|
callOrder.push('cursor');
|
|
});
|
|
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {
|
|
callOrder.push('claude-code');
|
|
});
|
|
const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {
|
|
callOrder.push('gemini');
|
|
});
|
|
const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {
|
|
callOrder.push('codex');
|
|
});
|
|
|
|
const ROOT_DIR = TEST_DIR;
|
|
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
|
|
|
const { skills } = utils.readSourceFiles(ROOT_DIR);
|
|
const patterns = utils.readPatterns(ROOT_DIR);
|
|
transformers.transformCursor(skills, DIST_DIR, patterns);
|
|
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
|
transformers.transformGemini(skills, DIST_DIR, patterns);
|
|
transformers.transformCodex(skills, DIST_DIR, patterns);
|
|
|
|
expect(callOrder).toEqual(['cursor', 'claude-code', 'gemini', 'codex']);
|
|
|
|
readSourceFilesSpy.mockRestore();
|
|
readPatternsSpy.mockRestore();
|
|
transformCursorSpy.mockRestore();
|
|
transformClaudeCodeSpy.mockRestore();
|
|
transformGeminiSpy.mockRestore();
|
|
transformCodexSpy.mockRestore();
|
|
});
|
|
|
|
test('should include agents and kiro transformers', () => {
|
|
const { skills } = utils.readSourceFiles(TEST_DIR);
|
|
const patterns = utils.readPatterns(TEST_DIR);
|
|
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
|
|
|
// These should not throw
|
|
transformers.transformAgents(skills, DIST_DIR, patterns);
|
|
transformers.transformGitHub(skills, DIST_DIR, patterns);
|
|
transformers.transformKiro(skills, DIST_DIR, patterns);
|
|
|
|
// Verify outputs
|
|
expect(fs.existsSync(path.join(DIST_DIR, 'agents/.agents/skills'))).toBe(true);
|
|
expect(fs.existsSync(path.join(DIST_DIR, 'github/.github/skills'))).toBe(true);
|
|
expect(fs.existsSync(path.join(DIST_DIR, 'kiro/.kiro/skills'))).toBe(true);
|
|
});
|
|
});
|