diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 522687d86..13925bf09 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -2933,7 +2933,8 @@ describe('Codex generation (--host codex)', () => { // A template's codex_default_prompt: key is the sentence Codex's picker // inserts for the skill; without the key the generic fallback keeps every // entry valid. The key is generator input only: no host render carries it. - const CODEX_PROMPT_SKILLS = ['ship', 'review', 'qa', 'autoplan', 'office-hours', 'land-and-deploy']; + // Value: protects=the new /modernize skill's picker prompt reaches its Codex render; fails_when=modernize's key is dropped, misquoted or unrendered; why_new=modernize-skill.test reads the template only; seam=none; no_claim=prompt wording quality + const CODEX_PROMPT_SKILLS = ['ship', 'review', 'qa', 'autoplan', 'office-hours', 'land-and-deploy', 'modernize']; test('codex_default_prompt: in a template becomes that skill\'s default_prompt', () => { for (const dir of CODEX_PROMPT_SKILLS) { const tmpl = fs.readFileSync(path.join(ROOT, dir, 'SKILL.md.tmpl'), 'utf-8'); diff --git a/test/host-renders-openai-yaml-proof.test.ts b/test/host-renders-openai-yaml-proof.test.ts new file mode 100644 index 000000000..b32a80d91 --- /dev/null +++ b/test/host-renders-openai-yaml-proof.test.ts @@ -0,0 +1,53 @@ +/** + * bin/gstack-host-renders.sh: the WEAK proof that an agents/openai.yaml is the + * generator's own render, now that a template's codex_default_prompt: makes + * the default_prompt line vary per skill (it used to be one fixed sentence). + * + * Value: protects=_gstack_hr_generated accepts the generator's openai.yaml for any default_prompt and still rejects a foreign or edited one; fails_when=the shape check reverts to the generic-prompt literal, or drops display_name/policy/6-line limits; why_new=host-renders-prune asserts one backed-up line; no test exercises the rejected shapes; seam=none; no_claim=what the prune does after the proof + */ +import { describe, expect, test } from 'bun:test'; +import { spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { generateOpenAIYaml } from '../scripts/gen-skill-docs'; + +const ROOT = path.resolve(import.meta.dir, '..'); +const HELPER = path.join(ROOT, 'bin/gstack-host-renders.sh'); + +describe.skipIf(process.platform === 'win32')('_gstack_hr_generated proves an openai.yaml by shape, not by its prompt sentence', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-openai-yaml-proof-')); + const tree = path.join(dir, '.agents'); + const real = path.join(dir, 'checkout'); + fs.mkdirSync(real, { recursive: true }); + + function proven(rel: string, content: string): boolean { + const file = path.join(tree, rel); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content); + const r = spawnSync('bash', ['-c', `. "${HELPER}"\n_gstack_hr_generated "${file}" "${tree}" "${real}" && echo proven || echo kept`], { + encoding: 'utf8', timeout: 30_000, env: { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: os.tmpdir() }, + }); + expect(r.status, r.stderr).toBe(0); + return r.stdout.trim() === 'proven'; + } + + const custom = generateOpenAIYaml('gstack-ship', 'short', 'Use $gstack-ship to take this branch to a pull request: "quoted", colons: too.'); + const generic = generateOpenAIYaml('gstack-careful', 'short'); + + test('the generator\'s render is proven whether the prompt is the skill\'s own sentence or the fallback', () => { + expect(custom).not.toContain('Use gstack-ship for this task.'); + expect(proven('skills/gstack-ship/agents/openai.yaml', custom)).toBe(true); + expect(generic).toContain('default_prompt: "Use gstack-careful for this task."'); + expect(proven('skills/gstack-careful/agents/openai.yaml', generic)).toBe(true); + }); + + test('a file that is not the generator\'s shape for that skill directory is kept, not backed up', () => { + expect(proven('skills/gstack-qa/agents/openai.yaml', custom), 'display_name names another skill').toBe(false); + expect(proven('skills/gstack-ship/agents/openai.yaml', custom.replace('policy:\n', 'rules:\n')), 'no policy: block').toBe(false); + expect(proven('skills/gstack-ship/agents/openai.yaml', custom.replace(' default_prompt: "', ' default_prompt: \'')), 'prompt not double-quoted').toBe(false); + expect(proven('skills/gstack-ship/agents/openai.yaml', custom + ' my_note: hand-written\n'), 'a seventh line').toBe(false); + expect(proven('skills/gstack-ship/agents/openai.yaml', 'notes:\n' + custom.slice('interface:\n'.length)), 'first line is not interface:').toBe(false); + expect(proven('skills/gstack-ship/openai.yaml', custom), 'not under agents/').toBe(false); + }); +}); diff --git a/test/unattended-grant.test.ts b/test/unattended-grant.test.ts new file mode 100644 index 000000000..95c369a21 --- /dev/null +++ b/test/unattended-grant.test.ts @@ -0,0 +1,44 @@ +/** + * docs/unattended.md#what-an-unattended-session-may-do: the grant a parent + * gives an unattended run, and the pointer /ship's handoff section carries so a + * lane reads it where it reports done. + * + * Value: protects=the handoff pointer resolves to the grant section, and the grant states merge-never, the pinned-lease push form and the /modernize disposition; fails_when=the heading is renamed or moved, the pointer is dropped from the template or its render, or the grant loses a rule; why_new=no test reads the new section or the handoff pointer; seam=none; no_claim=that a run obeys the grant (eval-side) + */ +import { describe, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +const ROOT = path.resolve(import.meta.dir, '..'); +const read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), 'utf8'); +const POINTER = 'docs/unattended.md#what-an-unattended-session-may-do'; +const slug = (heading: string) => heading.toLowerCase().replace(/[^a-z0-9 -]/g, '').trim().replace(/ /g, '-'); + +describe('the unattended grant and the /ship handoff pointer to it', () => { + const docs = read('docs/unattended.md'); + const grantStart = docs.indexOf('\n## What an unattended session may do\n'); + const grant = docs.slice(grantStart + 1, (i => i === -1 ? docs.length : i)(docs.indexOf('\n## ', grantStart + 1))); + + test('the handoff section points at a heading that exists, in the template and its render', () => { + expect(grantStart).toBeGreaterThan(0); + const heading = grant.slice(3, grant.indexOf('\n')); + expect(`docs/unattended.md#${slug(heading)}`).toBe(POINTER); + expect(read('ship/sections/handoff.md.tmpl')).toContain(`What an\nunattended run may do is ${POINTER}.`); + expect(read('ship/sections/handoff.md')).toContain(POINTER); + expect(grant).toContain('`/ship`\'s handoff section (`ship/sections/handoff.md`) points here'); + }); + + test('the grant names each rule once: merge never, a pinned-lease force-push on its own branch, named test methods, own-eyes proof', () => { + const flat = grant.replace(/\s+/g, ' '); + expect(flat).toContain('- **Merge: never.**'); + expect(flat).toContain('- **Force-push: its own branch only, after a rebase the plan named, with a pinned lease.**'); + expect(flat).toContain('- **Tests: the methods the repository\'s instructions name, without asking first.**'); + expect(flat).toContain('- **Proof is what the run saw itself.**'); + expect(flat).toContain('`git push --force-with-lease=: origin `'); + expect(flat).toContain('a plain `--force` is never allowed'); + expect(flat).toContain('`git range-diff .. ..` runs before the push'); + expect(flat).toContain('a `/modernize` disposition names before it happens'); + expect(flat).toContain('enabling auto-merge, adding the PR to a merge queue, or `gh pr ready` on a PR the run did not open'); + expect(flat).toContain('appended to `decisions.jsonl` with `kind: "approval"` and `status: "pending"`'); + }); +});