mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-24 10:39:57 +02:00
86bb93758a
Two free gate tests so a future carve or workflow refactor can't silently
regress:
- ship-pr-title-version-always-loaded: asserts the invariant lives in the
always-loaded ship/SKILL.md skeleton (not only sections/), and that the
skeleton+sections union keeps BOTH the create and the existing-PR update
title paths. Modeled on test/auq-format-always-loaded.test.ts.
- pr-title-sync-workflow-safety: static tripwire that fails CI if
pr-title-sync.yml checks out PR-head code or inlines an attacker-controlled
${{ github.event.pull_request.* }} field inside a run: block (the two
pull_request_target footguns actionlint cannot catch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
4.6 KiB
TypeScript
93 lines
4.6 KiB
TypeScript
/**
|
|
* /ship PR-title-version rule is ALWAYS-LOADED — token-reduction safety net (gate, free).
|
|
*
|
|
* The anxiety this kills: the v1.54.0.0 carve ("carve /ship into skeleton +
|
|
* on-demand sections, -59% always-loaded") moved the rule "PR title MUST start
|
|
* with v$NEW_VERSION" out of the always-loaded monolith and entirely into the
|
|
* lazily-loaded `ship/sections/pr-body.md`. The agent then sets the version
|
|
* prefix only if it happens to read that section before creating the PR; when it
|
|
* doesn't, PRs land with bare titles. This is the exact regression that shipped
|
|
* — every recent open PR lacked a `v...` prefix until this guard + the skeleton
|
|
* invariant restored it.
|
|
*
|
|
* This is the title-rule analogue of `test/auq-format-always-loaded.test.ts`,
|
|
* which guards the AskUserQuestion format the same way. A carve that re-buries
|
|
* the title rule fails here in milliseconds instead of surfacing weeks later as
|
|
* a wave of version-less PR titles.
|
|
*
|
|
* The guarantee, made mechanical and per-PR:
|
|
* 1. SKELETON PRESENCE — `ship/SKILL.md` (the always-loaded skeleton) carries
|
|
* the invariant: the `v$NEW_VERSION` token, the single-source-of-truth
|
|
* helper name, and a directive near a "PR title" mention. Present the
|
|
* instant /ship reaches the push/PR steps, no section read required.
|
|
* 2. UNION SURVIVAL (both paths) — the FULL procedure still exists somewhere
|
|
* in skeleton+sections for BOTH the create path (`gh pr create --title
|
|
* "v$NEW_VERSION ...`) AND the existing-PR update path (the `gh pr edit
|
|
* --title` rewrite rule). A drop of either is the failure.
|
|
*/
|
|
import { describe, test, expect } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const SHIP_SKILL = path.join(ROOT, 'ship', 'SKILL.md');
|
|
const SHIP_SECTIONS = path.join(ROOT, 'ship', 'sections');
|
|
|
|
function readUnion(): string {
|
|
let union = fs.readFileSync(SHIP_SKILL, 'utf-8');
|
|
for (const f of fs.readdirSync(SHIP_SECTIONS)) {
|
|
if (f.endsWith('.md') && !f.endsWith('.md.tmpl')) {
|
|
union += '\n' + fs.readFileSync(path.join(SHIP_SECTIONS, f), 'utf-8');
|
|
}
|
|
}
|
|
return union;
|
|
}
|
|
|
|
describe('/ship PR-title-version rule is always-loaded (token-reduction safety net)', () => {
|
|
test('sanity: ship is a carved skill (has sections/*.md)', () => {
|
|
// Guards against a path regression that would make the union/skeleton checks
|
|
// vacuously pass against a non-carved skill.
|
|
expect(fs.existsSync(SHIP_SECTIONS)).toBe(true);
|
|
expect(fs.readdirSync(SHIP_SECTIONS).some(f => f.endsWith('.md'))).toBe(true);
|
|
});
|
|
|
|
test('skeleton (ship/SKILL.md) carries the PR-title-version invariant', () => {
|
|
const skeleton = fs.readFileSync(SHIP_SKILL, 'utf-8');
|
|
// Robust independent markers, NOT one brittle full-sentence regex (so
|
|
// rewording the prose doesn't false-fail). All three must be present in the
|
|
// always-loaded skeleton.
|
|
const markers: Array<{ name: string; re: RegExp }> = [
|
|
{ name: 'v$NEW_VERSION token', re: /v\$NEW_VERSION/ },
|
|
{ name: 'gstack-pr-title-rewrite helper reference', re: /gstack-pr-title-rewrite/ },
|
|
{ name: 'a directive (MUST/always/never) near a PR-title mention', re: /(MUST|always|never)[^\n]{0,80}\btitle\b|\btitle\b[^\n]{0,80}(MUST|always|never)/i },
|
|
];
|
|
const missing = markers.filter(m => !m.re.test(skeleton));
|
|
if (missing.length > 0) {
|
|
throw new Error(
|
|
`ship/SKILL.md (the always-loaded skeleton) is missing the PR-title-version ` +
|
|
`invariant — a carve likely re-buried it in sections/. Missing:\n` +
|
|
missing.map(m => ` - ${m.name} (${m.re.source})`).join('\n'),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('union (skeleton+sections) keeps BOTH the create and the update title rules', () => {
|
|
const union = readUnion();
|
|
const paths: Array<{ name: string; re: RegExp }> = [
|
|
// create path: `gh pr create --title "v$NEW_VERSION ...`
|
|
{ name: 'PR create path prefixes the version', re: /pr create[^\n]*--title[^\n]*v\$NEW_VERSION/i },
|
|
// update/idempotent path: the existing-PR `gh pr edit --title` rewrite rule
|
|
{ name: 'PR update path rewrites the title', re: /pr edit[^\n]*--title/i },
|
|
];
|
|
const missing = paths.filter(p => !p.re.test(union));
|
|
if (missing.length > 0) {
|
|
throw new Error(
|
|
`ship skeleton+sections dropped a PR-title-version code path. The update ` +
|
|
`path is the more important idempotent /ship path — a create-only guard ` +
|
|
`would miss its rot. Missing:\n` +
|
|
missing.map(p => ` - ${p.name} (${p.re.source})`).join('\n'),
|
|
);
|
|
}
|
|
});
|
|
});
|