@skill-test/vitest 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,29 +6,51 @@ deterministic checks. Built on
6
6
  [`@skill-test/sdk`](../../sdks/typescript/README.md) — the SDK's API is
7
7
  re-exported here, so a vitest suite needs only this one dependency.
8
8
 
9
- ## As code
9
+ ## Define the whole case in code (recommended)
10
+
11
+ Build the case — skill, input, evals, an optional simulated user, mocks — right
12
+ in the test, and register it in one line with `skillTest`. Everything the YAML
13
+ carries has a typed builder:
10
14
 
11
15
  ```ts
12
- import { runSkill, assistantText } from "@skill-test/vitest";
13
-
14
- test("greeter names the patient", async () => {
15
- const report = await runSkill("cases/greet.yaml", {
16
- platforms: ["claude-code"],
17
- models: ["claude-opus-4-8"],
18
- });
19
- expect(report.passed, report.runs[0]?.case).toBe(true);
20
- expect(assistantText(report.runs[0]!.transcript)).toContain("Dr. Smith");
21
- });
16
+ import { skillTest, testCase, boolean, numeric } from "@skill-test/vitest";
17
+
18
+ skillTest(
19
+ "greeter names the patient",
20
+ testCase({
21
+ skill: "skills/greeter", // resolved relative to the working dir
22
+ input: "Greet Dr. Smith, who has an appointment today.",
23
+ evals: [
24
+ boolean("the reply greets Dr. Smith by name"),
25
+ numeric("how warm is the tone", { min: 0, max: 10, threshold: 7 }),
26
+ ],
27
+ }),
28
+ );
22
29
  ```
23
30
 
24
- ## One-liner
31
+ Multi-turn cases add `user(...)`; call-count checks use `called` / `notCalled`
32
+ referencing a named `stub`/`spy`. For a matrix or extra deterministic checks,
33
+ call `runSkill` from an ordinary `test` — it takes the same case object:
25
34
 
26
35
  ```ts
27
- import { skillTest } from "@skill-test/vitest";
28
-
29
- skillTest("greeter confirms the appointment", "cases/greet.yaml");
36
+ import { runSkill, testCase, boolean, assistantText } from "@skill-test/vitest";
37
+
38
+ test("greeter across the matrix", async () => {
39
+ const report = await runSkill(
40
+ testCase({ skill: "skills/greeter", input: "Greet Dr. Smith", evals: [boolean("greets by name")] }),
41
+ { platforms: ["claude-code"], models: ["claude-opus-4-8"] },
42
+ );
43
+ expect(report.passed).toBe(true);
44
+ expect(assistantText(report.runs[0]!.transcript)).toContain("Dr. Smith");
45
+ });
30
46
  ```
31
47
 
48
+ ## Or point at a YAML file
49
+
50
+ `skillTest` and `runSkill` accept a path just as well
51
+ (`skillTest("greeter", "cases/greet.yaml")`). The full field reference for both
52
+ forms is [`docs/schema.md`](../../docs/schema.md).
53
+
32
54
  ## Assert on tool use, and stream
33
55
 
34
56
  The SDK's tool-event and streaming surfaces are re-exported too. `toolCalls`
@@ -38,27 +60,33 @@ test can **short-circuit** on bad behavior:
38
60
 
39
61
  ```ts
40
62
  import { it, expect } from "vitest";
41
- import { runSkill, toolCalls, streamSkill } from "@skill-test/vitest";
63
+ import { runSkill, streamSkill, testCase, toolCalls, boolean } from "@skill-test/vitest";
64
+
65
+ const editCase = testCase({
66
+ skill: "skills/editor",
67
+ input: "Update the config and commit it.",
68
+ evals: [boolean("the change was committed")],
69
+ });
42
70
 
43
71
  it("commits without deleting", async () => {
44
- const report = await runSkill("cases/edit.skilltest.yaml");
72
+ const report = await runSkill(editCase);
45
73
  const calls = toolCalls(report.runs[0]!.transcript);
46
74
  expect(calls.some((c) => String(c.input?.command).includes("git commit"))).toBe(true);
47
75
  expect(calls.some((c) => String(c.input?.command).includes("rm -rf"))).toBe(false);
48
76
  });
49
77
 
50
78
  it("makes no network call", async () => {
51
- for await (const ev of streamSkill("cases/edit.skilltest.yaml")) {
79
+ for await (const ev of streamSkill(editCase)) {
52
80
  expect(ev.event.name).not.toBe("curl"); // break to abort early
53
81
  }
54
82
  });
55
83
  ```
