Five behaviors that v1.46 ships but had no test coverage. All now pinned.
A) --host all idempotency (test/gen-skill-docs-idempotency.test.ts)
The default test ran Claude host only. Non-Claude hosts (Codex, Factory,
Cursor, OpenClaw, GBrain, Slate, OpenCode, Hermes, Kiro) each have their
own output paths and could carry their own non-deterministic fields. We
hit a "--host all needed for freshness check" mid-/ship. Now: two
consecutive `bun run gen:skill-docs --host all` runs must produce
byte-identical outputs across a per-host sample (.agents/, .cursor/,
.factory/, .gbrain/). Catches per-host adapter regressions before CI.
B) --catalog-mode=full opt-out (test/catalog-mode-full.test.ts)
The legacy escape hatch had zero tests. 6 new tests across two layers:
static (CATALOG_MODE_ARG parsed; conditional gate present; default is
"trim"; invalid value throws) + smoke (actual --catalog-mode=full run
produces a multi-line `description: |` block + omits "## When to invoke"
body section; mutates the working tree then restores in a finally block).
C) parity-baseline-v1.44.1.json integrity (test/parity-baseline-integrity.test.ts)
The baseline is the source of every v1→v2 number cited in the
CHANGELOG v1.46.0.0 entry. Anyone could edit it without test failure
until now. 8 new tests pin: existence, tag, capturedFromCommit
allowlist, expected v1.44 numbers (51 skills, ~2,915 KB, ~9,319
catalog tokens), CHANGELOG references this file by path, per-skill
shape, and a SHA256 byte-stability hash. Any edit fails with a clear
"if intentional, update EXPECTED_HASH AND the CHANGELOG numbers" signal.
D) Live appliesTo gate end-to-end (test/resolver-entry.test.ts extended)
The unwrapResolver unit tests covered the function; the gen-skill-docs.ts
substitution loop that USES the gate had no integration coverage. 6 new
tests simulate the exact 4-line shape from gen-skill-docs.ts:457-467
against synthetic registries: plain-function fires unconditionally,
gated fires when true / empty-string when false, mixed registries
compose, parameterized resolvers respect gates, unknown resolvers throw.
E) Per-skill min-size floor (test/skill-size-budget.test.ts extended)
The existing 200-byte body coverage-floor is a noise floor — a skill
that lost 99.75% of content still passes. 1 new test asserts every
skill stays ≥80% of its v1.44.1 baseline size (the parity-suite
content invariants only covered 10 of 51 skills; the remaining 41
were uncovered). SECTIONS_EXTRACTED hook in place for v2.0.0.0 when
the sections/ pattern legitimately shrinks ship/plan-ceo/etc. past
the floor.
Test plan:
- bun test focused 17-file suite: 1202 pass, 0 fail
(+23 new tests vs the pre-fill 1179 baseline)
- catalog-mode=full mutates working tree then restores cleanly
- --host all idempotency runs two full gen passes in <1s on this machine
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the conditional-resolver-injection plumbing from the v2_PLAN A.1
step. Resolvers can now be either a bare ResolverFn (always fires, current
behavior) or a ResolverEntry { resolve, appliesTo? } (gated; appliesTo
returning false skips the resolver, substitutes empty string).
Why infrastructure-only: the audit during T0a confirmed most resolvers
don't need gating. The {{NAME}} placeholder system is already conditional
at the template level — a resolver only fires for skills that reference it.
The gate is for future use when a placeholder's audience needs a structural
guardrail beyond social convention, or when a sub-resolver inside a larger
composed resolver (e.g. preamble) needs per-skill skip.
scripts/gen-skill-docs.ts:444 now uses unwrapResolver() to handle both
shapes. RESOLVERS map signature widens from Record<string, ResolverFn>
to Record<string, ResolverValue>. All existing resolvers stay bare
functions and work unchanged.
Test plan:
- bun test test/resolver-entry.test.ts: 6 pass (gate plumbing + registry)
- bun test test/gen-skill-docs.test.ts: 389 pass (no regression)
- bun run gen:skill-docs --dry-run: all SKILL.md files FRESH (no diff)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>