Vectrum
Sign In
39 lines · 1.8 KB · TypeScript
// Run with `bun test`.
import assert from "node:assert/strict";
import { test } from "node:test";
import { array, boolean, integer, object, objectSchema, oneOf, parseArgs, string, ToolError } from "./params";

const props = {
  owner: string("Owner.", { min: 1, max: 39 }),
  limit: integer("Limit.", { min: 1, max: 50 }).default(20),
  state: oneOf("State.", ["open", "closed"] as const).optional(),
  files: array("Files.", object("A file.", { path: string("Path."), binary: boolean("Binary.").default(false) })).default([]),
};

test("parses arguments, filling in defaults", () => {
  assert.deepEqual(parseArgs(props, { owner: "joni", files: [{ path: "a" }] }), {
    owner: "joni",
    limit: 20,
    state: undefined,
    files: [{ path: "a", binary: false }],
  });
  // Agents send null for "not given", and numbers as strings.
  assert.deepEqual(parseArgs(props, { owner: "joni", limit: "5", state: null }), { owner: "joni", limit: 5, state: undefined, files: [] });
});

test("refuses bad arguments with messages an agent can act on", () => {
  const fails = (args: unknown, message: RegExp) => assert.throws(() => parseArgs(props, args), (e) => e instanceof ToolError && message.test(e.message));
  fails({}, /owner is required/);
  fails({ owner: "" }, /owner can't be empty/);
  fails({ owner: "a", limit: 99 }, /limit must be between 1 and 50/);
  fails({ owner: "a", state: "merged" }, /state must be one of open, closed/);
  fails({ owner: "a", file: [] }, /Unknown argument file/);
  fails({ owner: "a", files: [{ path: 1 }] }, /files\[0\]\.path must be a string/);
});

test("describes itself as JSON Schema", () => {
  const schema = objectSchema(props);
  assert.deepEqual(schema.required, ["owner"]);
  assert.equal(schema.additionalProperties, false);
  assert.deepEqual((schema.properties as Record<string, { default?: unknown }>).limit.default, 20);
});