@vitronai/themis 0.1.0-beta.4 → 0.1.2
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 +17 -1
- package/README.md +42 -2
- package/docs/api.md +21 -2
- package/docs/migration.md +167 -0
- package/docs/publish.md +2 -0
- package/docs/release-checklist.md +28 -0
- package/docs/release-policy.md +11 -8
- package/docs/roadmap.md +18 -0
- package/docs/schemas/agent-result.v1.json +8 -2
- package/docs/schemas/contract-diff.v1.json +120 -0
- package/docs/showcases.md +117 -0
- package/docs/vscode-extension.md +13 -0
- package/docs/why-themis.md +34 -3
- package/globals.d.ts +1 -0
- package/index.d.ts +57 -0
- package/package.json +1 -1
- package/src/artifacts.js +63 -2
- package/src/assets/themisVerdictEngine.png +0 -0
- package/src/cli.js +16 -3
- package/src/contracts.js +330 -0
- package/src/migrate.js +104 -4
- package/src/reporter.js +79 -3
- package/src/runner.js +2 -0
- package/src/runtime.js +32 -3
- package/src/test-utils.js +13 -0
- package/src/worker.js +1 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/vitron-ai/themis/docs/schemas/contract-diff.v1.json",
|
|
4
|
+
"title": "Themis Contract Diff v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema", "runId", "createdAt", "artifacts", "summary", "items"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"const": "themis.contract.diff.v1"
|
|
12
|
+
},
|
|
13
|
+
"runId": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"createdAt": {
|
|
17
|
+
"type": "string"
|
|
18
|
+
},
|
|
19
|
+
"artifacts": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"required": ["contractDiff"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"contractDiff": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"summary": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"required": ["total", "created", "updated", "drifted", "unchanged"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"total": { "type": "number" },
|
|
35
|
+
"created": { "type": "number" },
|
|
36
|
+
"updated": { "type": "number" },
|
|
37
|
+
"drifted": { "type": "number" },
|
|
38
|
+
"unchanged": { "type": "number" }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": {
|
|
44
|
+
"$ref": "#/$defs/item"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"$defs": {
|
|
49
|
+
"item": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"required": ["key", "name", "file", "testName", "fullName", "contractFile", "status", "updateCommand", "diff"],
|
|
53
|
+
"properties": {
|
|
54
|
+
"key": { "type": "string" },
|
|
55
|
+
"name": { "type": "string" },
|
|
56
|
+
"file": { "type": "string" },
|
|
57
|
+
"testName": { "type": "string" },
|
|
58
|
+
"fullName": { "type": "string" },
|
|
59
|
+
"contractFile": { "type": "string" },
|
|
60
|
+
"status": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"enum": ["created", "updated", "drifted", "unchanged"]
|
|
63
|
+
},
|
|
64
|
+
"updateCommand": { "type": "string" },
|
|
65
|
+
"diff": {
|
|
66
|
+
"$ref": "#/$defs/diff"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"diff": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"required": ["equal", "unchangedCount", "added", "removed", "changed"],
|
|
74
|
+
"properties": {
|
|
75
|
+
"equal": { "type": "boolean" },
|
|
76
|
+
"unchangedCount": { "type": "number" },
|
|
77
|
+
"added": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": { "$ref": "#/$defs/afterEntry" }
|
|
80
|
+
},
|
|
81
|
+
"removed": {
|
|
82
|
+
"type": "array",
|
|
83
|
+
"items": { "$ref": "#/$defs/beforeEntry" }
|
|
84
|
+
},
|
|
85
|
+
"changed": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": { "$ref": "#/$defs/changeEntry" }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"afterEntry": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"additionalProperties": true,
|
|
94
|
+
"required": ["path", "after"],
|
|
95
|
+
"properties": {
|
|
96
|
+
"path": { "type": "string" },
|
|
97
|
+
"after": {}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"beforeEntry": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"additionalProperties": true,
|
|
103
|
+
"required": ["path", "before"],
|
|
104
|
+
"properties": {
|
|
105
|
+
"path": { "type": "string" },
|
|
106
|
+
"before": {}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"changeEntry": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"additionalProperties": true,
|
|
112
|
+
"required": ["path", "before", "after"],
|
|
113
|
+
"properties": {
|
|
114
|
+
"path": { "type": "string" },
|
|
115
|
+
"before": {},
|
|
116
|
+
"after": {}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Showcase Comparisons
|
|
2
|
+
|
|
3
|
+
These examples are the clearest proof of where Themis should beat Jest and Vitest for AI-agent-heavy JS/TS test loops.
|
|
4
|
+
|
|
5
|
+
## 1. Replace snapshots with contract capture
|
|
6
|
+
|
|
7
|
+
Jest/Vitest snapshot flow:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
test('banner state', () => {
|
|
11
|
+
expect(renderBanner(model)).toMatchSnapshot();
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Themis contract flow:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
test('banner state', () => {
|
|
19
|
+
captureContract('banner state', {
|
|
20
|
+
title: renderBanner(model).title,
|
|
21
|
+
actions: renderBanner(model).actions
|
|
22
|
+
}, {
|
|
23
|
+
maskPaths: ['$.requestId'],
|
|
24
|
+
sortArrays: true
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Why it wins:
|
|
30
|
+
|
|
31
|
+
- captures only the fields that matter
|
|
32
|
+
- masks volatile IDs and timestamps instead of accepting noisy churn
|
|
33
|
+
- writes machine-readable drift to `.themis/contract-diff.json`
|
|
34
|
+
- updates stay explicit with `npx themis test --update-contracts`
|
|
35
|
+
|
|
36
|
+
## 2. Migrate Jest/Vitest suites without a rewrite freeze
|
|
37
|
+
|
|
38
|
+
Starting suite:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { describe, it, expect } from '@jest/globals';
|
|
42
|
+
|
|
43
|
+
describe('worker', () => {
|
|
44
|
+
it('records calls', () => {
|
|
45
|
+
const fnRef = fn();
|
|
46
|
+
fnRef('ok');
|
|
47
|
+
expect(fnRef).toBeCalledTimes(1);
|
|
48
|
+
expect({ status: 'ok' }).toStrictEqual({ status: 'ok' });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
After `npx themis migrate jest --convert`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
describe('worker', () => {
|
|
57
|
+
test('records calls', () => {
|
|
58
|
+
const fnRef = fn();
|
|
59
|
+
fnRef('ok');
|
|
60
|
+
expect(fnRef).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect({ status: 'ok' }).toEqual({ status: 'ok' });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Why it wins:
|
|
67
|
+
|
|
68
|
+
- removes common compatibility imports
|
|
69
|
+
- rewrites common matcher aliases into native Themis forms
|
|
70
|
+
- emits `.themis/migration-report.json` so agents and humans can track what changed
|
|
71
|
+
|
|
72
|
+
## 3. Give agents a repair loop humans can still read
|
|
73
|
+
|
|
74
|
+
Jest/Vitest mostly produce console text plus optional snapshots.
|
|
75
|
+
|
|
76
|
+
Themis produces:
|
|
77
|
+
|
|
78
|
+
- `.themis/failed-tests.json`
|
|
79
|
+
- `.themis/run-diff.json`
|
|
80
|
+
- `.themis/fix-handoff.json`
|
|
81
|
+
- `.themis/contract-diff.json`
|
|
82
|
+
|
|
83
|
+
Why it wins:
|
|
84
|
+
|
|
85
|
+
- agents get deterministic machine-readable artifacts
|
|
86
|
+
- humans still get focused CLI and HTML diffs
|
|
87
|
+
- rerun, repair, and acceptance loops are explicit instead of buried in terminal logs
|
|
88
|
+
|
|
89
|
+
## 4. Keep local loops fast without losing determinism
|
|
90
|
+
|
|
91
|
+
Themis local loop:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx themis test --watch --isolation in-process --cache --reporter next
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Why it wins:
|
|
98
|
+
|
|
99
|
+
- optimized for short edit-rerun-review cycles
|
|
100
|
+
- preserves canonical JSON and artifact contracts
|
|
101
|
+
- keeps failure diffs and contract drift visible while iterating
|
|
102
|
+
|
|
103
|
+
## 5. Surface migration and contract work directly in VS Code
|
|
104
|
+
|
|
105
|
+
Themis sidebar groups:
|
|
106
|
+
|
|
107
|
+
- `Contract Review`
|
|
108
|
+
- `Migration Review`
|
|
109
|
+
- `Failures`
|
|
110
|
+
- `Generated Review`
|
|
111
|
+
|
|
112
|
+
Why it wins:
|
|
113
|
+
|
|
114
|
+
- the editor reads artifact contracts instead of reimplementing runner logic
|
|
115
|
+
- one action accepts reviewed contracts
|
|
116
|
+
- one action reruns migration codemods for the detected framework
|
|
117
|
+
- humans and agents operate from the same source of truth
|
package/docs/vscode-extension.md
CHANGED
|
@@ -7,8 +7,10 @@ This is the intended shape of the editor UX:
|
|
|
7
7
|
- a Themis activity-bar container
|
|
8
8
|
- a results sidebar driven by `.themis/*` artifacts
|
|
9
9
|
- commands to run tests, rerun failures, refresh results, and open the HTML report
|
|
10
|
+
- commands to accept reviewed contract baselines and rerun migration codemods
|
|
10
11
|
- failure navigation that jumps from artifact data into the source file
|
|
11
12
|
- generated-review navigation for source files, generated tests, hint sidecars, and backlog items
|
|
13
|
+
- contract-review and migration-review groups driven by `.themis/contract-diff.json` and `.themis/migration-report.json`
|
|
12
14
|
|
|
13
15
|
## Current MVP Scope
|
|
14
16
|
|
|
@@ -17,6 +19,8 @@ The scaffold currently supports:
|
|
|
17
19
|
- reading `.themis/last-run.json`
|
|
18
20
|
- reading `.themis/failed-tests.json`
|
|
19
21
|
- reading `.themis/run-diff.json`
|
|
22
|
+
- reading `.themis/contract-diff.json`
|
|
23
|
+
- reading `.themis/migration-report.json`
|
|
20
24
|
- reading `.themis/generate-last.json`
|
|
21
25
|
- reading `.themis/generate-map.json`
|
|
22
26
|
- reading `.themis/generate-backlog.json`
|
|
@@ -27,6 +31,15 @@ The scaffold currently supports:
|
|
|
27
31
|
|
|
28
32
|
The extension is intentionally thin. It shells out to Themis commands and treats the CLI plus artifacts as the canonical contract.
|
|
29
33
|
|
|
34
|
+
Current command surface:
|
|
35
|
+
|
|
36
|
+
- `Themis: Run Tests`
|
|
37
|
+
- `Themis: Rerun Failed`
|
|
38
|
+
- `Themis: Update Contracts`
|
|
39
|
+
- `Themis: Run Migration Codemods`
|
|
40
|
+
- `Themis: Open HTML Report`
|
|
41
|
+
- `Themis: Refresh Results`
|
|
42
|
+
|
|
30
43
|
## Local Development
|
|
31
44
|
|
|
32
45
|
Open the repository in VS Code and use an Extension Development Host pointed at:
|
package/docs/why-themis.md
CHANGED
|
@@ -6,6 +6,7 @@ The core positioning is simple:
|
|
|
6
6
|
|
|
7
7
|
- The best unit test framework for AI agents in Node.js and TypeScript
|
|
8
8
|
- An AI verdict engine for human and agent review loops
|
|
9
|
+
- A contract-first alternative to snapshot-heavy test maintenance
|
|
9
10
|
|
|
10
11
|
## What "Next-Gen" Means Here
|
|
11
12
|
|
|
@@ -42,6 +43,8 @@ Themis supports structured outputs for tooling loops:
|
|
|
42
43
|
|
|
43
44
|
`--rerun-failed`, `--watch`, and test-name filtering (`--match`) reduce iteration time and keep failure focus tight.
|
|
44
45
|
|
|
46
|
+
The fast local loop matters for AI-assisted editing too: `--watch --isolation in-process --cache` gives agents and humans a short edit-run-review cycle instead of full-suite friction.
|
|
47
|
+
|
|
45
48
|
## 4) Modern JS/TS Project Parity
|
|
46
49
|
|
|
47
50
|
Themis is built for current Node.js and TypeScript repos:
|
|
@@ -69,11 +72,38 @@ Themis keeps a lean JS runtime and ships first-party typings:
|
|
|
69
72
|
Themis ships workflow features agents can use directly:
|
|
70
73
|
|
|
71
74
|
- direct contract assertions instead of snapshot-file churn
|
|
75
|
+
- generated contract tests that keep baselines in readable source instead of opaque snapshot blobs
|
|
76
|
+
- machine-readable artifacts that let agents inspect and explain changes before updating tests
|
|
72
77
|
- mocks and spies with `fn`, `spyOn`, and `mock`
|
|
73
78
|
- `.themis/run-diff.json` and `.themis/run-history.json`
|
|
74
79
|
- HTML verdict reports for human review
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
### Comparable To Snapshot Workflows, Without Snapshot Rot
|
|
82
|
+
|
|
83
|
+
Many teams use snapshots because they want cheap baseline capture, safe review, and easy updates. Themis should meet that need without copying the snapshot mechanism.
|
|
84
|
+
|
|
85
|
+
Themis favors:
|
|
86
|
+
|
|
87
|
+
- normalized contract assertions over broad serialized dumps
|
|
88
|
+
- readable generated tests over hidden `.snap` files
|
|
89
|
+
- field-level and behavior-level diffs over large text churn
|
|
90
|
+
- intentional regeneration and migration flows over blanket "accept all changes"
|
|
91
|
+
|
|
92
|
+
The result is comparable coverage value with clearer semantics for both humans and agents.
|
|
93
|
+
|
|
94
|
+
## 8) Migration Path From Jest And Vitest
|
|
95
|
+
|
|
96
|
+
Adoption does not need to be a rewrite.
|
|
97
|
+
|
|
98
|
+
Themis already supports:
|
|
99
|
+
|
|
100
|
+
- runtime compatibility for `@jest/globals`, `vitest`, and `@testing-library/react`
|
|
101
|
+
- `themis migrate <jest|vitest>` scaffolding for incremental adoption
|
|
102
|
+
- optional import rewriting to a local compatibility bridge
|
|
103
|
+
|
|
104
|
+
The strategic direction is clear: start with compatibility, then move suites toward native Themis contracts and intent-first tests as teams touch them.
|
|
105
|
+
|
|
106
|
+
## 9) Performance Discipline, Not Guesswork
|
|
77
107
|
|
|
78
108
|
Performance is measured and guarded:
|
|
79
109
|
|
|
@@ -81,13 +111,13 @@ Performance is measured and guarded:
|
|
|
81
111
|
- regression gate (`npm run benchmark:gate`)
|
|
82
112
|
- threshold config (`benchmark-gate.json`)
|
|
83
113
|
|
|
84
|
-
##
|
|
114
|
+
## 10) CLI Designed for Humans and Machines
|
|
85
115
|
|
|
86
116
|
- high-signal human reporter (`--next`)
|
|
87
117
|
- strict machine reporter outputs (`--json`, `--agent`)
|
|
88
118
|
- branded banner for human mode only
|
|
89
119
|
|
|
90
|
-
##
|
|
120
|
+
## 11) Editor Surface Without Replacing The CLI
|
|
91
121
|
|
|
92
122
|
Themis includes a thin VS Code extension scaffold that reads `.themis/*` artifacts, reruns tests, and opens the HTML report. The CLI remains the source of truth.
|
|
93
123
|
|
|
@@ -108,4 +138,5 @@ If you describe Themis publicly, use this framing:
|
|
|
108
138
|
- "The best unit test framework for AI agents in Node.js and TypeScript"
|
|
109
139
|
- "An AI verdict engine for human and agent review loops"
|
|
110
140
|
- "Intent-first testing with deterministic reruns and machine-readable artifacts"
|
|
141
|
+
- "A better alternative to snapshot-heavy workflows: explicit contracts, readable diffs, intentional updates"
|
|
111
142
|
- "JS-fast runtime with first-party TypeScript DX and benchmark-gated discipline"
|
package/globals.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ declare global {
|
|
|
44
44
|
var advanceTimersByTime: (ms: number) => void;
|
|
45
45
|
var runAllTimers: () => void;
|
|
46
46
|
var flushMicrotasks: FlushMicrotasks;
|
|
47
|
+
var captureContract: import('./index').CaptureContract;
|
|
47
48
|
var mockFetch: (handlerOrResponse: unknown) => FetchMock;
|
|
48
49
|
var restoreFetch: () => void;
|
|
49
50
|
var resetFetchMocks: () => void;
|
package/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface TestResult {
|
|
|
18
18
|
export interface FileResult {
|
|
19
19
|
file: string;
|
|
20
20
|
tests: TestResult[];
|
|
21
|
+
contracts?: ContractCaptureEvent[];
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface RunMeta {
|
|
@@ -78,6 +79,7 @@ export interface RunOptions {
|
|
|
78
79
|
tsconfigPath?: string | null;
|
|
79
80
|
isolation?: 'worker' | 'in-process';
|
|
80
81
|
cache?: boolean;
|
|
82
|
+
updateContracts?: boolean;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
export interface ThemisConfig {
|
|
@@ -100,6 +102,8 @@ export interface MigrationResult {
|
|
|
100
102
|
packageUpdated: boolean;
|
|
101
103
|
rewriteImports: boolean;
|
|
102
104
|
rewrittenFiles: string[];
|
|
105
|
+
convert?: boolean;
|
|
106
|
+
convertedFiles?: string[];
|
|
103
107
|
reportPath: string;
|
|
104
108
|
report: {
|
|
105
109
|
schema: 'themis.migration.report.v1';
|
|
@@ -112,18 +116,23 @@ export interface MigrationResult {
|
|
|
112
116
|
testingLibraryReact: number;
|
|
113
117
|
rewrittenFiles: number;
|
|
114
118
|
rewrittenImports: number;
|
|
119
|
+
convertedFiles: number;
|
|
120
|
+
convertedAssertions: number;
|
|
121
|
+
removedImports: number;
|
|
115
122
|
};
|
|
116
123
|
files: Array<{
|
|
117
124
|
file: string;
|
|
118
125
|
imports: string[];
|
|
119
126
|
}>;
|
|
120
127
|
rewrites: string[];
|
|
128
|
+
conversions?: string[];
|
|
121
129
|
nextActions: string[];
|
|
122
130
|
};
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
export interface MigrationOptions {
|
|
126
134
|
rewriteImports?: boolean;
|
|
135
|
+
convert?: boolean;
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
export interface GenerateOptions {
|
|
@@ -208,6 +217,7 @@ export interface RunArtifactPaths {
|
|
|
208
217
|
runDiff: string;
|
|
209
218
|
runHistory: string;
|
|
210
219
|
fixHandoff: string;
|
|
220
|
+
contractDiff: string;
|
|
211
221
|
}
|
|
212
222
|
|
|
213
223
|
export interface RunComparison {
|
|
@@ -226,6 +236,32 @@ export interface RunArtifacts {
|
|
|
226
236
|
paths: RunArtifactPaths;
|
|
227
237
|
}
|
|
228
238
|
|
|
239
|
+
export interface ContractDiffEntry {
|
|
240
|
+
path: string;
|
|
241
|
+
before?: unknown;
|
|
242
|
+
after?: unknown;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface ContractDiff {
|
|
246
|
+
equal: boolean;
|
|
247
|
+
unchangedCount: number;
|
|
248
|
+
added: ContractDiffEntry[];
|
|
249
|
+
removed: ContractDiffEntry[];
|
|
250
|
+
changed: ContractDiffEntry[];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface ContractCaptureEvent {
|
|
254
|
+
key: string;
|
|
255
|
+
name: string;
|
|
256
|
+
status: 'created' | 'updated' | 'drifted' | 'unchanged';
|
|
257
|
+
contractFile: string;
|
|
258
|
+
file: string;
|
|
259
|
+
testName: string;
|
|
260
|
+
fullName: string;
|
|
261
|
+
updateCommand: string;
|
|
262
|
+
diff: ContractDiff;
|
|
263
|
+
}
|
|
264
|
+
|
|
229
265
|
export interface GenerateGateFailure {
|
|
230
266
|
code: string;
|
|
231
267
|
count: number;
|
|
@@ -448,6 +484,23 @@ export interface GenerateSummary {
|
|
|
448
484
|
helperRemoved: boolean;
|
|
449
485
|
}
|
|
450
486
|
|
|
487
|
+
export interface ContractDiffPayload {
|
|
488
|
+
schema: 'themis.contract.diff.v1';
|
|
489
|
+
runId: string;
|
|
490
|
+
createdAt: string;
|
|
491
|
+
artifacts: {
|
|
492
|
+
contractDiff: string;
|
|
493
|
+
};
|
|
494
|
+
summary: {
|
|
495
|
+
total: number;
|
|
496
|
+
created: number;
|
|
497
|
+
updated: number;
|
|
498
|
+
drifted: number;
|
|
499
|
+
unchanged: number;
|
|
500
|
+
};
|
|
501
|
+
items: ContractCaptureEvent[];
|
|
502
|
+
}
|
|
503
|
+
|
|
451
504
|
export function main(argv: string[]): Promise<void>;
|
|
452
505
|
export function collectAndRun(filePath: string, options?: Omit<RunOptions, 'maxWorkers'>): Promise<FileResult>;
|
|
453
506
|
export function runTests(files: string[], options?: RunOptions): Promise<RunResult>;
|
|
@@ -471,6 +524,10 @@ export interface MockResult {
|
|
|
471
524
|
value: unknown;
|
|
472
525
|
}
|
|
473
526
|
|
|
527
|
+
export interface CaptureContract {
|
|
528
|
+
(name: string, value: unknown, options?: { file?: string }): unknown;
|
|
529
|
+
}
|
|
530
|
+
|
|
474
531
|
export interface MockState<TArgs extends unknown[] = unknown[]> {
|
|
475
532
|
calls: TArgs[];
|
|
476
533
|
results: MockResult[];
|
package/package.json
CHANGED
package/src/artifacts.js
CHANGED
|
@@ -7,6 +7,7 @@ const FAILED_TESTS_FILE = 'failed-tests.json';
|
|
|
7
7
|
const RUN_DIFF_FILE = 'run-diff.json';
|
|
8
8
|
const RUN_HISTORY_FILE = 'run-history.json';
|
|
9
9
|
const FIX_HANDOFF_FILE = 'fix-handoff.json';
|
|
10
|
+
const CONTRACT_DIFF_FILE = 'contract-diff.json';
|
|
10
11
|
const GENERATE_MAP_FILE = 'generate-map.json';
|
|
11
12
|
const GENERATE_BACKLOG_FILE = 'generate-backlog.json';
|
|
12
13
|
|
|
@@ -23,7 +24,8 @@ function writeRunArtifacts(cwd, result) {
|
|
|
23
24
|
failedTests: path.join(ARTIFACT_DIR, FAILED_TESTS_FILE),
|
|
24
25
|
runDiff: path.join(ARTIFACT_DIR, RUN_DIFF_FILE),
|
|
25
26
|
runHistory: path.join(ARTIFACT_DIR, RUN_HISTORY_FILE),
|
|
26
|
-
fixHandoff: path.join(ARTIFACT_DIR, FIX_HANDOFF_FILE)
|
|
27
|
+
fixHandoff: path.join(ARTIFACT_DIR, FIX_HANDOFF_FILE),
|
|
28
|
+
contractDiff: path.join(ARTIFACT_DIR, CONTRACT_DIFF_FILE)
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
result.artifacts = {
|
|
@@ -83,6 +85,14 @@ function writeRunArtifacts(cwd, result) {
|
|
|
83
85
|
});
|
|
84
86
|
fs.writeFileSync(historyPath, `${stringifyArtifact(nextHistory)}\n`, 'utf8');
|
|
85
87
|
|
|
88
|
+
const contractDiffPayload = buildContractDiffPayload(result, {
|
|
89
|
+
runId,
|
|
90
|
+
createdAt: new Date().toISOString(),
|
|
91
|
+
relativePaths
|
|
92
|
+
});
|
|
93
|
+
const contractDiffPath = path.join(artifactDir, CONTRACT_DIFF_FILE);
|
|
94
|
+
fs.writeFileSync(contractDiffPath, `${stringifyArtifact(contractDiffPayload)}\n`, 'utf8');
|
|
95
|
+
|
|
86
96
|
const fixHandoffPath = path.join(artifactDir, FIX_HANDOFF_FILE);
|
|
87
97
|
let fixHandoff = null;
|
|
88
98
|
if (failedTests.length > 0) {
|
|
@@ -100,9 +110,11 @@ function writeRunArtifacts(cwd, result) {
|
|
|
100
110
|
diffPath,
|
|
101
111
|
historyPath,
|
|
102
112
|
fixHandoffPath,
|
|
113
|
+
contractDiffPath,
|
|
103
114
|
failuresPayload,
|
|
104
115
|
comparison,
|
|
105
|
-
fixHandoff
|
|
116
|
+
fixHandoff,
|
|
117
|
+
contractDiffPayload
|
|
106
118
|
};
|
|
107
119
|
}
|
|
108
120
|
|
|
@@ -192,6 +204,55 @@ function collectFailureNames(result) {
|
|
|
192
204
|
return names.sort();
|
|
193
205
|
}
|
|
194
206
|
|
|
207
|
+
function buildContractDiffPayload(result, context) {
|
|
208
|
+
const items = [];
|
|
209
|
+
for (const fileEntry of result.files || []) {
|
|
210
|
+
for (const contract of fileEntry.contracts || []) {
|
|
211
|
+
items.push({
|
|
212
|
+
key: String(contract.key || ''),
|
|
213
|
+
name: String(contract.name || ''),
|
|
214
|
+
file: String(fileEntry.file || contract.file || ''),
|
|
215
|
+
testName: String(contract.testName || ''),
|
|
216
|
+
fullName: String(contract.fullName || ''),
|
|
217
|
+
contractFile: String(contract.contractFile || ''),
|
|
218
|
+
status: String(contract.status || 'unchanged'),
|
|
219
|
+
updateCommand: String(contract.updateCommand || ''),
|
|
220
|
+
diff: normalizeContractDiff(contract.diff)
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const summary = {
|
|
226
|
+
total: items.length,
|
|
227
|
+
created: items.filter((item) => item.status === 'created').length,
|
|
228
|
+
updated: items.filter((item) => item.status === 'updated').length,
|
|
229
|
+
drifted: items.filter((item) => item.status === 'drifted').length,
|
|
230
|
+
unchanged: items.filter((item) => item.status === 'unchanged').length
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
schema: 'themis.contract.diff.v1',
|
|
235
|
+
runId: context.runId,
|
|
236
|
+
createdAt: context.createdAt,
|
|
237
|
+
artifacts: {
|
|
238
|
+
contractDiff: context.relativePaths.contractDiff
|
|
239
|
+
},
|
|
240
|
+
summary,
|
|
241
|
+
items
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function normalizeContractDiff(diff) {
|
|
246
|
+
const safeDiff = diff || {};
|
|
247
|
+
return {
|
|
248
|
+
equal: Boolean(safeDiff.equal),
|
|
249
|
+
unchangedCount: Number(safeDiff.unchangedCount || 0),
|
|
250
|
+
added: Array.isArray(safeDiff.added) ? safeDiff.added : [],
|
|
251
|
+
removed: Array.isArray(safeDiff.removed) ? safeDiff.removed : [],
|
|
252
|
+
changed: Array.isArray(safeDiff.changed) ? safeDiff.changed : []
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
195
256
|
function createRunId(startedAt) {
|
|
196
257
|
return String(startedAt || '')
|
|
197
258
|
.replace(/[:.]/g, '-')
|
|
Binary file
|
package/src/cli.js
CHANGED
|
@@ -80,6 +80,9 @@ async function main(argv) {
|
|
|
80
80
|
if (result.rewriteImports) {
|
|
81
81
|
console.log(`Imports: rewrote ${result.rewrittenFiles.length} file(s) to local Themis compatibility imports.`);
|
|
82
82
|
}
|
|
83
|
+
if (result.convertedFiles && result.convertedFiles.length > 0) {
|
|
84
|
+
console.log(`Codemods: converted ${result.convertedFiles.length} file(s) to Themis-native patterns.`);
|
|
85
|
+
}
|
|
83
86
|
console.log('Runtime compatibility is enabled for @jest/globals, vitest, and @testing-library/react imports.');
|
|
84
87
|
console.log('Next: run npx themis test or npm run test:themis');
|
|
85
88
|
return;
|
|
@@ -180,6 +183,7 @@ async function executeTestRun(cwd, flags) {
|
|
|
180
183
|
match: flags.match || null,
|
|
181
184
|
allowedFullNames,
|
|
182
185
|
noMemes: Boolean(flags.noMemes),
|
|
186
|
+
updateContracts: Boolean(flags.updateContracts),
|
|
183
187
|
cwd,
|
|
184
188
|
environment,
|
|
185
189
|
setupFiles: config.setupFiles,
|
|
@@ -284,6 +288,10 @@ function parseFlags(args) {
|
|
|
284
288
|
flags.noMemes = true;
|
|
285
289
|
continue;
|
|
286
290
|
}
|
|
291
|
+
if (token === '--update-contracts') {
|
|
292
|
+
flags.updateContracts = true;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
287
295
|
if (token === '-w' || token === '--watch') {
|
|
288
296
|
flags.watch = true;
|
|
289
297
|
continue;
|
|
@@ -345,13 +353,18 @@ function parseFlags(args) {
|
|
|
345
353
|
function parseMigrateFlags(args) {
|
|
346
354
|
const flags = {
|
|
347
355
|
source: args[0],
|
|
348
|
-
rewriteImports: false
|
|
356
|
+
rewriteImports: false,
|
|
357
|
+
convert: false
|
|
349
358
|
};
|
|
350
359
|
|
|
351
360
|
for (let i = 1; i < args.length; i += 1) {
|
|
352
361
|
const token = args[i];
|
|
353
362
|
if (token === '--rewrite-imports') {
|
|
354
363
|
flags.rewriteImports = true;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (token === '--convert') {
|
|
367
|
+
flags.convert = true;
|
|
355
368
|
}
|
|
356
369
|
}
|
|
357
370
|
|
|
@@ -563,8 +576,8 @@ function printUsage() {
|
|
|
563
576
|
console.log(' generate [path] Scan source files and generate Themis contract tests');
|
|
564
577
|
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]');
|
|
565
578
|
console.log(' scan [path] Alias for generate');
|
|
566
|
-
console.log(' migrate <jest|vitest> [--rewrite-imports] Scaffold an incremental migration bridge for existing suites');
|
|
567
|
-
console.log(' test [--json] [--agent] [--next] [--reporter spec|next|json|agent|html] [--workers N] [--stability N] [--environment node|jsdom] [--isolation worker|in-process] [--cache] [-w|--watch] [--html-output path] [--match regex] [--rerun-failed] [--no-memes] [--lexicon classic|themis]');
|
|
579
|
+
console.log(' migrate <jest|vitest> [--rewrite-imports] [--convert] Scaffold an incremental migration bridge for existing suites');
|
|
580
|
+
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] [-w|--watch] [--html-output path] [--match regex] [--rerun-failed] [--no-memes] [--lexicon classic|themis]');
|
|
568
581
|
}
|
|
569
582
|
|
|
570
583
|
function printGenerateSummary(summary, cwd) {
|