mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
test(skill-validation): cover bundled browser-skills
Adds 7 assertions per bundled skill at <root>/browser-skills/<name>/: - SKILL.md exists - frontmatter parses with required fields (name/host/triggers/args) - script.ts exists - _lib/browse-client.ts exists and matches the canonical SDK byte-for-byte - script.test.ts exists - script.ts imports browse from ./_lib/browse-client The byte-identical SDK check enforces the version-pinning contract: when the canonical SDK at browse/src/browse-client.ts changes, every bundled skill's _lib/ copy must be re-synced or this test fails. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1669,3 +1669,83 @@ describe('sidebar agent (#584)', () => {
|
||||
expect(content).not.toContain("proc.stderr.on('data', () => {})");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Browser-skills validation ──────────────────────────────────
|
||||
//
|
||||
// Browser-skills are bundled in <gstack-root>/browser-skills/<name>/. Each
|
||||
// must have a SKILL.md whose frontmatter satisfies the contract enforced by
|
||||
// browse/src/browser-skills.ts:parseSkillFile (host required, args + triggers
|
||||
// parseable as the right shape). This test catches malformed bundled skills
|
||||
// at CI time, before they ship.
|
||||
|
||||
describe('Bundled browser-skills frontmatter contract', () => {
|
||||
const browserSkillsRoot = path.join(ROOT, 'browser-skills');
|
||||
|
||||
function listBundledSkillDirs(): string[] {
|
||||
if (!fs.existsSync(browserSkillsRoot)) return [];
|
||||
return fs.readdirSync(browserSkillsRoot)
|
||||
.filter(name => !name.startsWith('.'))
|
||||
.map(name => path.join(browserSkillsRoot, name))
|
||||
.filter(dir => {
|
||||
try { return fs.statSync(dir).isDirectory(); } catch { return false; }
|
||||
});
|
||||
}
|
||||
|
||||
test('each bundled skill has a SKILL.md', () => {
|
||||
for (const dir of listBundledSkillDirs()) {
|
||||
const skillFile = path.join(dir, 'SKILL.md');
|
||||
expect(fs.existsSync(skillFile)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('each bundled skill SKILL.md frontmatter parses with required fields', async () => {
|
||||
const { parseSkillFile } = await import('../browse/src/browser-skills');
|
||||
for (const dir of listBundledSkillDirs()) {
|
||||
const name = path.basename(dir);
|
||||
const content = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf-8');
|
||||
// parseSkillFile throws on missing required fields; we just want to
|
||||
// make sure none of our shipped skills tripwire it.
|
||||
const { frontmatter } = parseSkillFile(content, { skillName: name });
|
||||
expect(frontmatter.name).toBe(name);
|
||||
expect(typeof frontmatter.host).toBe('string');
|
||||
expect(frontmatter.host.length).toBeGreaterThan(0);
|
||||
expect(Array.isArray(frontmatter.triggers)).toBe(true);
|
||||
expect(Array.isArray(frontmatter.args)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('each bundled skill has a script.ts', () => {
|
||||
for (const dir of listBundledSkillDirs()) {
|
||||
expect(fs.existsSync(path.join(dir, 'script.ts'))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('each bundled skill ships a sibling SDK at _lib/browse-client.ts', () => {
|
||||
for (const dir of listBundledSkillDirs()) {
|
||||
expect(fs.existsSync(path.join(dir, '_lib', 'browse-client.ts'))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('each bundled skill has a script.test.ts', () => {
|
||||
for (const dir of listBundledSkillDirs()) {
|
||||
expect(fs.existsSync(path.join(dir, 'script.test.ts'))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("each bundled skill's _lib/browse-client.ts matches the canonical SDK", () => {
|
||||
// If the canonical SDK changes, the bundled copy must be updated. This
|
||||
// test enforces that — the _lib copy should be byte-identical.
|
||||
const canonical = fs.readFileSync(path.join(ROOT, 'browse', 'src', 'browse-client.ts'), 'utf-8');
|
||||
for (const dir of listBundledSkillDirs()) {
|
||||
const sibling = fs.readFileSync(path.join(dir, '_lib', 'browse-client.ts'), 'utf-8');
|
||||
expect(sibling).toBe(canonical);
|
||||
}
|
||||
});
|
||||
|
||||
test('script.ts imports browse from ./_lib/browse-client', () => {
|
||||
for (const dir of listBundledSkillDirs()) {
|
||||
const content = fs.readFileSync(path.join(dir, 'script.ts'), 'utf-8');
|
||||
expect(content).toMatch(/from\s+['"]\.\/_lib\/browse-client['"]/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user