mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
test: cover the shared-preamble hoist (ship Step 7)
Adds test/preamble-hoist.test.ts (tier 1 unchanged; tier 2+ SKILL.md plus sections/preamble.md reproduces the pre-hoist preamble; pointer position and id) and two gen-skill-docs-checks cases (one shared preamble per host, beside every pointer and named by its id; preamble artifact kind). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
f9aa06cf81
commit
0e5d11fd91
2 files changed
+137
No files matched your search
@@ -55,6 +55,29 @@ describe('generator artifact and dry-run contract', () => {
|
||||
].sort((a, b) => a.relativePath.localeCompare(b.relativePath)));
|
||||
});
|
||||
|
||||
// Generated by /ship Step 7 coverage audit (GSTA-96).
|
||||
// Value: protects=on each host every tier 2+ SKILL.md pointer has a sections/preamble.md beside it, all byte-identical under one id that the pointer names;
|
||||
// fails_when=a post-hash host rewrite or skill-dependent render gives two skills different shared files under one id, or a pointer or file is orphaned;
|
||||
// why_new=the all-host validation above checks commands only; goldens pin ship's SKILL.md, never the shared files across skills;
|
||||
// seam=none; no_claim=that installs copy the file (host-conformance checks installed pointers resolve)
|
||||
test('each host renders one shared preamble, beside every pointer and named by its id', () => {
|
||||
const pointerId = /^## Shared preamble \(load first\)\n\nThis skill follows gstack shared preamble `([0-9a-f]{12})`/m;
|
||||
const preambles = generated.artifacts.filter(a => a.kind === 'preamble');
|
||||
const skills = generated.artifacts.filter(a => a.kind === 'skill' && pointerId.test(fs.readFileSync(path.join(render, a.relativePath), 'utf-8')));
|
||||
expect(skills.length).toBeGreaterThan(40 * ALL_HOST_NAMES.length);
|
||||
expect(preambles.map(a => `${a.host}:${path.dirname(path.dirname(a.relativePath))}`).sort())
|
||||
.toEqual(skills.map(a => `${a.host}:${path.dirname(a.relativePath)}`).sort());
|
||||
for (const host of ALL_HOST_NAMES) {
|
||||
const bodies = new Set(preambles.filter(a => a.host === host).map(a => fs.readFileSync(path.join(render, a.relativePath), 'utf-8')));
|
||||
expect(bodies.size, host).toBe(1);
|
||||
const id = /^# gstack shared preamble ([0-9a-f]{12})$/m.exec([...bodies][0])?.[1];
|
||||
expect(id, host).toBeDefined();
|
||||
for (const skill of skills.filter(a => a.host === host)) {
|
||||
expect(pointerId.exec(fs.readFileSync(path.join(render, skill.relativePath), 'utf-8'))?.[1], skill.relativePath).toBe(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('include-minus-skip semantics share one predicate', () => {
|
||||
const host = getHostConfig('claude');
|
||||
expect(includesSkill(host, 'claude-code')).toBe(false);
|
||||
@@ -353,6 +376,20 @@ describe('shared content validators', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Generated by /ship Step 7 coverage audit (GSTA-96).
|
||||
// Value: protects=skill:check rejects an unknown $B command or snapshot flag in a generated sections/preamble.md;
|
||||
// fails_when='preamble' is dropped from the artifact kinds whose commands validateGeneratedArtifact checks;
|
||||
// why_new=the all-host run above only proves valid preamble files pass; the section row covers kind 'section' alone;
|
||||
// seam=none; no_claim=none beyond protects
|
||||
test('the shared preamble validates commands without requiring frontmatter', () => {
|
||||
for (const [content, message] of [['```bash\n$B snapshot --bogus\n```', 'Unknown snapshot flag'], ['```bash\n$B nonexistent-command\n```', 'unknown command']]) {
|
||||
fs.writeFileSync(file, content);
|
||||
const diagnostics = validateGeneratedArtifact(base, { ...artifact, kind: 'preamble', host: 'claude' });
|
||||
expect(diagnostics).toHaveLength(1);
|
||||
expect(diagnostics[0].message).toContain(message);
|
||||
}
|
||||
});
|
||||
|
||||
test('carved sections also validate commands without requiring frontmatter', () => {
|
||||
fs.writeFileSync(file, '```bash\n$B snapshot --bogus\n```');
|
||||
const diagnostics = validateGeneratedArtifact(base, { ...artifact, kind: 'section', host: 'claude' });
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Shared-preamble hoist (GSTA-96, scripts/resolvers/preamble-hoist.ts): the
|
||||
* hoist moves preamble text, it never changes it.
|
||||
*
|
||||
* Generated by /ship Step 7 coverage audit (GSTA-96).
|
||||
*
|
||||
* Value: protects=every tier 2+ skill, on every host and render option, follows exactly the preamble sections it inlined before the hoist; tier 1 is unchanged;
|
||||
* fails_when=a section is hoisted by heading instead of byte identity, a cut-down variant drops a changed subsection, or a section is neither inline nor shared;
|
||||
* why_new=only ship's SKILL.md goldens pin hoist output and they regenerate with it; no test compares the hoisted render to generatePreamble;
|
||||
* seam=none; no_claim=real template frontmatter and host post-render rewrites (synthetic contexts at the resolver boundary; rendered files are checked in gen-skill-docs-checks)
|
||||
*
|
||||
* Value: protects=the "Shared preamble (load first)" pointer opens a tier 2+ preamble, before the start-command section, and names the shared file's id;
|
||||
* fails_when=the pointer moves after the start command, or its id stops matching the shared file's heading;
|
||||
* why_new=skill-fixture.test.ts pins only that the pointer follows plan-eng-review's scope gate;
|
||||
* seam=none; no_claim=that a model honors the pointer (an eval question; resolver output only)
|
||||
*/
|
||||
import { afterAll, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { generatePreamble } from '../scripts/resolvers/preamble';
|
||||
import { generateSharedPreambleFile, generateSkillPreamble, hoistsPreamble } from '../scripts/resolvers/preamble-hoist';
|
||||
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
|
||||
import { ALL_HOST_NAMES } from '../hosts/index';
|
||||
import { SHARED_POINTER_HEADING, SHARED_PREAMBLE_FILE, readSkillEffective } from './helpers/skill-startup';
|
||||
|
||||
const SKILLS = ['ship', 'review', 'qa', 'investigate', 'retro', 'office-hours', 'plan-ceo-review', 'plan-eng-review',
|
||||
'plan-design-review', 'plan-devex-review', 'make-pdf', 'gstack-upgrade', 'ios-fix'];
|
||||
const OPTIONS: Partial<TemplateContext>[] = [{}, { interactive: true }, { interactive: false }, { explainLevel: 'terse' }, { planMode: false }, { model: 'opus-4-7' }];
|
||||
|
||||
const contexts = (): TemplateContext[] => ALL_HOST_NAMES.flatMap(host => SKILLS.flatMap(skillName => [1, 2, 3, 4].flatMap(tier =>
|
||||
OPTIONS.map(option => ({ skillName, tmplPath: `${skillName}/SKILL.md.tmpl`, host, paths: HOST_PATHS[host], preambleTier: tier, ...option } as TemplateContext)))));
|
||||
|
||||
const label = (ctx: TemplateContext) => `${ctx.host}/${ctx.skillName} tier ${ctx.preambleTier} ${JSON.stringify({ interactive: ctx.interactive, explainLevel: ctx.explainLevel, planMode: ctx.planMode, model: ctx.model })}`;
|
||||
|
||||
/** `## ` sections outside code fences, sorted: the effective view groups sections differently, never changes them. */
|
||||
function sections(text: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let current: string[] = [];
|
||||
let fence: string | null = null;
|
||||
for (const line of text.split('\n')) {
|
||||
const delimiter = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
|
||||
if (delimiter) {
|
||||
if (!fence) fence = delimiter;
|
||||
else if (delimiter[0] === fence[0] && delimiter.length >= fence.length) fence = null;
|
||||
} else if (!fence && line.startsWith('## ') && current.some(l => l.trim())) {
|
||||
parts.push(current.join('\n').trim());
|
||||
current = [];
|
||||
}
|
||||
current.push(line);
|
||||
}
|
||||
if (current.some(l => l.trim())) parts.push(current.join('\n').trim());
|
||||
return parts.sort();
|
||||
}
|
||||
|
||||
/** The headless upgrade-check rules attach to whichever section precedes them; compare them apart (as plan-mode-off-generation does). */
|
||||
const UPGRADE_RULES = /\n\nIf `PROACTIVE` is [^\n]*\n\nIf `SKILL_PREFIX` is [^\n]*/;
|
||||
const apart = (text: string) => ({ sections: sections(text.replace(UPGRADE_RULES, '')), upgradeRules: UPGRADE_RULES.exec(text)?.[0] ?? null });
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-preamble-hoist-'));
|
||||
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
|
||||
describe('shared-preamble hoist', () => {
|
||||
test('tier 1 is unchanged; tier 2+ SKILL.md plus sections/preamble.md follows exactly the pre-hoist preamble', () => {
|
||||
let hoisted = 0;
|
||||
contexts().forEach((ctx, i) => {
|
||||
const full = generatePreamble(ctx);
|
||||
const own = generateSkillPreamble(ctx);
|
||||
if (!hoistsPreamble(ctx)) {
|
||||
expect(own, label(ctx)).toBe(full);
|
||||
return;
|
||||
}
|
||||
const dir = path.join(tmp, String(i));
|
||||
fs.mkdirSync(path.join(dir, 'sections'), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'SKILL.md'), `# fixture\n\n${own}\n`);
|
||||
fs.writeFileSync(path.join(dir, 'sections', SHARED_PREAMBLE_FILE), generateSharedPreambleFile(ctx));
|
||||
const effective = readSkillEffective(path.join(dir, 'SKILL.md')).slice('# fixture\n\n'.length);
|
||||
expect(apart(effective), label(ctx)).toEqual(apart(full));
|
||||
hoisted++;
|
||||
});
|
||||
expect(hoisted).toBe(ALL_HOST_NAMES.length * SKILLS.length * 3 * OPTIONS.length);
|
||||
});
|
||||
|
||||
test('the pointer opens a tier 2+ preamble, precedes the start command and names the shared file id', () => {
|
||||
for (const ctx of contexts()) {
|
||||
const own = generateSkillPreamble(ctx);
|
||||
if (!hoistsPreamble(ctx)) {
|
||||
expect(own, label(ctx)).not.toContain(SHARED_POINTER_HEADING);
|
||||
continue;
|
||||
}
|
||||
expect(own.startsWith(`${SHARED_POINTER_HEADING}\n`), label(ctx)).toBe(true);
|
||||
const start = own.search(/^## Preamble \((?:run first|after scope gate)\)$/m);
|
||||
expect(start, label(ctx)).toBeGreaterThan(0);
|
||||
expect(own.indexOf(`\n${SHARED_POINTER_HEADING}\n`), label(ctx)).toBe(-1);
|
||||
const id = /^# gstack shared preamble ([0-9a-f]{12})$/m.exec(generateSharedPreambleFile(ctx))?.[1];
|
||||
expect(id, label(ctx)).toBeDefined();
|
||||
expect(own.slice(0, start), label(ctx)).toContain(`shared preamble \`${id}\``);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user