mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
177 lines
6.7 KiB
TypeScript
177 lines
6.7 KiB
TypeScript
/**
|
|
* Tests for src/core/skillpack/init-scaffold.ts +
|
|
* src/core/skillpack/pack-publish.ts — the publisher side.
|
|
*/
|
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
import { spawnSync } from 'child_process';
|
|
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
|
|
import {
|
|
InitScaffoldError,
|
|
runInitScaffold,
|
|
} from '../src/core/skillpack/init-scaffold.ts';
|
|
import {
|
|
PackPublishError,
|
|
runPackPublish,
|
|
} from '../src/core/skillpack/pack-publish.ts';
|
|
import { loadSkillpackManifest } from '../src/core/skillpack/manifest-v1.ts';
|
|
import { runDoctor } from '../src/core/skillpack/doctor.ts';
|
|
|
|
function hasGnuTar(): boolean {
|
|
for (const bin of ['gtar', 'tar']) {
|
|
const r = spawnSync(bin, ['--version'], { encoding: 'utf-8' });
|
|
if (r.status === 0 && r.stdout.includes('GNU')) return true;
|
|
}
|
|
return false;
|
|
}
|
|
const SKIP_NO_GNU = !hasGnuTar();
|
|
|
|
let tmp: string;
|
|
beforeAll(() => {
|
|
tmp = mkdtempSync(join(tmpdir(), 'init-pack-'));
|
|
});
|
|
afterAll(() => {
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('runInitScaffold — cathedral default', () => {
|
|
test('scaffolds every file the rubric expects', () => {
|
|
const dir = mkdtempSync(join(tmp, 'init-cathedral-'));
|
|
const result = runInitScaffold({ targetDir: dir, name: 'test-pack' });
|
|
for (const expected of [
|
|
'skillpack.json',
|
|
'skills/test-pack/SKILL.md',
|
|
'skills/test-pack/routing-eval.jsonl',
|
|
'runbooks/bootstrap.md',
|
|
'CHANGELOG.md',
|
|
'README.md',
|
|
'LICENSE',
|
|
'.gitignore',
|
|
'test/example.test.ts',
|
|
'e2e/example.e2e.test.ts',
|
|
'evals/test-pack.judge.json',
|
|
]) {
|
|
expect(existsSync(join(dir, expected))).toBe(true);
|
|
}
|
|
expect(result.filesWritten.length).toBeGreaterThan(8);
|
|
expect(result.manifest.name).toBe('test-pack');
|
|
});
|
|
|
|
test('--minimal omits test / e2e / evals', () => {
|
|
const dir = mkdtempSync(join(tmp, 'init-min-'));
|
|
runInitScaffold({ targetDir: dir, name: 'mini-pack', minimal: true });
|
|
expect(existsSync(join(dir, 'skillpack.json'))).toBe(true);
|
|
expect(existsSync(join(dir, 'test/example.test.ts'))).toBe(false);
|
|
expect(existsSync(join(dir, 'e2e/example.e2e.test.ts'))).toBe(false);
|
|
expect(existsSync(join(dir, 'evals/mini-pack.judge.json'))).toBe(false);
|
|
});
|
|
|
|
test('refuses to overwrite existing files', () => {
|
|
const dir = mkdtempSync(join(tmp, 'init-overwrite-'));
|
|
mkdirSync(join(dir, 'skills/preexist'), { recursive: true });
|
|
writeFileSync(join(dir, 'skills/preexist/SKILL.md'), 'user content');
|
|
const result = runInitScaffold({ targetDir: dir, name: 'preexist' });
|
|
// The user's SKILL.md should be in filesSkippedExisting
|
|
// filesSkippedExisting holds join()-built paths — '\' on win32.
|
|
expect(
|
|
result.filesSkippedExisting.some((p) => p.endsWith(join('skills', 'preexist', 'SKILL.md'))),
|
|
).toBe(true);
|
|
// And contents preserved.
|
|
expect(require('fs').readFileSync(join(dir, 'skills/preexist/SKILL.md'), 'utf-8')).toBe('user content');
|
|
});
|
|
|
|
test('rejects invalid kebab name', () => {
|
|
try {
|
|
runInitScaffold({ targetDir: tmp, name: 'UpperCase' });
|
|
throw new Error('should have thrown');
|
|
} catch (err) {
|
|
expect((err as InitScaffoldError).code).toBe('invalid_name');
|
|
}
|
|
});
|
|
|
|
test('--dry-run writes nothing but reports plan', () => {
|
|
const dir = mkdtempSync(join(tmp, 'init-dry-'));
|
|
const result = runInitScaffold({ targetDir: dir, name: 'dry-pack', dryRun: true });
|
|
expect(result.filesWritten.length).toBeGreaterThan(0);
|
|
expect(existsSync(join(dir, 'skillpack.json'))).toBe(false);
|
|
});
|
|
|
|
test('freshly scaffolded pack scores 10/10 on doctor --quick', async () => {
|
|
const dir = mkdtempSync(join(tmp, 'init-tenten-'));
|
|
runInitScaffold({ targetDir: dir, name: 'reference' });
|
|
const r = await runDoctor({ packRoot: dir, mode: 'quick' });
|
|
expect(r.score).toBe(10);
|
|
expect(r.tier_eligibility).toBe('endorsed');
|
|
});
|
|
});
|
|
|
|
describe('runPackPublish — happy path', () => {
|
|
test.skipIf(SKIP_NO_GNU)('emits a deterministic tarball when doctor passes', async () => {
|
|
const dir = mkdtempSync(join(tmp, 'pack-good-'));
|
|
runInitScaffold({ targetDir: dir, name: 'pack-good' });
|
|
const result = await runPackPublish({ packRoot: dir });
|
|
expect(result.refused_reason).toBeNull();
|
|
expect(result.tarball).not.toBeNull();
|
|
expect(result.tarball?.outPath).toContain('pack-good-0.1.0.tgz');
|
|
expect(result.tarball?.sha256.length).toBe(64);
|
|
expect(result.tarball?.fileCount).toBeGreaterThan(0);
|
|
expect(result.tarball?.tier_eligibility).toBe('endorsed');
|
|
expect(existsSync(result.tarball!.outPath)).toBe(true);
|
|
});
|
|
|
|
test('refuses to pack when doctor reports blocked tier', async () => {
|
|
const dir = mkdtempSync(join(tmp, 'pack-bad-'));
|
|
runInitScaffold({ targetDir: dir, name: 'pack-bad' });
|
|
// Sabotage core dimension 5 by removing CHANGELOG.
|
|
rmSync(join(dir, 'CHANGELOG.md'));
|
|
const result = await runPackPublish({ packRoot: dir });
|
|
expect(result.tarball).toBeNull();
|
|
expect(result.refused_reason).toContain('changelog_present_and_current');
|
|
});
|
|
|
|
test('dry-run skips tarball but runs doctor', async () => {
|
|
const dir = mkdtempSync(join(tmp, 'pack-dry-'));
|
|
runInitScaffold({ targetDir: dir, name: 'pack-dry' });
|
|
const result = await runPackPublish({ packRoot: dir, dryRun: true });
|
|
expect(result.tarball).toBeNull();
|
|
expect(result.doctor?.score).toBe(10);
|
|
});
|
|
|
|
test('throws when skillpack.json missing', async () => {
|
|
const dir = mkdtempSync(join(tmp, 'pack-missing-'));
|
|
try {
|
|
await runPackPublish({ packRoot: dir });
|
|
throw new Error('should have thrown');
|
|
} catch (err) {
|
|
expect((err as PackPublishError).code).toBe('manifest_load_failed');
|
|
}
|
|
});
|
|
|
|
test.skipIf(SKIP_NO_GNU)('--skip-doctor packs without running the gate', async () => {
|
|
const dir = mkdtempSync(join(tmp, 'pack-skipdoc-'));
|
|
runInitScaffold({ targetDir: dir, name: 'pack-skipdoc' });
|
|
const result = await runPackPublish({ packRoot: dir, skipDoctor: true });
|
|
expect(result.doctor).toBeNull();
|
|
expect(result.tarball).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('end-to-end init -> doctor -> pack', () => {
|
|
test.skipIf(SKIP_NO_GNU)('full publisher loop on a fresh pack', async () => {
|
|
const dir = mkdtempSync(join(tmp, 'e2e-'));
|
|
// 1. init
|
|
runInitScaffold({ targetDir: dir, name: 'e2e-pack' });
|
|
// 2. doctor
|
|
const doc = await runDoctor({ packRoot: dir, mode: 'quick' });
|
|
expect(doc.score).toBe(10);
|
|
// 3. pack
|
|
const pkg = await runPackPublish({ packRoot: dir });
|
|
expect(pkg.tarball).not.toBeNull();
|
|
// 4. manifest round-trip
|
|
const m = loadSkillpackManifest(dir);
|
|
expect(m.name).toBe('e2e-pack');
|
|
});
|
|
});
|