gstack/test/ship-completion-report.test.ts

75 lines
3.4 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const root = join(import.meta.dir, '..');
const skillPaths = [
'ship/SKILL.md.tmpl',
'ship/SKILL.md',
'test/fixtures/golden/claude-ship-SKILL.md',
'test/fixtures/golden/codex-ship-SKILL.md',
'test/fixtures/golden/factory-ship-SKILL.md',
];
const engineeringHeadings = [
'### Outcome',
'### Problem and root cause',
'### Investigation and decisions',
'### Implementation',
'### Verification',
'### Risks and operational impact',
'### Remaining work',
'### Decision required',
];
describe('ship completion report', () => {
for (const relativePath of skillPaths) {
test(`${relativePath} requires the complete two-layer handoff`, () => {
const skill = readFileSync(join(root, relativePath), 'utf8');
const handoff = skill.indexOf('## Step 22: Full engineering handoff');
const engineeringSummary = skill.indexOf('## Engineering summary', handoff);
const uiEvidence = skill.indexOf('### UI before/after evidence', handoff);
const putSimply = skill.lastIndexOf('### Put simply');
const uiEvidenceGate = skill.indexOf('3. **UI evidence gate:**');
const pushStep = skill.indexOf('## Step 17: Push');
expect(handoff).toBeGreaterThan(-1);
expect(engineeringSummary).toBeGreaterThan(handoff);
expect(skill).toContain('Use each heading');
let previousHeading = engineeringSummary;
for (const heading of engineeringHeadings) {
const headingIndex = skill.indexOf(heading, previousHeading);
expect(headingIndex).toBeGreaterThan(previousHeading);
previousHeading = headingIndex;
}
expect(uiEvidence).toBeGreaterThan(previousHeading);
expect(skill).toContain('Write `None` when the completion contract is fully satisfied.');
expect(skill).toContain('Write `None` when no decision is required.');
expect(skill).toContain('matched **Before** and **After** screenshots');
expect(skill).toContain('merge-base/base revision in an isolated worktree');
expect(skill).toContain('current branch after implementation and verification');
expect(skill).toContain('route, application state, data, viewport, theme, and zoom');
expect(skill).toContain('Use safe test data; never expose credentials, customer data, or private information.');
expect(skill).toContain('`![Before](path)` and `![After](path)` inline');
expect(skill).toContain('include both artifact paths');
expect(skill).toContain('Do not commit screenshot artifacts unless the repository explicitly requires it.');
expect(skill).toContain('claim UI completion');
expect(uiEvidenceGate).toBeGreaterThan(-1);
expect(pushStep).toBeGreaterThan(uiEvidenceGate);
expect(skill).toMatch(/do not\s+push or create\/update the PR\/MR/);
expect(skill).toContain('STOP before Step 17');
expect(skill).toContain('or offer a waiver');
expect(putSimply).toBeGreaterThan(uiEvidence);
expect(skill.slice(putSimply)).toContain('- **Why:**');
expect(skill.slice(putSimply)).toContain('- **What:**');
expect(skill.slice(putSimply)).toContain('- **How:**');
expect(skill.slice(handoff).match(/^### .+$/gm)?.at(-1)).toBe('### Put simply');
expect(skill.slice(handoff).match(/^## Step /gm)?.length).toBe(1);
expect(skill).not.toContain('output the PR URL at the end');
});
}
});