56
84
 
57
- ## Recommended: auto-discover a tree of cases
85
+ ## Auto-discover existing YAML cases
58
86
 
59
- When vitest is your primary test runner, keep your cases as data and let one
60
- test module collect them. Name each case `*.skilltest.yaml` (or `.yml`) and add
61
- a single `skills.test.ts`:
87
+ Cases can also live as data: name each `*.skilltest.yaml` (or `.yml`) and let
88
+ one test module collect the whole tree — useful when a suite already has YAML
89
+ cases, or when non-engineers author them:
62
90
 
63
91
  ```ts
64
92
  // skills.test.ts
@@ -80,8 +108,7 @@ This is the closest vitest equivalent to pytest's auto-collection: vitest only
80
108
  collects its own test modules, so the one-line `discover()` call stands in for a
81
109
  file collector. Adding a case is then just dropping in a YAML file — no code
82
110
  change. Pass run options as the second argument (`discover("cases", { platforms:
83
- ["claude-code"] })`); for matrices or deterministic mix-in assertions, reach for
84
- `runSkill` in an ordinary `test()` instead.
111
+ ["claude-code"] })`). YAML cases and code-defined ones mix freely in one suite.
85
112
 
86
113
  ## Configuration
87
114
 
package/dist/index.d.ts CHANGED
@@ -2,23 +2,27 @@
2
2
  * `@skill-test/vitest` — run AI-skill tests and natural-language evals in vitest.
3
3
  *
4
4
  * The vitest integration on top of `@skill-test/sdk`, whose API is re-exported
5
- * here so a vitest suite needs only this one dependency:
5
+ * here so a vitest suite needs only this one dependency. Define the whole case
6
+ * in code (the recommended form) and register it in one line:
6
7
  *
7
8
  * ```ts
8
- * import { skillTest, discover, runSkill, assistantText } from "@skill-test/vitest";
9
+ * import { skillTest, testCase, boolean } from "@skill-test/vitest";
9
10
  *
10
- * skillTest("greeter names the patient", "cases/greet.yaml");
11
- * // or auto-discover a tree of *.skilltest.yaml cases:
12
- * discover("cases");
13
- *
14
- * // For matrices or deterministic mix-in checks, use the SDK API directly:
15
- * test("greeter", async () => {
16
- * const report = await runSkill("cases/greet.yaml");
17
- * expect(report.passed).toBe(true);
18
- * expect(assistantText(report.runs[0]!.transcript)).toContain("Dr. Smith");
19
- * });
11
+ * skillTest(
12
+ * "greeter names the patient",
13
+ * testCase({
14
+ * skill: "skills/greeter",
15
+ * input: "Greet Dr. Smith.",
16
+ * evals: [boolean("the reply greets Dr. Smith by name")],
17
+ * }),
18
+ * );
20
19
  * ```
21
20
  *
21
+ * Existing YAML cases stay first-class: `skillTest("greeter", "cases/greet.yaml")`
22
+ * runs a file, and `discover("cases")` auto-collects a whole tree of
23
+ * `*.skilltest.yaml` cases. For matrices or deterministic mix-in checks, call
24
+ * the SDK's `runSkill` from an ordinary `test()`.
25
+ *
22
26
  * This module (via the helpers) imports `vitest`, so only load it inside a
23
27
  * vitest run. `@skill-test/vitest/vitest` remains as an alias for the helpers.
24
28
  */
package/dist/index.js CHANGED
@@ -2,23 +2,27 @@
2
2
  * `@skill-test/vitest` — run AI-skill tests and natural-language evals in vitest.
3
3
  *
4
4
  * The vitest integration on top of `@skill-test/sdk`, whose API is re-exported
5
- * here so a vitest suite needs only this one dependency:
5
+ * here so a vitest suite needs only this one dependency. Define the whole case
6
+ * in code (the recommended form) and register it in one line:
6
7
  *
