@vitronai/themis 1.2.1 → 1.3.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/docs/api.md CHANGED
@@ -28,7 +28,7 @@ For machine-readable agent adoption metadata, see [`themis.ai.json`](../themis.a
28
28
  themis test [options]
29
29
  themis init [--agents]
30
30
  themis generate [path]
31
- themis migrate <jest|vitest>
31
+ themis migrate <jest|vitest|node>
32
32
  ```
33
33
 
34
34
  ## `themis init`
@@ -203,7 +203,7 @@ Migration options:
203
203
  | `--reporter spec\|next\|json\|agent\|html` | string | Explicit reporter override. |
204
204
  | `--workers <N>` | positive integer | Override worker count. Invalid values fail fast. |
205
205
  | `--environment node\|jsdom` | string | Override the configured test environment. |
206
- | `--isolation worker\|in-process` | string | Select worker isolation or a zero IPC in-process execution mode. |
206
+ | `--isolation worker\|in-process\|process` | string | Select isolation model. `worker` (default) = worker thread per file. `in-process` = sequential in the parent (fastest reruns; shares ESM cache + process state across files). `process` = `child_process.fork` per file, mirroring `node --test` (use when tests mutate `process.env`/`process.cwd()` at module load). |
207
207
  | `--cache` | flag | Enable file-level result caching for in-process local loops. |
208
208
  | `--update-contracts` | flag | Accept updated `captureContract(...)` baselines for the selected tests. |
209
209
  | `-w`, `--watch` | flag | Rerun the selected suite when watched project files change. |
@@ -219,7 +219,7 @@ Migration compatibility:
219
219
  - imports from `@jest/globals` are supported at runtime
220
220
  - imports from `vitest` are supported at runtime
221
221
  - imports from `@testing-library/react` are supported via Themis `render`, `screen`, `fireEvent`, `waitFor`, `cleanup`, and `act`
222
- - `themis migrate <jest|vitest>` also emits `.themis/migration/migration-report.json` with detected files, migration mode details, assistant findings, and recommended next actions
222
+ - `themis migrate <jest|vitest|node>` also emits `.themis/migration/migration-report.json` with detected files, migration mode details, assistant findings, and recommended next actions
223
223
 
224
224
  Additional option:
225
225
 
@@ -229,6 +229,7 @@ Execution note:
229
229
 
230
230
  - `--watch --isolation in-process --cache` is the fastest local rerun mode
231
231
  - `--isolation worker` remains the safer mode for CI and global-heavy suites
232
+ - `--isolation process` is required for tests that mutate `process.env`/`process.cwd()` at module load (matches `node --test`'s isolation model)
232
233
  - `--watch` is intended for short edit-run-review loops for both humans and AI agents
233
234
 
234
235
  Snapshot note:
package/docs/migration.md CHANGED
@@ -1,4 +1,4 @@
1
- # Migrating From Jest And Vitest
1
+ # Migrating From Jest, Vitest, And node:test
2
2
 
3
3
  Themis is designed for incremental migration. Start by running existing suites under the Themis runtime, then convert touched tests toward native contracts and `intent(...)` flows as you work.
4
4
 
@@ -12,14 +12,42 @@ npx themis migrate jest --assist
12
12
  npx themis test
13
13
  ```
14
14
 
15
- Use `vitest` instead of `jest` for Vitest suites.
15
+ Use `vitest` for Vitest suites or `node` for `node:test` suites in place of `jest`.
16
16
 
17
17
  ## Migration modes
18
18
 
19
- - `themis migrate <jest|vitest>`: scaffold config, setup, compat bridge, and migration report.
20
- - `--rewrite-imports`: point framework imports at `themis.compat.js`.
21
- - `--convert`: remove common Jest/Vitest imports and rewrite common matcher/test patterns into Themis-native forms.
22
- - `--assist`: run the safe rewrite and conversion passes together, then report leftover Jest/Vitest-only helpers that still need manual follow-up.
19
+ - `themis migrate <jest|vitest|node>`: scaffold config and migration report. For `jest`/`vitest`, also writes a setup file and a compat bridge; `node` skips both (Themis provides the same globals natively).
20
+ - `--rewrite-imports`: point framework imports at `themis.compat.js` (jest/vitest only — `node` source has no compat shim, conversion is direct).
21
+ - `--convert`: remove common framework imports and rewrite matcher/test patterns into Themis-native forms.
22
+ - `--assist`: run the safe rewrite and conversion passes together, then report leftover framework-specific helpers that still need manual follow-up.
23
+
24
+ ## node:test specifics
25
+
26
+ `themis migrate node` handles the following transforms:
27
+
28
+ | Input (node:test + node:assert/strict) | Output (Themis) |
29
+ | --- | --- |
30
+ | `import test from 'node:test'` | dropped (`test` is a Themis global) |
31
+ | `import assert from 'node:assert/strict'` | dropped (`expect` replaces all asserts) |
32
+ | `assert.equal(a, b)` / `strictEqual` | `expect(a).toBe(b)` |
33
+ | `assert.deepEqual(a, b)` / `deepStrictEqual` | `expect(a).toEqual(b)` |
34
+ | `assert.ok(v)` | `expect(v).toBeTruthy()` |
35
+ | `assert.match(s, /re/)` | `expect(s).toMatch(/re/)` |
36
+ | `await assert.rejects(fn, /re/)` | async try/catch wrapper + `toMatch` on the error message |
37
+ | `test.after(fn)` / `test.afterEach(fn)` | `afterAll(fn)` / `afterEach(fn)` |
38
+ | `test(name, { timeout }, fn)` | `test(name, fn)` (options arg silently dropped) |
39
+
40
+ Not supported in this pass: `t.test()` subtests, `t.context`, `test.only`, `describe`/`it` exported from `node:test` (use Themis globals instead), `assert.throws`/`notEqual`/`fail`/`doesNotReject`, source-map line preservation. The optional 3rd-arg message string on `assert.equal`-family calls is silently dropped.
41
+
42
+ ## Process-state isolation
43
+
44
+ `node:test` runs each test file in its own child process. If your suite mutates `process.env`, `process.cwd()`, or other process-level state at module load (e.g. `process.env.HOME = mkdtempSync(...)` before `await import('../dist/index.js')`), pair `themis test` with per-file process isolation:
45
+
46
+ ```bash
47
+ npx themis test --isolation process
48
+ ```
49
+
50
+ This spawns a fresh Node child process per test file via `child_process.fork`, mirroring `node --test`'s isolation model. The default `worker` mode shares process-state (especially `os.homedir()` cached at worker startup) across files and will surface as cross-file leakage for state-mutating tests.
23
51
 
24
52
  ## Before And After
25
53
 
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "source": {
14
14
  "type": "string",
15
- "enum": ["jest", "vitest"]
15
+ "enum": ["jest", "vitest", "node"]
16
16
  },
17
17
  "createdAt": {
18
18
  "type": "string"
@@ -35,6 +35,8 @@
35
35
  "jestGlobals",
36
36
  "vitest",
37
37
  "testingLibraryReact",
38
+ "nodeTest",
39
+ "nodeAssert",
38
40
  "rewrittenFiles",
39
41
  "rewrittenImports",
40
42
  "convertedFiles",
@@ -50,6 +52,8 @@
50
52
  "jestGlobals": { "type": "number" },
51
53
  "vitest": { "type": "number" },
52
54
  "testingLibraryReact": { "type": "number" },
55
+ "nodeTest": { "type": "number" },
56
+ "nodeAssert": { "type": "number" },
53
57
  "rewrittenFiles": { "type": "number" },
54
58
  "rewrittenImports": { "type": "number" },
55
59
  "convertedFiles": { "type": "number" },
@@ -0,0 +1,230 @@
1
+ # Testing With Claude Code and Themis
2
+
3
+ A step-by-step walkthrough showing how Themis turns Claude Code into a test-writing machine that gets it right on the first try.
4
+
5
+ ## The Problem
6
+
7
+ When you ask Claude Code to write unit tests, it reaches for Jest or Vitest by default. The tests it generates are often correct, but just as often they have subtle issues: wrong import paths, misused mocking APIs, snapshot tests where assertions would be better, setup files where the framework handles things natively. You end up in an edit-test-fix loop that burns time and context window.
8
+
9
+ Themis fixes this by shipping structured guidance directly to Claude Code — a skill, slash commands, and a `CLAUDE.md` that tells Claude exactly how to write, run, and fix tests. No copy-pasting docs. No explaining the framework. Claude just knows.
10
+
11
+ ## What You'll See
12
+
13
+ By the end of this tutorial you'll have:
14
+
15
+ 1. A Node.js project with Themis installed and Claude Code fully wired up
16
+ 2. Generated tests that pass on the first run
17
+ 3. A structured failure-fix loop where Claude reads machine-parseable repair hints instead of raw stack traces
18
+ 4. Slash commands (`/themis-test`, `/themis-generate`, `/themis-fix`) that work out of the box
19
+
20
+ ## Step 1: Set Up a Project
21
+
22
+ Start with any Node.js or TypeScript project. For this tutorial we'll use a small utility library.
23
+
24
+ ```bash
25
+ mkdir demo-project && cd demo-project
26
+ npm init -y
27
+ ```
28
+
29
+ Create a source file at `src/cart.js`:
30
+
31
+ ```js
32
+ class Cart {
33
+ constructor() {
34
+ this.items = [];
35
+ }
36
+
37
+ add(item) {
38
+ if (!item || !item.name || typeof item.price !== 'number') {
39
+ throw new TypeError('Item must have a name and a numeric price');
40
+ }
41
+ const existing = this.items.find((i) => i.name === item.name);
42
+ if (existing) {
43
+ existing.quantity += item.quantity || 1;
44
+ } else {
45
+ this.items.push({ ...item, quantity: item.quantity || 1 });
46
+ }
47
+ }
48
+
49
+ remove(name) {
50
+ const index = this.items.findIndex((i) => i.name === name);
51
+ if (index === -1) throw new Error(`Item "${name}" not in cart`);
52
+ this.items.splice(index, 1);
53
+ }
54
+
55
+ total() {
56
+ return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
57
+ }
58
+
59
+ checkout(paymentMethod) {
60
+ if (this.items.length === 0) throw new Error('Cannot checkout an empty cart');
61
+ const receipt = {
62
+ items: this.items.map((i) => ({ ...i })),
63
+ total: this.total(),
64
+ paymentMethod,
65
+ timestamp: new Date().toISOString()
66
+ };
67
+ this.items = [];
68
+ return receipt;
69
+ }
70
+ }
71
+
72
+ module.exports = { Cart };
73
+ ```
74
+
75
+ ## Step 2: Install Themis With Claude Code Integration
76
+
77
+ ```bash
78
+ npm install -D @vitronai/themis@latest
79
+ npx themis init --claude-code
80
+ ```
81
+
82
+ That one command installs:
83
+
84
+ - `CLAUDE.md` — adoption rules at the repo root that Claude Code reads automatically
85
+ - `.claude/skills/themis/SKILL.md` — a skill that auto-loads when Claude sees a test-related request
86
+ - `.claude/commands/themis-test.md` — `/themis-test` slash command
87
+ - `.claude/commands/themis-generate.md` — `/themis-generate` slash command
88
+ - `.claude/commands/themis-migrate.md` — `/themis-migrate` slash command
89
+ - `.claude/commands/themis-fix.md` — `/themis-fix` slash command
90
+
91
+ You can verify:
92
+
93
+ ```bash
94
+ cat CLAUDE.md # Themis adoption rules
95
+ ls .claude/skills/ # themis/SKILL.md
96
+ ls .claude/commands/ # four slash command files
97
+ ```
98
+
99
+ ## Step 3: Generate Tests
100
+
101
+ Open Claude Code in the project and type:
102
+
103
+ ```
104
+ /themis-generate src
105
+ ```
106
+
107
+ Claude uses the installed skill context to run `npx themis generate src`. Generated tests land under `__themis__/tests/` as `.generated.test.js` files. These are deterministic, contract-style tests — not LLM-generated guesses.
108
+
109
+ ## Step 4: Run the Test Loop
110
+
111
+ ```
112
+ /themis-test
113
+ ```
114
+
115
+ This runs `npx themis test --reporter agent` and Claude reads the structured JSON output. If everything passes, you're done. If there are failures, Claude sees:
116
+
117
+ ```json
118
+ {
119
+ "failures": [
120
+ {
121
+ "cluster": "cart-checkout-validation",
122
+ "repairHints": ["checkout() throws when cart is empty — test passes an empty cart but expects success"],
123
+ "sourceFile": "src/cart.js",
124
+ "lineNumber": 32,
125
+ "expected": "Error: Cannot checkout an empty cart",
126
+ "actual": "{ items: [], total: 0 }"
127
+ }
128
+ ]
129
+ }
130
+ ```
131
+
132
+ Instead of re-reading a raw stack trace, Claude acts on the `repairHints` directly. This is the key difference: structured signals instead of unstructured error output.
133
+
134
+ ## Step 5: Ask Claude to Write More Tests
135
+
136
+ Now ask Claude to add coverage for edge cases:
137
+
138
+ ```
139
+ Write additional tests for the Cart class covering:
140
+ - adding duplicate items increments quantity
141
+ - removing a non-existent item throws
142
+ - checkout clears the cart
143
+ - total with no items returns 0
144
+ ```
145
+
146
+ Because the Themis skill is loaded, Claude will:
147
+
148
+ 1. Use `intent(...)` for behavior tests and `test(...)` for pure unit checks
149
+ 2. Follow the four-phase shape: context, run, verify, cleanup
150
+ 3. Use `expect(...)` assertions (not snapshots)
151
+ 4. Place tests alongside the generated ones, not in a random `tests/` directory
152
+
153
+ Run `/themis-test` again to verify.
154
+
155
+ ## Step 6: Fix Failures (When They Happen)
156
+
157
+ If any test fails, use:
158
+
159
+ ```
160
+ /themis-fix
161
+ ```
162
+
163
+ Claude will:
164
+
165
+ 1. Run `npx themis test --reporter agent` to get the current failures
166
+ 2. Group failures by `cluster` — fixes within a cluster share a root cause
167
+ 3. Read `repairHints` before looking at the stack trace
168
+ 4. Apply the smallest fix that addresses the root cause
169
+ 5. Re-run with `--rerun-failed` to confirm the fix without running the full suite
170
+
171
+ This cluster-based fixing is faster than fixing tests one at a time, and the `--rerun-failed` flag means you don't pay the cost of a full suite run after each fix.
172
+
173
+ ## Step 7: Optional — Wire Up the Automated Hook
174
+
175
+ For the tightest possible loop, add a PostToolUse hook that runs Themis automatically after every edit Claude makes:
176
+
177
+ Add this to `.claude/settings.json`:
178
+
179
+ ```json
180
+ {
181
+ "hooks": {
182
+ "PostToolUse": [
183
+ {
184
+ "matcher": "Edit|Write|MultiEdit",
185
+ "hooks": [
186
+ {
187
+ "type": "command",
188
+ "command": "node node_modules/@vitronai/themis/scripts/claude-hook.js"
189
+ }
190
+ ]
191
+ }
192
+ ]
193
+ }
194
+ }
195
+ ```
196
+
197
+ Now every time Claude edits a `.js`/`.ts`/`.jsx`/`.tsx` file, Themis runs automatically. If tests fail, the structured failure JSON is fed back into the conversation — Claude sees it immediately and can fix it in the next turn without you running anything.
198
+
199
+ The hook is smart about scope:
200
+
201
+ - Skips non-source edits (docs, config, etc.)
202
+ - Uses `--rerun-failed` when there's a prior failure artifact
203
+ - Exits silently when tests pass (no context noise)
204
+ - Set `THEMIS_HOOK_DISABLED=1` to pause it temporarily
205
+
206
+ ## Why This Works
207
+
208
+ The magic is not in Themis being a better test runner (though it is faster). The magic is in the **structured agent context**:
209
+
210
+ 1. **The skill** tells Claude exactly when and how to use Themis — it auto-loads without you mentioning the framework
211
+ 2. **The CLAUDE.md** provides rules about what to avoid (no setup shims, no snapshots as defaults, no ad-hoc test directories)
212
+ 3. **The `--reporter agent` output** gives Claude machine-parseable failure data with repair hints, instead of raw stack traces it has to re-parse
213
+ 4. **The slash commands** encode the correct workflow so Claude doesn't have to figure out which flags to pass
214
+
215
+ In Tessl evaluations across 10 scenarios, agents scored **37% without** the Themis skill context and **97% with it**. The context is the product.
216
+
217
+ ## What's Next
218
+
219
+ - **Migrate from Jest or Vitest**: Run `/themis-migrate` — Claude walks through the four-step incremental migration
220
+ - **Cursor users**: Run `npx themis init --cursor` to install `.cursorrules`
221
+ - **Both at once**: `npx themis init --agents --claude-code --cursor`
222
+ - **Auto-detection**: A bare `npx themis init` detects which agents are present and installs the right assets automatically
223
+
224
+ ## Links
225
+
226
+ - npm: [`@vitronai/themis`](https://www.npmjs.com/package/@vitronai/themis)
227
+ - GitHub: [vitron-ai/themis](https://github.com/vitron-ai/themis)
228
+ - Tessl tile: [vitron-ai/themis](https://tessl.io/registry/vitron-ai/themis)
229
+ - Eval results: [37% baseline → 97% with skill](https://tessl.io/eval-runs/019d72a0-8211-74ea-84ef-a8e336ead3d2)
230
+ - Adoption guide: [`docs/agents-adoption.md`](agents-adoption.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitronai/themis",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "A Node.js and TypeScript unit test framework designed for AI coding agents. Drop-in alternative to Jest and Vitest with machine-readable failure output, structured repair hints, and one-command migration.",
5
5
  "license": "MIT",
6
6
  "author": "Vitron AI",
@@ -97,6 +97,8 @@
97
97
  "benchmark:first-try": "node scripts/benchmark-first-try.js",
98
98
  "benchmark:gate": "node scripts/benchmark-gate.js",
99
99
  "proof:migration": "node scripts/verify-migration-fixtures.js",
100
+ "proof:esm": "node scripts/verify-esm-fixtures.js",
101
+ "verify:dogfood": "node scripts/verify-alethia-bridge-dogfood.js",
100
102
  "pack:check": "npm pack --dry-run",
101
103
  "prepublishOnly": "npm run lint && npm test && npm run typecheck"
102
104
  },
package/src/cli.js CHANGED
@@ -147,7 +147,11 @@ async function main(argv) {
147
147
  }
148
148
  console.log(`Report: ${formatCliPath(cwd, result.reportPath)}`);
149
149
  }
150
- console.log('Runtime compatibility is enabled for @jest/globals, vitest, and @testing-library/react imports.');
150
+ if (result.source === 'node') {
151
+ console.log('node:test and node:assert imports are dropped during conversion; Themis provides test/expect/afterAll/afterEach as globals.');
152
+ } else {
153
+ console.log('Runtime compatibility is enabled for @jest/globals, vitest, and @testing-library/react imports.');
154
+ }
151
155
  console.log('Next: run npx themis test or npm run test:themis');
152
156
  return;
153
157
  }
@@ -778,10 +782,10 @@ function validateWorkerCount(flagValue, configValue) {
778
782
  }
779
783
 
780
784
  function validateIsolation(value) {
781
- if (value === 'worker' || value === 'in-process') {
785
+ if (value === 'worker' || value === 'in-process' || value === 'process') {
782
786
  return;
783
787
  }
784
- throw new Error(`Unsupported --isolation value: ${value}. Use one of: worker, in-process.`);
788
+ throw new Error(`Unsupported --isolation value: ${value}. Use one of: worker, in-process, process.`);
785
789
  }
786
790
 
787
791
  function resolveWorkerCount(flagValue, configValue) {
@@ -814,8 +818,8 @@ function printUsage() {
814
818
  console.log(' generate [path] Scan source files and generate Themis contract tests');
815
819
  console.log(' Options: [--json] [--plan] [--output path] [--files a,b] [--match-source regex] [--match-export regex] [--scenario name] [--min-confidence level] [--require-confidence level] [--include regex] [--exclude regex] [--review] [--update] [--clean] [--changed] [--force] [--strict] [--write-hints] [--fail-on-skips] [--fail-on-conflicts]');
816
820
  console.log(' scan [path] Alias for generate');
817
- console.log(' migrate <jest|vitest> [--rewrite-imports] [--convert] [--assist] Scaffold an incremental migration bridge for existing suites');
818
- console.log(' test [--json] [--agent] [--next] [--reporter spec|next|json|agent|html] [--workers N] [--stability N] [--environment node|jsdom] [--isolation worker|in-process] [--cache] [--update-contracts] [--fix] [-w|--watch] [--html-output path] [--match regex] [--rerun-failed] [--no-memes] [--lexicon classic|themis]');
821
+ console.log(' migrate <jest|vitest|node> [--rewrite-imports] [--convert] [--assist] Scaffold an incremental migration bridge for existing suites');
822
+ console.log(' test [--json] [--agent] [--next] [--reporter spec|next|json|agent|html] [--workers N] [--stability N] [--environment node|jsdom] [--isolation worker|in-process|process] [--cache] [--update-contracts] [--fix] [-w|--watch] [--html-output path] [--match regex] [--rerun-failed] [--no-memes] [--lexicon classic|themis]');
819
823
  }
820
824
 
821
825
  function printGenerateSummary(summary, cwd) {
package/src/config.js CHANGED
@@ -5,7 +5,7 @@ const os = require('os');
5
5
  const DEFAULT_CONFIG = {
6
6
  testDir: 'tests',
7
7
  generatedTestsDir: path.join('__themis__', 'tests'),
8
- testRegex: '\\.(test|spec)\\.(js|jsx|ts|tsx)$',
8
+ testRegex: '\\.(test|spec)\\.(js|jsx|ts|tsx|mjs|cjs)$',
9
9
  maxWorkers: Math.max(1, os.cpus().length - 1),
10
10
  reporter: 'next',
11
11
  environment: 'node',
package/src/expect.js CHANGED
@@ -70,6 +70,24 @@ function createExpect(_context = {}) {
70
70
 
71
71
  throw new Error('toContain only supports strings and arrays');
72
72
  },
73
+ toMatch(expected) {
74
+ if (typeof received !== 'string') {
75
+ throw new Error(`toMatch expects a string, received ${format(received)}`);
76
+ }
77
+ if (typeof expected === 'string') {
78
+ if (!received.includes(expected)) {
79
+ throw new Error(`Expected ${format(received)} to match substring ${format(expected)}`);
80
+ }
81
+ return;
82
+ }
83
+ if (expected instanceof RegExp) {
84
+ if (!expected.test(received)) {
85
+ throw new Error(`Expected ${format(received)} to match ${String(expected)}`);
86
+ }
87
+ return;
88
+ }
89
+ throw new Error('toMatch expects a string or RegExp');
90
+ },
73
91
  toThrow(match) {
74
92
  if (typeof received !== 'function') {
75
93
  throw new Error('toThrow expects a function');