@vitronai/themis 0.1.1 → 0.1.3
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/CHANGELOG.md +12 -1
- package/README.md +7 -3
- package/benchmark-gate.json +1 -1
- package/package.json +2 -1
- package/src/migrate.js +14 -2
- package/src/runtime.js +146 -21
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,22 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.1.3 - 2026-03-24
|
|
8
|
+
|
|
9
|
+
- Added provider-heavy RTL migration fixtures (Jest + Vitest) that exercise table/@testing-library flows, timers, and context updates so the proof lane generates meaningful diff artifacts (`tests/fixtures/migration/*-provider/**`, `scripts/verify-migration-fixtures.js`).
|
|
10
|
+
- Documented and uploaded the expanded proof cases plus updated the migration/job story in the README so the release highlights the dominance claim (`README.md:256`).
|
|
11
|
+
|
|
12
|
+
## 0.1.2 - 2026-03-24
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Corrected the README visual split so the main badge remains `src/assets/themisLogo.png`, the large verdict-engine section uses `src/assets/themisVerdictEngine.png`, and the HTML report continues to use `src/assets/themisReport.png`.
|
|
17
|
+
|
|
7
18
|
## 0.1.1 - 2026-03-23
|
|
8
19
|
|
|
9
20
|
### Changed
|
|
10
21
|
|
|
11
|
-
- Updated the
|
|
22
|
+
- Updated the README visuals so the primary badge stays on `src/assets/themisLogo.png`, the verdict-engine section uses `src/assets/themisVerdictEngine.png`, and the HTML report continues to use `src/assets/themisReport.png`.
|
|
12
23
|
|
|
13
24
|
## 0.1.0 - 2026-03-23
|
|
14
25
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Themis
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="src/assets/
|
|
4
|
+
<img src="src/assets/themisLogo.png" alt="Themis logo mark" width="42" valign="middle">
|
|
5
5
|
<a href="https://github.com/vitron-ai/themis/actions/workflows/ci.yml">
|
|
6
6
|
<img src="https://img.shields.io/github/actions/workflow/status/vitron-ai/themis/ci.yml?branch=main&style=for-the-badge&label=THEMIS%20VERDICT%20PIPELINE&labelColor=111827&color=16a34a" alt="Themis verdict pipeline status" valign="middle">
|
|
7
7
|
</a>
|
|
@@ -12,7 +12,7 @@ Themis is an intent-first unit test framework for AI agents in Node.js and TypeS
|
|
|
12
12
|
It is built to be the best test loop for agent workflows: deterministic reruns, machine-readable outputs, strict phase semantics, and a branded AI verdict engine for humans.
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<img src="src/assets/
|
|
15
|
+
<img src="src/assets/themisVerdictEngine.png" alt="Themis verdict engine art" width="960">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
## Contents
|
|
@@ -63,7 +63,8 @@ Themis is built for modern Node.js and TypeScript projects:
|
|
|
63
63
|
|
|
64
64
|
## Visuals
|
|
65
65
|
|
|
66
|
-
- Real Themis brand mark: [`src/assets/
|
|
66
|
+
- Real Themis brand mark: [`src/assets/themisLogo.png`](src/assets/themisLogo.png)
|
|
67
|
+
- Verdict engine art: [`src/assets/themisVerdictEngine.png`](src/assets/themisVerdictEngine.png)
|
|
67
68
|
- HTML verdict report art: [`src/assets/themisReport.png`](src/assets/themisReport.png)
|
|
68
69
|
- Background art used by the report: [`src/assets/themisBg.png`](src/assets/themisBg.png)
|
|
69
70
|
|
|
@@ -245,12 +246,15 @@ Short version:
|
|
|
245
246
|
- `npm run typecheck`: validates TypeScript types for Themis globals and DSL contracts.
|
|
246
247
|
- `npm run benchmark:gate`: fails when benchmark performance exceeds the configured threshold.
|
|
247
248
|
- `npm run pack:check`: previews the npm publish payload.
|
|
249
|
+
- `npm run proof:migration`: migrates checked-in Jest/Vitest fixture suites and proves they run cleanly under Themis.
|
|
248
250
|
|
|
249
251
|
## CI & Release Proof
|
|
250
252
|
|
|
251
253
|
- Compatibility job runs `npm test` on Node 18 and 20.
|
|
252
254
|
- Release surface job runs `npm run typecheck`, `npm run pack:check`, the HTML + agent reports, verifies `.themis/contract-diff.json`, produces `.themis/benchmark-last.json`/`.themis/migration-proof.json`, and uploads all of the artifacts for later inspection.
|
|
253
255
|
- Perf gate job runs `npm run benchmark:gate` with `BENCH_MAX_AVG_MS=2500` to guard against regressions before publishing.
|
|
256
|
+
- Migration proof job runs `npm run proof:migration` against checked-in Jest/Vitest fixtures for basic suites, table tests, RTL/jsdom flows, timers, module mocking, and a context/provider-heavy RTL example, then uploads the resulting migration reports plus Themis run artifacts as evidence.
|
|
257
|
+
- Release `0.1.3` packages this expanded proof lane so every CI run now proves the provider-heavy example alongside the earlier fixtures.
|
|
254
258
|
|
|
255
259
|
## Agent Guide
|
|
256
260
|
|
package/benchmark-gate.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitronai/themis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Intent-first unit test framework for AI agents in Node.js and TypeScript, powered by an AI verdict engine",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vitron AI",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"typecheck": "tsc -p tsconfig.json --pretty false",
|
|
75
75
|
"benchmark": "node scripts/benchmark.js",
|
|
76
76
|
"benchmark:gate": "node scripts/benchmark-gate.js",
|
|
77
|
+
"proof:migration": "node scripts/verify-migration-fixtures.js",
|
|
77
78
|
"pack:check": "npm pack --dry-run",
|
|
78
79
|
"prepublishOnly": "npm test && npm run typecheck"
|
|
79
80
|
},
|
package/src/migrate.js
CHANGED
|
@@ -261,13 +261,25 @@ function convertMigrationSourceText(sourceText) {
|
|
|
261
261
|
{ pattern: /\.toBeCalled\s*\(/g, replacement: '.toHaveBeenCalled(' },
|
|
262
262
|
{ pattern: /\.lastCalledWith\s*\(/g, replacement: '.toHaveBeenCalledWith(' },
|
|
263
263
|
{ pattern: /\.toBeTruthy\s*\(\s*\)/g, replacement: '.toBeTruthy()' },
|
|
264
|
-
{ pattern: /\.toBeFalsy\s*\(\s*\)/g, replacement: '.toBeFalsy()' }
|
|
264
|
+
{ pattern: /\.toBeFalsy\s*\(\s*\)/g, replacement: '.toBeFalsy()' },
|
|
265
|
+
{ pattern: /\b(?:jest|vi)\.fn\s*\(/g, replacement: 'fn(' },
|
|
266
|
+
{ pattern: /\b(?:jest|vi)\.spyOn\s*\(/g, replacement: 'spyOn(' },
|
|
267
|
+
{ pattern: /\b(?:jest|vi)\.mock\s*\(/g, replacement: 'mock(' },
|
|
268
|
+
{ pattern: /\b(?:jest|vi)\.unmock\s*\(/g, replacement: 'unmock(' },
|
|
269
|
+
{ pattern: /\b(?:jest|vi)\.clearAllMocks\s*\(/g, replacement: 'clearAllMocks(' },
|
|
270
|
+
{ pattern: /\b(?:jest|vi)\.resetAllMocks\s*\(/g, replacement: 'resetAllMocks(' },
|
|
271
|
+
{ pattern: /\b(?:jest|vi)\.restoreAllMocks\s*\(/g, replacement: 'restoreAllMocks(' },
|
|
272
|
+
{ pattern: /\b(?:jest|vi)\.useFakeTimers\s*\(/g, replacement: 'useFakeTimers(' },
|
|
273
|
+
{ pattern: /\b(?:jest|vi)\.useRealTimers\s*\(/g, replacement: 'useRealTimers(' },
|
|
274
|
+
{ pattern: /\b(?:jest|vi)\.advanceTimersByTime\s*\(/g, replacement: 'advanceTimersByTime(' },
|
|
275
|
+
{ pattern: /\b(?:jest|vi)\.runAllTimers\s*\(/g, replacement: 'runAllTimers(' },
|
|
276
|
+
{ pattern: /\b(?:jest|vi)\.resetModules\s*\(/g, replacement: 'resetModules(' }
|
|
265
277
|
];
|
|
266
278
|
|
|
267
279
|
for (const entry of replacements) {
|
|
268
280
|
source = source.replace(entry.pattern, () => {
|
|
269
281
|
convertedAssertions += 1;
|
|
270
|
-
return entry.replacement;
|
|
282
|
+
return typeof entry.replacement === 'function' ? entry.replacement() : entry.replacement;
|
|
271
283
|
});
|
|
272
284
|
}
|
|
273
285
|
|
package/src/runtime.js
CHANGED
|
@@ -24,6 +24,7 @@ function createSuite(name, parent = null) {
|
|
|
24
24
|
return {
|
|
25
25
|
name,
|
|
26
26
|
parent,
|
|
27
|
+
skipped: false,
|
|
27
28
|
suites: [],
|
|
28
29
|
tests: [],
|
|
29
30
|
hooks: {
|
|
@@ -140,27 +141,17 @@ function collectAndRun(filePath, options = {}) {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
function buildRuntimeApi({ root, options, testUtils, runtimeExpect, getCurrentSuite, setCurrentSuite }) {
|
|
144
|
+
const describeApi = createDescribeApi({
|
|
145
|
+
getCurrentSuite,
|
|
146
|
+
setCurrentSuite
|
|
147
|
+
});
|
|
148
|
+
const testApi = createTestApi({
|
|
149
|
+
getCurrentSuite
|
|
150
|
+
});
|
|
151
|
+
|
|
143
152
|
return {
|
|
144
|
-
describe
|
|
145
|
-
|
|
146
|
-
throw new Error(`describe(${name}) requires a callback`);
|
|
147
|
-
}
|
|
148
|
-
const suite = createSuite(name, getCurrentSuite());
|
|
149
|
-
getCurrentSuite().suites.push(suite);
|
|
150
|
-
const parent = getCurrentSuite();
|
|
151
|
-
setCurrentSuite(suite);
|
|
152
|
-
try {
|
|
153
|
-
fn();
|
|
154
|
-
} finally {
|
|
155
|
-
setCurrentSuite(parent);
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
test(name, fn) {
|
|
159
|
-
if (typeof fn !== 'function') {
|
|
160
|
-
throw new Error(`test(${name}) requires a callback`);
|
|
161
|
-
}
|
|
162
|
-
getCurrentSuite().tests.push({ name, fn });
|
|
163
|
-
},
|
|
153
|
+
describe: describeApi,
|
|
154
|
+
test: testApi,
|
|
164
155
|
intent(name, define) {
|
|
165
156
|
if (typeof define !== 'function') {
|
|
166
157
|
throw new Error(`intent(${name}) requires a callback`);
|
|
@@ -183,10 +174,121 @@ function buildRuntimeApi({ root, options, testUtils, runtimeExpect, getCurrentSu
|
|
|
183
174
|
getCurrentSuite().hooks.afterAll.push(fn);
|
|
184
175
|
},
|
|
185
176
|
expect: runtimeExpect,
|
|
177
|
+
resetModules() {
|
|
178
|
+
if (testUtils && typeof testUtils.resetAllMocks === 'function') {
|
|
179
|
+
testUtils.resetAllMocks();
|
|
180
|
+
}
|
|
181
|
+
},
|
|
186
182
|
...testUtils
|
|
187
183
|
};
|
|
188
184
|
}
|
|
189
185
|
|
|
186
|
+
function createDescribeApi({ getCurrentSuite, setCurrentSuite }) {
|
|
187
|
+
const describeApi = (name, fn) => {
|
|
188
|
+
if (typeof fn !== 'function') {
|
|
189
|
+
throw new Error(`describe(${name}) requires a callback`);
|
|
190
|
+
}
|
|
191
|
+
const suite = createSuite(name, getCurrentSuite());
|
|
192
|
+
getCurrentSuite().suites.push(suite);
|
|
193
|
+
const parent = getCurrentSuite();
|
|
194
|
+
setCurrentSuite(suite);
|
|
195
|
+
try {
|
|
196
|
+
fn();
|
|
197
|
+
} finally {
|
|
198
|
+
setCurrentSuite(parent);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
describeApi.only = describeApi;
|
|
203
|
+
describeApi.skip = (name, fn) => {
|
|
204
|
+
if (typeof fn !== 'function') {
|
|
205
|
+
throw new Error(`describe.skip(${name}) requires a callback`);
|
|
206
|
+
}
|
|
207
|
+
const suite = createSuite(name, getCurrentSuite());
|
|
208
|
+
suite.skipped = true;
|
|
209
|
+
getCurrentSuite().suites.push(suite);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return wrapEachRunner(describeApi, 'describe.each', (row, index, name, fn) => {
|
|
213
|
+
const args = normalizeEachArgs(row);
|
|
214
|
+
describeApi(formatParameterizedName(name, args, index), () => fn(...args));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function createTestApi({ getCurrentSuite }) {
|
|
219
|
+
const addTest = (name, fn, options = {}) => {
|
|
220
|
+
if (typeof fn !== 'function') {
|
|
221
|
+
throw new Error(`test(${name}) requires a callback`);
|
|
222
|
+
}
|
|
223
|
+
getCurrentSuite().tests.push({
|
|
224
|
+
name,
|
|
225
|
+
fn,
|
|
226
|
+
skipped: Boolean(options.skipped)
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const testApi = (name, fn) => {
|
|
231
|
+
addTest(name, fn);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
testApi.only = testApi;
|
|
235
|
+
testApi.skip = (name, fn = async () => {}) => {
|
|
236
|
+
addTest(name, typeof fn === 'function' ? fn : async () => {}, { skipped: true });
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return wrapEachRunner(testApi, 'test.each', (row, index, name, fn) => {
|
|
240
|
+
const args = normalizeEachArgs(row);
|
|
241
|
+
addTest(formatParameterizedName(name, args, index), () => fn(...args));
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function wrapEachRunner(target, apiName, registerRow) {
|
|
246
|
+
target.each = (rows) => {
|
|
247
|
+
if (!Array.isArray(rows)) {
|
|
248
|
+
throw new Error(`${apiName}(...) requires an array of rows`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return (name, fn) => {
|
|
252
|
+
if (typeof fn !== 'function') {
|
|
253
|
+
throw new Error(`${apiName}(...)(...) requires a callback`);
|
|
254
|
+
}
|
|
255
|
+
rows.forEach((row, index) => {
|
|
256
|
+
registerRow(row, index, name, fn);
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return target;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeEachArgs(row) {
|
|
265
|
+
return Array.isArray(row) ? row : [row];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function formatParameterizedName(name, args, index) {
|
|
269
|
+
let cursor = 0;
|
|
270
|
+
const formatted = String(name || '').replace(/%[#sdifjop]/g, (token) => {
|
|
271
|
+
if (token === '%#') {
|
|
272
|
+
return String(index);
|
|
273
|
+
}
|
|
274
|
+
const value = args[cursor];
|
|
275
|
+
cursor += 1;
|
|
276
|
+
if (token === '%j' || token === '%o' || token === '%p') {
|
|
277
|
+
return stringifyParameterizedValue(value);
|
|
278
|
+
}
|
|
279
|
+
return String(value);
|
|
280
|
+
});
|
|
281
|
+
return formatted;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function stringifyParameterizedValue(value) {
|
|
285
|
+
try {
|
|
286
|
+
return JSON.stringify(value);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return String(value);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
190
292
|
function buildCompatibilityVirtualModules(bindings) {
|
|
191
293
|
return {
|
|
192
294
|
'@jest/globals': () => buildJestGlobals(bindings.api, bindings.testUtils),
|
|
@@ -273,6 +375,11 @@ function resolveSetupFiles(setupFiles, cwd) {
|
|
|
273
375
|
async function runSuite(suite, lineage, results, options) {
|
|
274
376
|
const nextLineage = suite.name === '__root__' ? lineage : [...lineage, suite];
|
|
275
377
|
|
|
378
|
+
if (suite.skipped) {
|
|
379
|
+
pushSkippedSuiteResults(suite, nextLineage, results);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
276
383
|
let beforeAllFailed = false;
|
|
277
384
|
for (const hook of suite.hooks.beforeAll) {
|
|
278
385
|
try {
|
|
@@ -293,7 +400,7 @@ async function runSuite(suite, lineage, results, options) {
|
|
|
293
400
|
let beforeEachSucceeded = false;
|
|
294
401
|
const shouldRun = shouldRunTest(testName, options);
|
|
295
402
|
|
|
296
|
-
if (!shouldRun) {
|
|
403
|
+
if (test.skipped || !shouldRun) {
|
|
297
404
|
results.push({
|
|
298
405
|
name: test.name,
|
|
299
406
|
fullName: testName,
|
|
@@ -372,6 +479,22 @@ function collectHooks(lineage, kind, reverse) {
|
|
|
372
479
|
return hooks;
|
|
373
480
|
}
|
|
374
481
|
|
|
482
|
+
function pushSkippedSuiteResults(suite, lineage, results) {
|
|
483
|
+
const nextLineage = suite.name === '__root__' ? lineage : [...lineage, suite];
|
|
484
|
+
for (const test of suite.tests) {
|
|
485
|
+
results.push({
|
|
486
|
+
name: test.name,
|
|
487
|
+
fullName: [...formatLineage(nextLineage), test.name].join(' > '),
|
|
488
|
+
status: 'skipped',
|
|
489
|
+
durationMs: 0,
|
|
490
|
+
error: null
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
for (const child of suite.suites) {
|
|
494
|
+
pushSkippedSuiteResults(child, nextLineage, results);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
375
498
|
function installGlobals(api) {
|
|
376
499
|
const names = [
|
|
377
500
|
'describe',
|
|
@@ -390,6 +513,7 @@ function installGlobals(api) {
|
|
|
390
513
|
'clearAllMocks',
|
|
391
514
|
'resetAllMocks',
|
|
392
515
|
'restoreAllMocks',
|
|
516
|
+
'resetModules',
|
|
393
517
|
'render',
|
|
394
518
|
'screen',
|
|
395
519
|
'fireEvent',
|
|
@@ -426,6 +550,7 @@ function installGlobals(api) {
|
|
|
426
550
|
global.clearAllMocks = api.clearAllMocks;
|
|
427
551
|
global.resetAllMocks = api.resetAllMocks;
|
|
428
552
|
global.restoreAllMocks = api.restoreAllMocks;
|
|
553
|
+
global.resetModules = api.resetModules;
|
|
429
554
|
global.render = api.render;
|
|
430
555
|
global.screen = api.screen;
|
|
431
556
|
global.fireEvent = api.fireEvent;
|