7
8
  * ```ts
8
- * import { skillTest, discover, runSkill, assistantText } from "@skill-test/vitest";
9
+ * import { skillTest, testCase, boolean } from "@skill-test/vitest";
9
10
  *
10
- * skillTest("greeter names the patient", "cases/greet.yaml");
11
- * // or auto-discover a tree of *.skilltest.yaml cases:
12
- * discover("cases");
13
- *
14
- * // For matrices or deterministic mix-in checks, use the SDK API directly:
15
- * test("greeter", async () => {
16
- * const report = await runSkill("cases/greet.yaml");
17
- * expect(report.passed).toBe(true);
18
- * expect(assistantText(report.runs[0]!.transcript)).toContain("Dr. Smith");
19
- * });
11
+ * skillTest(
12
+ * "greeter names the patient",
13
+ * testCase({
14
+ * skill: "skills/greeter",
15
+ * input: "Greet Dr. Smith.",
16
+ * evals: [boolean("the reply greets Dr. Smith by name")],
17
+ * }),
18
+ * );
20
19
  * ```
21
20
  *
21
+ * Existing YAML cases stay first-class: `skillTest("greeter", "cases/greet.yaml")`
22
+ * runs a file, and `discover("cases")` auto-collects a whole tree of
23
+ * `*.skilltest.yaml` cases. For matrices or deterministic mix-in checks, call
24
+ * the SDK's `runSkill` from an ordinary `test()`.
25
+ *
22
26
  * This module (via the helpers) imports `vitest`, so only load it inside a
23
27
  * vitest run. `@skill-test/vitest/vitest` remains as an alias for the helpers.
24
28
  */
package/dist/vitest.d.ts CHANGED
@@ -1,8 +1,13 @@
1
- import { type RunOptions } from "@skill-test/sdk";
1
+ import { type RunOptions, type TestCaseInput } from "@skill-test/sdk";
2
2
  /** Filename suffixes a case file must carry to be auto-discovered. */
3
3
  export declare const CASE_SUFFIXES: readonly [".skilltest.yaml", ".skilltest.yml"];
4
- /** Register a vitest test that runs `casePath` and asserts every eval passed. */
5
- export declare function skillTest(name: string, casePath: string, options?: RunOptions): void;
4
+ /**
5
+ * Register a vitest test that runs a case and asserts every eval passed. The
6
+ * case may be a code-defined {@link TestCaseInput} (the recommended form —
7
+ * build it with `testCase`/`boolean`/`numeric`/… re-exported here) or a path to
8
+ * a YAML file.
9
+ */
10
+ export declare function skillTest(name: string, caseInput: string | TestCaseInput, options?: RunOptions): void;
6
11
  /**
7
12
  * Recursively find every `*.skilltest.yaml` / `*.skilltest.yml` case under `dir`
8
13
  * and register each as a vitest test (named by its path relative to `dir`).
package/dist/vitest.js CHANGED
@@ -19,10 +19,15 @@ import { describeFailures, runSkill } from "@skill-test/sdk";
19
19
  import { expect, test } from "vitest";
20
20
  /** Filename suffixes a case file must carry to be auto-discovered. */
21
21
  export const CASE_SUFFIXES = [".skilltest.yaml", ".skilltest.yml"];
22
- /** Register a vitest test that runs `casePath` and asserts every eval passed. */
23
- export function skillTest(name, casePath, options = {}) {
22
+ /**
23
+ * Register a vitest test that runs a case and asserts every eval passed. The
24
+ * case may be a code-defined {@link TestCaseInput} (the recommended form —
25
+ * build it with `testCase`/`boolean`/`numeric`/… re-exported here) or a path to
26
+ * a YAML file.
27
+ */
28
+ export function skillTest(name, caseInput, options = {}) {
24
29
  test(name, async () => {
25
- const report = await runSkill(casePath, options);
30
+ const report = await runSkill(caseInput, options);
26
31
  expect(report.passed, describeFailures(report)).toBe(true);
27
32
  });
28
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-test/vitest",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "vitest integration for skilltest: register AI-skill test cases as vitest tests, built on @skill-test/sdk.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "dist"
29
29
  ],
30
30
  "dependencies": {
31
- "@skill-test/sdk": "0.5.0"
31
+ "@skill-test/sdk": "0.6.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "vitest": ">=1.0.0"