@vitronai/themis 0.1.4 → 0.1.6
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 +7 -0
- package/README.md +57 -29
- package/docs/agents-adoption.md +5 -2
- package/docs/api.md +45 -26
- package/docs/migration.md +2 -2
- package/docs/showcases.md +41 -6
- package/docs/vscode-extension.md +12 -12
- package/docs/why-themis.md +5 -3
- package/package.json +17 -4
- package/src/artifact-paths.js +111 -0
- package/src/artifacts.js +107 -39
- package/src/cli.js +156 -18
- package/src/contract-runtime.js +1166 -0
- package/src/environment.js +1 -1
- package/src/expect.js +1 -1
- package/src/generate.js +37 -1209
- package/src/gitignore.js +53 -0
- package/src/init.js +2 -13
- package/src/migrate.js +6 -1
- package/src/module-loader.js +13 -3
- package/src/reporter.js +14 -12
- package/src/runtime.js +2 -2
- package/src/test-utils.js +1 -1
- package/templates/AGENTS.themis.md +3 -0
package/src/gitignore.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function ensureGitignoreEntries(cwd, entries) {
|
|
5
|
+
const targetPath = path.join(cwd, '.gitignore');
|
|
6
|
+
const requestedEntries = [...new Set(
|
|
7
|
+
(Array.isArray(entries) ? entries : [])
|
|
8
|
+
.map((entry) => String(entry || '').trim())
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
)];
|
|
11
|
+
|
|
12
|
+
if (requestedEntries.length === 0) {
|
|
13
|
+
return {
|
|
14
|
+
path: targetPath,
|
|
15
|
+
updated: false
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const existing = fs.existsSync(targetPath)
|
|
20
|
+
? fs.readFileSync(targetPath, 'utf8')
|
|
21
|
+
: '';
|
|
22
|
+
const normalized = existing.replace(/\r\n/g, '\n');
|
|
23
|
+
const existingEntries = new Set(
|
|
24
|
+
normalized
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
);
|
|
29
|
+
const missingEntries = requestedEntries.filter((entry) => !existingEntries.has(entry));
|
|
30
|
+
|
|
31
|
+
if (missingEntries.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
path: targetPath,
|
|
34
|
+
updated: false
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let nextSource = normalized;
|
|
39
|
+
if (nextSource.length > 0 && !nextSource.endsWith('\n')) {
|
|
40
|
+
nextSource += '\n';
|
|
41
|
+
}
|
|
42
|
+
nextSource += `${missingEntries.join('\n')}\n`;
|
|
43
|
+
fs.writeFileSync(targetPath, nextSource, 'utf8');
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
path: targetPath,
|
|
47
|
+
updated: true
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
ensureGitignoreEntries
|
|
53
|
+
};
|
package/src/init.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
1
|
const { initConfig } = require('./config');
|
|
2
|
+
const { ensureGitignoreEntries } = require('./gitignore');
|
|
4
3
|
|
|
5
4
|
function runInit(cwd) {
|
|
6
5
|
initConfig(cwd);
|
|
7
|
-
|
|
8
|
-
const testsDir = path.join(cwd, 'tests');
|
|
9
|
-
if (!fs.existsSync(testsDir)) {
|
|
10
|
-
fs.mkdirSync(testsDir, { recursive: true });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const sample = path.join(testsDir, 'example.test.js');
|
|
14
|
-
if (!fs.existsSync(sample)) {
|
|
15
|
-
const content = `describe('math', () => {\n test('adds numbers', () => {\n expect(1 + 1).toBe(2);\n });\n});\n`;
|
|
16
|
-
fs.writeFileSync(sample, content, 'utf8');
|
|
17
|
-
}
|
|
6
|
+
ensureGitignoreEntries(cwd, ['.themis/']);
|
|
18
7
|
}
|
|
19
8
|
|
|
20
9
|
module.exports = {
|
package/src/migrate.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { DEFAULT_CONFIG, loadConfig } = require('./config');
|
|
4
|
+
const { ARTIFACT_RELATIVE_PATHS } = require('./artifact-paths');
|
|
5
|
+
const { ensureGitignoreEntries } = require('./gitignore');
|
|
4
6
|
|
|
5
7
|
const SUPPORTED_MIGRATION_SOURCES = new Set(['jest', 'vitest']);
|
|
6
8
|
const THEMIS_SETUP_FILE = path.join('tests', 'setup.themis.js');
|
|
7
9
|
const THEMIS_COMPAT_FILE = 'themis.compat.js';
|
|
8
|
-
const MIGRATION_REPORT_FILE =
|
|
10
|
+
const MIGRATION_REPORT_FILE = ARTIFACT_RELATIVE_PATHS.migrationReport;
|
|
9
11
|
const SCANNABLE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
10
12
|
const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', '.themis']);
|
|
11
13
|
|
|
@@ -43,6 +45,7 @@ function runMigrate(cwd, framework, options = {}) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, 'utf8');
|
|
48
|
+
const gitignore = ensureGitignoreEntries(projectRoot, ['.themis/']);
|
|
46
49
|
|
|
47
50
|
let packageUpdated = false;
|
|
48
51
|
if (fs.existsSync(packageJsonPath)) {
|
|
@@ -74,6 +77,8 @@ function runMigrate(cwd, framework, options = {}) {
|
|
|
74
77
|
compatPath,
|
|
75
78
|
packageJsonPath: fs.existsSync(packageJsonPath) ? packageJsonPath : null,
|
|
76
79
|
packageUpdated,
|
|
80
|
+
gitignorePath: gitignore.path,
|
|
81
|
+
gitignoreUpdated: gitignore.updated,
|
|
77
82
|
reportPath,
|
|
78
83
|
report,
|
|
79
84
|
rewriteImports: Boolean(options.rewriteImports),
|
package/src/module-loader.js
CHANGED
|
@@ -4,6 +4,8 @@ const Module = require('module');
|
|
|
4
4
|
|
|
5
5
|
const SUPPORTED_SOURCE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx'];
|
|
6
6
|
const RESOLVABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.json'];
|
|
7
|
+
const THEMIS_CONTRACT_RUNTIME_REQUEST = '@vitronai/themis/contract-runtime';
|
|
8
|
+
const THEMIS_CONTRACT_RUNTIME_PATH = path.join(__dirname, 'contract-runtime.js');
|
|
7
9
|
const DEFAULT_TS_COMPILER_OPTIONS = {
|
|
8
10
|
target: 'ES2020',
|
|
9
11
|
module: 'CommonJS',
|
|
@@ -56,6 +58,10 @@ function createModuleLoader(options = {}) {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
Module._resolveFilename = function themisResolveFilename(request, parent, isMain, resolutionOptions) {
|
|
61
|
+
if (request === THEMIS_CONTRACT_RUNTIME_REQUEST) {
|
|
62
|
+
return THEMIS_CONTRACT_RUNTIME_PATH;
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
if (Object.prototype.hasOwnProperty.call(virtualModules, request)) {
|
|
60
66
|
return request;
|
|
61
67
|
}
|
|
@@ -161,7 +167,7 @@ function createModuleLoader(options = {}) {
|
|
|
161
167
|
function safeRealpath(targetPath) {
|
|
162
168
|
try {
|
|
163
169
|
return fs.realpathSync.native(targetPath);
|
|
164
|
-
} catch
|
|
170
|
+
} catch {
|
|
165
171
|
return targetPath;
|
|
166
172
|
}
|
|
167
173
|
}
|
|
@@ -175,6 +181,10 @@ function resolveRequestValue({
|
|
|
175
181
|
isMain = false,
|
|
176
182
|
virtualModules = null
|
|
177
183
|
}) {
|
|
184
|
+
if (request === THEMIS_CONTRACT_RUNTIME_REQUEST) {
|
|
185
|
+
return THEMIS_CONTRACT_RUNTIME_PATH;
|
|
186
|
+
}
|
|
187
|
+
|
|
178
188
|
if (virtualModules && Object.prototype.hasOwnProperty.call(virtualModules, request)) {
|
|
179
189
|
return request;
|
|
180
190
|
}
|
|
@@ -270,7 +280,7 @@ function findNearestPackageType(filename, projectRoot, packageTypeCache) {
|
|
|
270
280
|
if (parsed.type === 'module') {
|
|
271
281
|
packageType = 'module';
|
|
272
282
|
}
|
|
273
|
-
} catch
|
|
283
|
+
} catch {
|
|
274
284
|
packageType = 'commonjs';
|
|
275
285
|
}
|
|
276
286
|
|
|
@@ -301,7 +311,7 @@ function getCompilerContext(compilerState, projectRoot, tsconfigPath, options =
|
|
|
301
311
|
let ts;
|
|
302
312
|
try {
|
|
303
313
|
ts = require('typescript');
|
|
304
|
-
} catch
|
|
314
|
+
} catch {
|
|
305
315
|
if (options.optional) {
|
|
306
316
|
return null;
|
|
307
317
|
}
|
package/src/reporter.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { buildStabilityReport } = require('./stability');
|
|
4
|
+
const { ARTIFACT_RELATIVE_PATHS, resolveArtifactPath } = require('./artifact-paths');
|
|
4
5
|
|
|
5
6
|
const REPORT_LEXICONS = {
|
|
6
7
|
classic: {
|
|
@@ -28,6 +29,14 @@ const REPORT_LEXICONS = {
|
|
|
28
29
|
fileSkipWord: 'deferred'
|
|
29
30
|
}
|
|
30
31
|
};
|
|
32
|
+
const AGENT_ARTIFACT_PATHS = Object.freeze({
|
|
33
|
+
lastRun: ARTIFACT_RELATIVE_PATHS.lastRun,
|
|
34
|
+
failedTests: ARTIFACT_RELATIVE_PATHS.failedTests,
|
|
35
|
+
runDiff: ARTIFACT_RELATIVE_PATHS.runDiff,
|
|
36
|
+
runHistory: ARTIFACT_RELATIVE_PATHS.runHistory,
|
|
37
|
+
fixHandoff: ARTIFACT_RELATIVE_PATHS.fixHandoff,
|
|
38
|
+
contractDiff: ARTIFACT_RELATIVE_PATHS.contractDiff
|
|
39
|
+
});
|
|
31
40
|
|
|
32
41
|
function printSpec(result, options = {}) {
|
|
33
42
|
const lexicon = resolveLexicon(options.lexicon);
|
|
@@ -62,14 +71,7 @@ function printAgent(result) {
|
|
|
62
71
|
const failureClusters = clusterFailures(failures);
|
|
63
72
|
const stability = result.stability || buildStabilityReport([result]);
|
|
64
73
|
const comparison = result.artifacts?.comparison || buildAgentComparison(result, failures);
|
|
65
|
-
const artifactPaths = result.artifacts?.paths ||
|
|
66
|
-
lastRun: '.themis/last-run.json',
|
|
67
|
-
failedTests: '.themis/failed-tests.json',
|
|
68
|
-
runDiff: '.themis/run-diff.json',
|
|
69
|
-
runHistory: '.themis/run-history.json',
|
|
70
|
-
fixHandoff: '.themis/fix-handoff.json',
|
|
71
|
-
contractDiff: '.themis/contract-diff.json'
|
|
72
|
-
};
|
|
74
|
+
const artifactPaths = result.artifacts?.paths || AGENT_ARTIFACT_PATHS;
|
|
73
75
|
|
|
74
76
|
const payload = {
|
|
75
77
|
schema: 'themis.agent.result.v1',
|
|
@@ -86,9 +88,9 @@ function printAgent(result) {
|
|
|
86
88
|
hints: {
|
|
87
89
|
rerunFailed: 'npx themis test --rerun-failed',
|
|
88
90
|
targetedRerun: 'npx themis test --match "<regex>"',
|
|
89
|
-
diffLastRun:
|
|
90
|
-
repairGenerated: '
|
|
91
|
-
reviewContracts:
|
|
91
|
+
diffLastRun: `cat ${ARTIFACT_RELATIVE_PATHS.runDiff}`,
|
|
92
|
+
repairGenerated: 'npx themis test --fix',
|
|
93
|
+
reviewContracts: `cat ${ARTIFACT_RELATIVE_PATHS.contractDiff}`
|
|
92
94
|
}
|
|
93
95
|
};
|
|
94
96
|
|
|
@@ -483,7 +485,7 @@ function fnv1a32(input) {
|
|
|
483
485
|
|
|
484
486
|
function resolveHtmlOutputPath(cwd, outputPath) {
|
|
485
487
|
if (!outputPath) {
|
|
486
|
-
return
|
|
488
|
+
return resolveArtifactPath(cwd, 'htmlReport');
|
|
487
489
|
}
|
|
488
490
|
if (path.isAbsolute(outputPath)) {
|
|
489
491
|
return outputPath;
|
package/src/runtime.js
CHANGED
|
@@ -140,7 +140,7 @@ function collectAndRun(filePath, options = {}) {
|
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
function buildRuntimeApi({ root, options, testUtils, runtimeExpect, getCurrentSuite, setCurrentSuite }) {
|
|
143
|
+
function buildRuntimeApi({ root: _root, options, testUtils, runtimeExpect, getCurrentSuite, setCurrentSuite }) {
|
|
144
144
|
const describeApi = createDescribeApi({
|
|
145
145
|
getCurrentSuite,
|
|
146
146
|
setCurrentSuite
|
|
@@ -284,7 +284,7 @@ function formatParameterizedName(name, args, index) {
|
|
|
284
284
|
function stringifyParameterizedValue(value) {
|
|
285
285
|
try {
|
|
286
286
|
return JSON.stringify(value);
|
|
287
|
-
} catch
|
|
287
|
+
} catch {
|
|
288
288
|
return String(value);
|
|
289
289
|
}
|
|
290
290
|
}
|
package/src/test-utils.js
CHANGED
|
@@ -6,7 +6,9 @@ Use `@vitronai/themis` as this repository's unit test framework.
|
|
|
6
6
|
|
|
7
7
|
- Install with `npm install -D @vitronai/themis`.
|
|
8
8
|
- Initialize with `npx themis init`.
|
|
9
|
+
- Keep `.themis/` in `.gitignore`.
|
|
9
10
|
- Generate tests with `npx themis generate src`.
|
|
11
|
+
- Expect generated tests under `tests/generated`, using `.generated.test.ts` for TS/TSX sources and `.generated.test.js` for JS/JSX sources.
|
|
10
12
|
- Run tests with `npx themis test`.
|
|
11
13
|
|
|
12
14
|
## Migration
|
|
@@ -21,6 +23,7 @@ Use `@vitronai/themis` as this repository's unit test framework.
|
|
|
21
23
|
- Prefer `intent(...)` for behavior and workflow tests.
|
|
22
24
|
- Prefer `test(...)` for low-level unit checks.
|
|
23
25
|
- Prefer deterministic assertions over snapshot-style baselines.
|
|
26
|
+
- Treat `.themis/` as Themis-managed artifact output.
|
|
24
27
|
- Treat generated tests under `tests/generated` as Themis-managed output unless the repo says otherwise.
|
|
25
28
|
|
|
26
29
|
## Agent Behavior
|