SPB Git

Repository Public Research preview

cli.test.mjs 5.8 KB 91 lines javascript

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
Files main
Menu

Theme