test/skill-listing.test.js
Rama main · réplica de lectura del repositorio original.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, writeFileSync, readFileSync, chmodSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { parseListing, probeListing, refreshListing, currentListing } from '../server/skill-listing.js';
import { reportUngrouped } from '../server/skill-families.js';
import { removeTempDir } from './temp-dir.js';
const FIXTURE = readFileSync(join(import.meta.dirname, 'fixtures', 'skill-probe-request.json'), 'utf8');
let root, file;
beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'a007-listing-')); file = join(root, 'skill-listing.json'); });
afterEach(() => removeTempDir(root));
describe('the captured request', () => {
it('gives every listed skill with its description, a later line included and a bare name kept', () => {
const skills = parseListing(FIXTURE);
expect(skills.map(s => s.name)).toEqual(['copywriting', 'hyperframes-cli', 'ship', 'ponytail:ponytail', 'dataviz',
'code-review', 'loop', 'claude-api', 'anthropic-skills:pdf', 'name-only-skill']);
expect(skills.find(s => s.name === 'claude-api').description).toMatch(/model migration\. TRIGGER — read BEFORE .* SKIP only when/);
expect(skills.at(-1).description).toBe('');
expect(parseListing('{"messages":[{"role":"user","content":"hi"}]}')).toBeNull();
expect(parseListing('not json')).toBeNull();
});
});
// A stand-in `claude` that posts the fixture to ANTHROPIC_BASE_URL with a key header.
const fakeClaude = (body) => {
const bin = join(root, 'claude');
writeFileSync(bin, `#!/usr/bin/env node\n${body}\n`);
chmodSync(bin, 0o755);
return bin;
};
const POST = `const s = JSON.parse(process.argv[process.argv.indexOf('--settings') + 1]);
fetch(process.env.ANTHROPIC_BASE_URL + '/v1/messages', { method: 'POST',
headers: { 'x-api-key': 'sk-ant-REAL-SECRET', authorization: 'Bearer REAL-OAUTH' },
body: require('fs').readFileSync(${JSON.stringify(join(import.meta.dirname, 'fixtures', 'skill-probe-request.json'))}) })
.then(() => setTimeout(() => {}, 60000));
if (s.env.ANTHROPIC_BASE_URL !== process.env.ANTHROPIC_BASE_URL || !process.env.ANTHROPIC_BASE_URL.startsWith('http://127.0.0.1:')) process.exit(3);
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) process.exit(4);`;
describe.skipIf(process.platform === 'win32')('the probe', () => {
it('reads the listing from a loopback request and keeps nothing of its headers', async () => {
const skills = await probeListing({ file: fakeClaude(POST), env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: 'REAL-OAUTH' } });
expect(skills.map(s => s.name)).toContain('dataviz');
const state = await refreshListing({ file, version: async () => '9.9.9 (Claude Code)', probe: async () => skills });
expect(JSON.stringify(state)).not.toMatch(/SECRET|OAUTH|api-key|authorization/i);
expect(readFileSync(file, 'utf8')).not.toMatch(/SECRET|OAUTH|api-key|authorization/i);
});
it('fails with why: claude missing, gone without a request, or too slow', async () => {
await expect(probeListing({ file: join(root, 'no-claude') })).rejects.toThrow(/not installed/);
await expect(probeListing({ file: fakeClaude('process.exit(2)') })).rejects.toThrow(/exited \(2\) without sending/);
await expect(probeListing({ file: fakeClaude('setTimeout(() => {}, 60000)'), timeoutMs: 300 })).rejects.toThrow(/within 0 s/);
});
});
describe('the version-keyed cache', () => {
it('probes once per version, retries a failure only on a new version, and tells Billion once why', async () => {
let version = '1.0.0 (Claude Code)';
let probes = 0;
let fail = false;
const opts = () => ({ file, version: async () => version, probe: async () => { probes++; if (fail) throw new Error('timed out'); return [{ name: 'dataviz', description: 'Charts.' }]; } });
expect((await refreshListing(opts())).skills).toHaveLength(1);
await refreshListing(opts());
expect(probes).toBe(1);
expect(JSON.parse(readFileSync(file, 'utf8'))).toEqual({ version, skills: [{ name: 'dataviz', description: 'Charts.' }] });
version = '1.1.0 (Claude Code)';
fail = true;
expect(await refreshListing(opts())).toEqual({ version, error: 'timed out' });
await refreshListing(opts());
expect(probes).toBe(2);
expect(currentListing(file).error).toBe('timed out');
const sent = [];
const send = (session, headline, lines) => { sent.push(headline, ...lines); return true; };
reportUngrouped({ id: 'b' }, send);
reportUngrouped({ id: 'b' }, send);
expect(sent.filter(l => l.startsWith('No built-in skill family'))).toHaveLength(1);
expect(sent).toContain('Why: timed out.');
// Back to a version cached on disk: no probe.
version = '1.0.0 (Claude Code)';
expect((await refreshListing(opts())).skills).toHaveLength(1);
expect(probes).toBe(2);
});
});