Repository Public Research preview
1// Tests of the skill's offline CLI and of the skill package itself. Run: node --test skill-tests/cli.test.mjs2import assert from "node:assert/strict";3import { execFileSync, spawnSync } from "node:child_process";4import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";5import { tmpdir } from "node:os";6import { join } from "node:path";7import { test } from "node:test";8import { fileURLToPath } from "node:url";910const skill = fileURLToPath(new URL("../skill/claude-api-toolkit/", import.meta.url));11const cli = join(skill, "scripts/claude-api.mjs");12const run = (args, input) => spawnSync(process.execPath, [cli, ...args], { input, encoding: "utf8", env: { PATH: process.env.PATH } });13const tmp = mkdtempSync(join(tmpdir(), "skill-cli-"));1415test("SKILL.md follows the Agent Skills specification", () => {16 const md = readFileSync(join(skill, "SKILL.md"), "utf8");17 const fm = /^---\n([\s\S]*?)\n---\n/.exec(md)[1];18 const name = /^name: (.+)$/m.exec(fm)[1];19 const description = /^description: (.+)$/m.exec(fm)[1];20 assert.equal(name, "claude-api-toolkit");21 assert.match(name, /^[a-z0-9]+(-[a-z0-9]+)*$/);22 assert.ok(description.length > 50 && description.length <= 1024);23 assert.ok(md.split("\n").length < 500);24 for (const ref of md.matchAll(/`(references\/[a-z-]+\.md)`/g)) assert.ok(existsSync(join(skill, ref[1])), `${ref[1]} is referenced but missing`);25 for (const f of readdirSync(join(skill, "references"))) assert.ok(md.includes(`references/${f}`) || readFileSync(join(skill, "README.md"), "utf8").includes(f), `${f} is not referenced`);26});2728test("the skill engine is byte-identical to the MCP package engine", () => {29 for (const f of ["lint.mjs", "cost.mjs", "scaffold.mjs", "knowledge.mjs", "guides.mjs"]) assert.equal(readFileSync(join(skill, "scripts/lib", f), "utf8"), readFileSync(new URL(`../mcp/lib/${f}`, import.meta.url), "utf8"), f);30 assert.equal(readFileSync(join(skill, "scripts/knowledge.json"), "utf8"), readFileSync(new URL("../mcp/knowledge.json", import.meta.url), "utf8"));31});3233test("scripts use no network, no dependencies and no credentials", () => {34 for (const f of ["scripts/claude-api.mjs", ...readdirSync(join(skill, "scripts/lib")).map((x) => `scripts/lib/${x}`)]) {35 const src = readFileSync(join(skill, f), "utf8");36 // Only the module's own header imports count; scaffold templates contain import lines as text.37 const header = src.split(/\n(?!import |\/\/|\/\*| \*|#!)/)[0];38 const imports = [...header.matchAll(/^import .* from "([^"]+)";$/gm)].map((m) => m[1]);39 for (const i of imports) assert.ok(i.startsWith("node:") || i.startsWith("./"), `${f} imports ${i}`);40 assert.ok(!/\bfetch\(|https\.request|net\.connect|process\.env\.ANTHROPIC/.test(src), `${f} touches network or credentials`);41 }42});4344test("lint, fix and migrate", () => {45 const bad = JSON.stringify({ model: "claude-opus-5-5", max_tokens: 100, temperature: 0, thinking: { type: "enabled", budget_tokens: 2000 }, messages: [{ role: "user", content: "hi" }] });46 const l = run(["lint", "-"], bad);47 assert.equal(l.status, 2);48 assert.deepEqual(JSON.parse(l.stdout).findings.filter((f) => f.severity === "error").map((f) => f.code).sort(), ["sampling-param-rejected", "thinking-budget-unsupported"]);49 const file = join(tmp, "req.json");50 writeFileSync(file, bad);51 const fixed = JSON.parse(run(["fix", file]).stdout);52 assert.equal(fixed.remaining.valid, true);53 writeFileSync(join(tmp, "fixed.json"), JSON.stringify(fixed.request));54 assert.equal(run(["lint", join(tmp, "fixed.json")]).status, 0);55 const mig = JSON.parse(run(["migrate", file, "claude-haiku-4-5"]).stdout);56 assert.equal(mig.to, "claude-haiku-4-5-20251001");57 assert.equal(JSON.parse(run(["lint", "-", "--api-version", "2023-06-01"], JSON.stringify({ model: "claude-haiku-4-5", max_tokens: 10, temperature: 0.1, messages: [{ role: "user", content: "x" }] })).stdout).valid, true);58 assert.equal(run(["lint", "/nonexistent.json"]).status, 1);59 assert.equal(run(["lint", "-"], "{not json").status, 1);60});6162test("models, cost, compare, guide", () => {63 assert.equal(JSON.parse(run(["models"]).stdout).models.length, 15);64 assert.equal(JSON.parse(run(["models", "claude-sonnet-5-5"]).stdout).thinking.between_tools, true);65 assert.equal(run(["models", "gpt-5"]).status, 1);66 assert.equal(JSON.parse(run(["cost", "claude-opus-5-5", "1000000", "1000000"]).stdout).usd_total, 24);67 assert.equal(JSON.parse(run(["cost", "claude-opus-5-5", "1000000", "1000000", "--batch"]).stdout).usd_total, 12);68 assert.equal(run(["cost", "claude-opus-5-5", "-1", "5"]).status, 1);69 assert.equal(JSON.parse(run(["compare", "1000", "1000"]).stdout)[0].model, "claude-haiku-5-5");70 assert.match(run(["guide", "thinking"]).stdout, /adaptive/);71 assert.equal(run(["guide", "nope"]).status, 1);72 assert.match(run([]).stderr, /claude-api\.mjs lint/);73});7475test("scaffold writes a file and refuses to overwrite", () => {76 const out = join(tmp, "agent.py");77 assert.equal(run(["scaffold", "manual_loop", "--language", "python", "--model", "claude-opus-5-5", "--out", out]).status, 0);78 assert.match(readFileSync(out, "utf8"), /tool_result/);79 assert.equal(run(["scaffold", "manual_loop", "--out", out]).status, 1);80 assert.equal(run(["scaffold", "nope"]).status, 1);81 execFileSync("python3", ["-c", `import ast,sys;ast.parse(open(${JSON.stringify(out)}).read())`]);82});8384test("generated references state their source and cover every model", () => {85 const rules = readFileSync(join(skill, "references/request-rules.md"), "utf8");86 const models = readFileSync(join(skill, "references/models.md"), "utf8");87 for (const id of Object.keys(JSON.parse(readFileSync(join(skill, "scripts/knowledge.json"))).models)) { assert.ok(rules.includes(id), id); assert.ok(models.includes(id), id); }88 assert.match(rules, /Generated by build-skill\.mjs from live API probes/);89 assert.match(rules, /fast-mode-2026-02-01/);90});91