Files
gstack/test/catalog-mode-full.test.ts
T
Garry TanandClaude Fable 5 1d945e74b9 fix(test): catalog-mode-full renders to out-dir; restore machinery deleted
The full-catalog smoke no longer rewrites all 71 SKILL.md then
regenerates to restore (with its 'CRITICAL: failed to restore' prayer
path) — it renders into a mkdtemp and additionally asserts tracked
ship/SKILL.md is byte-unchanged. TREE_MUTATING entry deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 05:32:59 +00:00

114 lines
5.1 KiB
TypeScript

/**
* Gap B (v1.46.0.0): --catalog-mode=full opt-out behavior.
*
* The catalog trim is the default. The opt-out (`--catalog-mode=full`)
* preserves v1.44 multi-line frontmatter descriptions for users / hosts
* that depend on the legacy fat catalog. Without this test, someone could
* break the conditional `if (host === 'claude' && CATALOG_MODE === 'trim')`
* and silently turn the opt-out path into a no-op — users with the flag
* still get trim'd output, the v1.44 behavior is gone.
*
* Two layers:
* 1. Static: the CATALOG_MODE flag is wired into gen-skill-docs.ts and
* the conditional gate is in the pipeline.
* 2. Smoke: running with --catalog-mode=full produces a frontmatter
* `description: |` block (multi-line) instead of the trim'd one-line
* `description: ...(gstack)` form.
*
* The smoke test renders the full-catalog variant into an isolated
* --out-dir — the working tree is never written, so there is no restore
* pass (and no half-restored tree if the test crashes mid-run).
*/
import { describe, test, expect } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const REPO_ROOT = path.resolve(import.meta.dir, '..');
const GEN_SKILL_DOCS = path.join(REPO_ROOT, 'scripts', 'gen-skill-docs.ts');
const SHIP_SKILL = path.join(REPO_ROOT, 'ship', 'SKILL.md');
describe('--catalog-mode=full opt-out wiring (static)', () => {
test('CATALOG_MODE_ARG parsing is wired into gen-skill-docs.ts', () => {
const src = fs.readFileSync(GEN_SKILL_DOCS, 'utf-8');
expect(src).toContain('CATALOG_MODE_ARG');
expect(src).toContain("a.startsWith('--catalog-mode')");
});
test('CATALOG_MODE accepts only "trim" or "full" — anything else throws', () => {
const src = fs.readFileSync(GEN_SKILL_DOCS, 'utf-8');
expect(src).toMatch(/val !== 'trim' && val !== 'full'/);
expect(src).toContain('Unknown catalog mode');
});
test('catalog trim only fires when CATALOG_MODE === "trim"', () => {
const src = fs.readFileSync(GEN_SKILL_DOCS, 'utf-8');
// The applyCatalogTrim call is gated by both host and CATALOG_MODE checks.
expect(src).toMatch(/CATALOG_MODE === 'trim'/);
expect(src).toContain('applyCatalogTrim(content, skillName)');
});
test('default CATALOG_MODE is "trim" (opt-out, not opt-in)', () => {
const src = fs.readFileSync(GEN_SKILL_DOCS, 'utf-8');
// The const initializer falls back to 'trim' when --catalog-mode is unset.
expect(src).toMatch(/if \(!CATALOG_MODE_ARG\) return 'trim'/);
});
});
describe('--catalog-mode=full opt-out behavior (smoke)', () => {
test('--catalog-mode=full produces multi-line description in frontmatter', () => {
// The TRACKED ship/SKILL.md carries the default trim'd form (read-only check).
// #1778: the trimmed ship description has an interior colon ("Ship workflow:")
// and is now YAML-quoted — tolerate the optional surrounding quotes.
const trimmedShip = fs.readFileSync(SHIP_SKILL, 'utf-8');
expect(trimmedShip).toMatch(/^description: "?Ship workflow:[^\n]*\(gstack\)"?\n/m);
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-catalog-full-'));
try {
// Render --catalog-mode=full into an isolated out-dir. The working
// tree is never written, so no restore pass is needed.
const result = spawnSync('bun', ['run', 'gen:skill-docs', '--catalog-mode=full', '--out-dir', outDir], {
cwd: REPO_ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 60_000,
});
expect(result.status).toBe(0);
// In the full-mode render, frontmatter description is the legacy
// multi-line block, not the trim'd one-line form.
const fullShip = fs.readFileSync(path.join(outDir, 'ship', 'SKILL.md'), 'utf-8');
expect(fullShip).toMatch(/^description: \|\s*$/m); // YAML block scalar
// Legacy multi-line content includes "Use when asked to..." in the
// frontmatter (in trim mode this lives in the body section).
const fmEnd = fullShip.indexOf('\n---', 4);
const fm = fullShip.slice(0, fmEnd);
expect(fm).toMatch(/Use when asked to/i);
// "When to invoke" body section should NOT be present in full mode
// (because the routing prose stayed in frontmatter).
const body = fullShip.slice(fmEnd);
expect(body).not.toContain('## When to invoke this skill');
// Non-mutation proof: the tracked ship/SKILL.md is byte-unchanged —
// a catalog-mode render must never rewrite the committed trim'd state.
expect(fs.readFileSync(SHIP_SKILL, 'utf-8')).toBe(trimmedShip);
} finally {
fs.rmSync(outDir, { recursive: true, force: true });
}
}, 180_000);
test('--catalog-mode=invalid throws a clear error', () => {
const result = spawnSync('bun', ['run', 'gen:skill-docs', '--catalog-mode=invalid'], {
cwd: REPO_ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 30_000,
});
expect(result.status).not.toBe(0);
const stderr = result.stderr?.toString() ?? '';
expect(stderr).toMatch(/Unknown catalog mode/);
expect(stderr).toMatch(/invalid/);
});
});