@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 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
 
@@ -6,5 +6,5 @@
6
6
  "workers": 4,
7
7
  "includeExternal": false
8
8
  },
9
- "maxThemisAvgMs": 1500
9
+ "maxThemisAvgMs": 1600
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitronai/themis",
3
- "version": "0.1.2",
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(name, fn) {
145
- if (typeof fn !== 'function') {
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;