mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-15 19:06:18 +08:00
a517ecee07
scripts/skill-conformance.test.ts auto-discovers each .claude/skills/*/SKILL.md with nc: fences (15 today) and asserts, per skill: lint clean (errors and warn-lints); per branch-scenario from a colocated apply-fixtures.json, a full applySkill run with stubbed exec/execStream/resolveRemote goes green — nothing deferred, nothing degraded to agent, every var bound; every when: guard value is exercised by some scenario (coverage read from ApplyResult.vars, with documented exclusions); static effect ordering (mutations before build, build before test, restart only after both); and a run-health sabotage probe — a failing fetch/check/external run must gate every later restart/step/wire. Fixture values satisfy the skills' real validate: regexes, so a fixture can't paper over a broken prompt. Skills with prompts but no fixture fail with an actionable message; prompt-less skills get a default empty scenario. scripts/skill-inputs.ts adds the inputsFromEnv helper from the seam spec §6 (NC_INPUT_<VAR>, collision error); its test round-trips env-supplied inputs through a real skill apply. Runs in the existing CI vitest step — no workflow change needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
83 lines
3.0 KiB
TypeScript
83 lines
3.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import { inputsFromEnv } from './skill-inputs.js';
|
|
import { applySkill, fullyApplied } from './skill-apply.js';
|
|
|
|
describe('inputsFromEnv (docs/skill-engine-seam.md §6)', () => {
|
|
it('maps NC_INPUT_<VAR> env keys onto prompt vars, ignoring unset and unrelated keys', () => {
|
|
const md = [
|
|
'```nc:prompt bot_token secret',
|
|
'Paste the token.',
|
|
'```',
|
|
'```nc:prompt owner_handle',
|
|
'Your handle?',
|
|
'```',
|
|
].join('\n');
|
|
const inputs = inputsFromEnv(md, {
|
|
NC_INPUT_BOT_TOKEN: 'xoxb-fake',
|
|
NC_INPUT_UNRELATED: 'ignored', // no matching prompt
|
|
PATH: '/usr/bin', // not NC_INPUT_-prefixed
|
|
// NC_INPUT_OWNER_HANDLE deliberately unset → omitted, not ''
|
|
});
|
|
expect(inputs).toEqual({ bot_token: 'xoxb-fake' });
|
|
});
|
|
|
|
it('errors on an uppercase collision instead of silently merging', () => {
|
|
const md = [
|
|
'```nc:prompt bot_token',
|
|
'Token?',
|
|
'```',
|
|
'```nc:prompt Bot_Token',
|
|
'Token again?',
|
|
'```',
|
|
].join('\n');
|
|
expect(() => inputsFromEnv(md, {})).toThrow(/NC_INPUT_BOT_TOKEN/);
|
|
});
|
|
|
|
// The round-trip proof for one real skill: env → inputsFromEnv → applySkill
|
|
// goes fully green for add-slack (webhook leg) with stubbed exec — the exact
|
|
// pipeline-consumer path the seam doc's §6 contract describes.
|
|
it('round-trips the env convention through a full programmatic apply of add-slack', async () => {
|
|
const skillDir = join(process.cwd(), '.claude/skills/add-slack');
|
|
const md = readFileSync(join(skillDir, 'SKILL.md'), 'utf8');
|
|
const inputs = inputsFromEnv(md, {
|
|
NC_INPUT_CONNECTION: 'webhook',
|
|
NC_INPUT_BOT_TOKEN: 'xoxb-fake-token',
|
|
NC_INPUT_SIGNING_SECRET: '0123456789abcdef',
|
|
NC_INPUT_OWNER_HANDLE: 'U12345678',
|
|
});
|
|
expect(inputs).toEqual({
|
|
connection: 'webhook',
|
|
bot_token: 'xoxb-fake-token',
|
|
signing_secret: '0123456789abcdef',
|
|
owner_handle: 'U12345678',
|
|
});
|
|
|
|
const root = mkdtempSync(join(tmpdir(), 'skill-inputs-'));
|
|
try {
|
|
mkdirSync(join(root, 'src/channels'), { recursive: true });
|
|
writeFileSync(join(root, 'src/channels/index.ts'), '// barrel\n');
|
|
writeFileSync(join(root, '.env'), '');
|
|
writeFileSync(join(root, 'package.json'), '{"name":"scratch"}\n');
|
|
|
|
const res = await applySkill(skillDir, root, {
|
|
inputs,
|
|
exec: (c) => {
|
|
if (c.includes('auth.test')) return '@nano in Acme';
|
|
if (c.includes('conversations.open')) return 'slack:D0FAKE';
|
|
},
|
|
resolveRemote: () => 'origin',
|
|
});
|
|
expect(res.deferred).toEqual([]);
|
|
expect(res.agentTasks).toEqual([]);
|
|
expect(fullyApplied(res)).toBe(true);
|
|
expect(res.vars.platform_id).toBe('slack:D0FAKE');
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|