@vitronai/themis 0.1.2 → 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 +5 -0
- package/README.md +3 -0
- 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,6 +4,11 @@ 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
|
+
|
|
7
12
|
## 0.1.2 - 2026-03-24
|
|
8
13
|
|
|
9
14
|
### Changed
|
package/README.md
CHANGED
|
@@ -246,12 +246,15 @@ Short version:
|
|
|
246
246
|
- `npm run typecheck`: validates TypeScript types for Themis globals and DSL contracts.
|
|
247
247
|
- `npm run benchmark:gate`: fails when benchmark performance exceeds the configured threshold.
|
|
248
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.
|
|
249
250
|
|
|
250
251
|
## CI & Release Proof
|
|
251
252
|
|
|
252
253
|
- Compatibility job runs `npm test` on Node 18 and 20.
|
|
253
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.
|
|
254
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.
|
|
255
258
|
|
|
256
259
|
## Agent Guide
|
|
257
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;
|