Files
open-design/apps/daemon/tests/skill-id-aliases.test.ts
aefba56a3f feat(skills): open-design-landing rename, kami skills, landing OG (#428)
* feat(skills): open-design-landing rename, kami skills, landing OG

- Rename editorial-collage skills to open-design-landing and -deck; refresh examples and compose script layout
- Add kami-deck and kami-landing skills with HTML examples
- Landing page: og.astro, index wiring, and style tweaks; package.json bump
- Web i18n: German and Russian copy for renamed and new skills
- Daemon test: update skill-asset-rewrite expectations for new paths
- Design systems: README and atelier-zero doc touch-ups
- Cross-skill SKILL.md reference updates

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(landing-page): document version-slot invariant and deprecation timeline

Address P3 review notes on PR #428:
- Note the `data-github-version` wrapper invariant (version string only)
  near the canonical URL block in `app/page.tsx`.
- Expand the `formatVersion` helper comment in `app/pages/index.astro`
  with concrete `release.name` / `tag_name` example shapes for each
  branch of the regex fallback.
- Tighten the `EditorialCollageDeckInputs` deprecation in
  `skills/open-design-landing-deck/schema.ts` to a specific removal
  version (v0.4.0) and add a "Migrating from editorial-collage-deck"
  section to the skill README.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* docs(landing-page, skills): clarify version slot script and rename migrations

- Describe GitHub version slots as driven by the inline enhancement script,
  not React hydration.
- Add editorial-collage → open-design-landing migration notes; fix README
  link copy (Astro static landing app).
- Extend deck README migration table with shared asset path renames.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(daemon): alias deprecated editorial-collage skill ids

The PR renames the editorial-collage / editorial-collage-deck skills
to open-design-landing / open-design-landing-deck, but the daemon
persists exact skill_id strings on projects and resolves them via
listSkills().find((s) => s.id === storedId). After the rename, any
project saved against an old id silently composes without the intended
skill prompt because the listing no longer exposes that id.

Add a SKILL_ID_ALIASES map in skills.ts plus a findSkillById() helper
that rewrites deprecated ids to their current canonical form, then
route every server-side lookup (skill detail, example HTML, asset
proxy, system-prompt composer) through it. Cover the alias map, the
resolver, and end-to-end resolution against a temp skills directory
with a regression test.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(kami-deck): route host od:slide messages through local go()

The host bridge classifies kami-deck as class-driven because go() toggles
.slide.active, but the visible slide is moved by deck.style.transform
which the bridge cannot drive. Listen for od:slide messages and dispatch
them through the local go() so toolbar next/prev and initialSlideIndex
restore actually shift the deck.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(kami-deck): sync deck transform with host-driven .active changes

The previous fix added a local od:slide listener but the host bridge in
apps/web/src/runtime/srcdoc.ts also listens for the same message and
calls setActive() (toggles .slide.active) without driving the deck
transform. Both listeners fired, the bridge re-read the just-toggled
active class, and overshot by one — and the bridge's restoreInitialSlide
path could move .active without a message at all, leaving the deck on
the original transform.

Stop the bridge from double-handling by calling stopImmediatePropagation
in the local listener (registered first because the bridge script is
appended to </body>), and add a MutationObserver that pulls the deck
transform onto whichever slide currently carries .active so the bridge's
direct setActive calls (notably the initial-slide restore) move the deck
too.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(i18n): align French content with renamed/new skills

PR #434 (French localization) merged into main with French copy for the
old editorial-collage / editorial-collage-deck skill ids; this branch
renamed those to open-design-landing / open-design-landing-deck and
added kami-deck and kami-landing. Update content.fr.ts to track the
rename and add French copy for the new kami skills so the
LOCALIZED_CONTENT_IDS coverage test passes once main is merged.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(open-design-landing-deck): sync deck transform with host-driven .active changes

Apply the same fix that landed in skills/kami-deck/example.html
(commits 96b255b, 8cbca30) to the open-design-landing-deck composer
runtime: the host bridge classifies this deck as class-driven because
go() toggles .slide.active, but the visible slide is moved by
deck.style.transform which the bridge can't drive. Add an od:slide
message listener that calls stopImmediatePropagation() and routes nav
through the local go(), plus a MutationObserver that pulls the deck
transform onto whichever slide carries .active so the bridge's direct
setActive calls (notably restoreInitialSlide) move the deck too.

Regenerates example.html via scripts/compose.ts; the regeneration also
picks up upstream nav-cta and brand-meta CSS additions in the sister
open-design-landing styles.css that the example had drifted from.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* docs(open-design-landing): align deploy story with Astro landing app

- Update SKILL contract: apps/landing-page is Astro static; clarify
  nextjs-app output_format as a historical enum label and <out>/nextjs
  as a legacy folder name.
- Replace optional-deploy section with fork + pnpm --filter landing-page build.
- Fix styles.css header and regenerate landing + deck example.html so
  inlined comments match.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(deck-runtime): bypass interaction lock for host/observer slide sync

The slide deck runtimes for kami-deck and open-design-landing-deck
gate go() behind a 700ms `lock` so wheel/key/touch input bursts can't
overshoot the transform transition. But applying the same gate to the
host bridge's od:slide messages and the MutationObserver watching
`.slide.active` creates a startup race: go(0) at the end of init sets
lock=true, and any host-driven `.active` change inside that window
(notably restoreInitialSlide) fires the observer, which calls go(i),
which exits at the lock guard — leaving the visible deck on slide 1
while the host counter advances to N.

Split the actual state update into an unthrottled `applySlide(n)`
helper that updates transform, `.active`, dot nav, and the progress
bar. Keep `lock` only on the user-input path through `go()`. Route
the message listener, the MutationObserver, and the initial render
through `applySlide` directly so host-driven sync always reaches the
deck transform regardless of the throttle state.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 19:22:46 +08:00

120 lines
4.5 KiB
TypeScript

// @ts-nocheck
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import {
SKILL_ID_ALIASES,
findSkillById,
listSkills,
resolveSkillId,
} from '../src/skills.js';
// Regression coverage for the editorial-collage → open-design-landing rename.
// The daemon persists the chosen skill_id verbatim on a project row and
// resolves it later by id, so a folder/frontmatter rename without a
// compatibility shim would silently drop the skill prompt for projects
// saved against the old id. These tests pin the alias map and the lookup
// helper that every server-side resolver must go through.
let skillsRoot;
beforeAll(async () => {
skillsRoot = await mkdtemp(path.join(tmpdir(), 'od-skills-aliases-'));
// Mimic the on-disk shape the production registry expects: one
// directory per skill, each with a SKILL.md whose frontmatter `name`
// becomes the canonical id returned by listSkills().
await mkdir(path.join(skillsRoot, 'open-design-landing'), { recursive: true });
await writeFile(
path.join(skillsRoot, 'open-design-landing', 'SKILL.md'),
'---\nname: open-design-landing\ndescription: Atelier Zero landing.\n---\n\nbody\n',
'utf8',
);
await mkdir(path.join(skillsRoot, 'open-design-landing-deck'), {
recursive: true,
});
await writeFile(
path.join(skillsRoot, 'open-design-landing-deck', 'SKILL.md'),
'---\nname: open-design-landing-deck\ndescription: Atelier Zero deck.\n---\n\nbody\n',
'utf8',
);
// An untouched skill so we can prove the helper still resolves
// non-aliased ids and does not match by accident.
await mkdir(path.join(skillsRoot, 'simple-deck'), { recursive: true });
await writeFile(
path.join(skillsRoot, 'simple-deck', 'SKILL.md'),
'---\nname: simple-deck\ndescription: Plain deck.\n---\n\nbody\n',
'utf8',
);
});
afterAll(async () => {
if (skillsRoot) await rm(skillsRoot, { recursive: true, force: true });
});
describe('SKILL_ID_ALIASES', () => {
it('maps the editorial-collage rename to its current canonical id', () => {
expect(SKILL_ID_ALIASES['editorial-collage']).toBe('open-design-landing');
expect(SKILL_ID_ALIASES['editorial-collage-deck']).toBe(
'open-design-landing-deck',
);
});
it('is frozen so callers cannot mutate the deprecation list at runtime', () => {
expect(Object.isFrozen(SKILL_ID_ALIASES)).toBe(true);
});
});
describe('resolveSkillId', () => {
it('forwards deprecated ids to their canonical replacement', () => {
expect(resolveSkillId('editorial-collage')).toBe('open-design-landing');
expect(resolveSkillId('editorial-collage-deck')).toBe(
'open-design-landing-deck',
);
});
it('passes non-aliased ids through unchanged', () => {
expect(resolveSkillId('simple-deck')).toBe('simple-deck');
expect(resolveSkillId('totally-unknown')).toBe('totally-unknown');
});
it('returns the input unchanged for empty / non-string ids', () => {
expect(resolveSkillId('')).toBe('');
expect(resolveSkillId(undefined)).toBeUndefined();
expect(resolveSkillId(null)).toBeNull();
});
});
describe('findSkillById', () => {
it('resolves a project saved with the old editorial-collage id to the renamed skill', async () => {
const skills = await listSkills(skillsRoot);
const skill = findSkillById(skills, 'editorial-collage');
expect(skill).toBeDefined();
expect(skill.id).toBe('open-design-landing');
expect(skill.body).toContain('body');
});
it('resolves a project saved with the old editorial-collage-deck id to the renamed deck skill', async () => {
const skills = await listSkills(skillsRoot);
const skill = findSkillById(skills, 'editorial-collage-deck');
expect(skill).toBeDefined();
expect(skill.id).toBe('open-design-landing-deck');
});
it('still resolves current ids exactly', async () => {
const skills = await listSkills(skillsRoot);
expect(findSkillById(skills, 'open-design-landing')?.id).toBe(
'open-design-landing',
);
expect(findSkillById(skills, 'simple-deck')?.id).toBe('simple-deck');
});
it('returns undefined for unknown ids and missing inputs', async () => {
const skills = await listSkills(skillsRoot);
expect(findSkillById(skills, 'definitely-not-a-skill')).toBeUndefined();
expect(findSkillById(skills, '')).toBeUndefined();
expect(findSkillById(null, 'open-design-landing')).toBeUndefined();
});
});