@vitronai/themis 0.1.0-beta.0
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 +41 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/benchmark-gate.json +10 -0
- package/bin/themis.js +8 -0
- package/docs/api.md +210 -0
- package/docs/publish.md +55 -0
- package/docs/release-policy.md +54 -0
- package/docs/schemas/agent-result.v1.json +277 -0
- package/docs/schemas/failures.v1.json +78 -0
- package/docs/vscode-extension.md +40 -0
- package/docs/why-themis.md +111 -0
- package/globals.d.ts +22 -0
- package/globals.js +1 -0
- package/index.d.ts +190 -0
- package/index.js +17 -0
- package/package.json +90 -0
- package/src/artifacts.js +207 -0
- package/src/assets/themisBg.png +0 -0
- package/src/assets/themisLogo.png +0 -0
- package/src/assets/themisReport.png +0 -0
- package/src/cli.js +395 -0
- package/src/config.js +52 -0
- package/src/discovery.js +34 -0
- package/src/environment.js +108 -0
- package/src/expect.js +175 -0
- package/src/init.js +22 -0
- package/src/module-loader.js +489 -0
- package/src/reporter.js +2141 -0
- package/src/runner.js +168 -0
- package/src/runtime.js +472 -0
- package/src/snapshots.js +90 -0
- package/src/stability.js +98 -0
- package/src/test-utils.js +201 -0
- package/src/verdict.js +71 -0
- package/src/watch.js +154 -0
- package/src/worker.js +26 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export type Awaitable<T> = T | Promise<T>;
|
|
2
|
+
|
|
3
|
+
export type TestStatus = 'passed' | 'failed' | 'skipped';
|
|
4
|
+
|
|
5
|
+
export interface NormalizedError {
|
|
6
|
+
message: string;
|
|
7
|
+
stack: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TestResult {
|
|
11
|
+
name: string;
|
|
12
|
+
fullName: string;
|
|
13
|
+
status: TestStatus;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
error: NormalizedError | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FileResult {
|
|
19
|
+
file: string;
|
|
20
|
+
tests: TestResult[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RunMeta {
|
|
24
|
+
startedAt: string;
|
|
25
|
+
finishedAt: string;
|
|
26
|
+
maxWorkers: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RunSummary {
|
|
30
|
+
total: number;
|
|
31
|
+
passed: number;
|
|
32
|
+
failed: number;
|
|
33
|
+
skipped: number;
|
|
34
|
+
durationMs: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type StabilityStatus = TestStatus | 'missing';
|
|
38
|
+
export type StabilityClassification = 'stable_pass' | 'stable_fail' | 'unstable';
|
|
39
|
+
|
|
40
|
+
export interface StabilityTestResult {
|
|
41
|
+
file: string;
|
|
42
|
+
testName: string;
|
|
43
|
+
fullName: string;
|
|
44
|
+
statuses: StabilityStatus[];
|
|
45
|
+
classification: StabilityClassification;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface StabilitySummary {
|
|
49
|
+
stablePass: number;
|
|
50
|
+
stableFail: number;
|
|
51
|
+
unstable: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface StabilityReport {
|
|
55
|
+
runs: number;
|
|
56
|
+
summary: StabilitySummary;
|
|
57
|
+
tests: StabilityTestResult[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RunResult {
|
|
61
|
+
meta: RunMeta;
|
|
62
|
+
files: FileResult[];
|
|
63
|
+
summary: RunSummary;
|
|
64
|
+
stability?: StabilityReport;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type TestEnvironment = 'node' | 'jsdom';
|
|
68
|
+
|
|
69
|
+
export interface RunOptions {
|
|
70
|
+
maxWorkers?: number;
|
|
71
|
+
match?: string | null;
|
|
72
|
+
allowedFullNames?: string[] | null;
|
|
73
|
+
noMemes?: boolean;
|
|
74
|
+
cwd?: string;
|
|
75
|
+
environment?: TestEnvironment;
|
|
76
|
+
setupFiles?: string[];
|
|
77
|
+
tsconfigPath?: string | null;
|
|
78
|
+
updateSnapshots?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ThemisConfig {
|
|
82
|
+
testDir: string;
|
|
83
|
+
testRegex: string;
|
|
84
|
+
maxWorkers: number;
|
|
85
|
+
reporter: string;
|
|
86
|
+
environment: TestEnvironment;
|
|
87
|
+
setupFiles: string[];
|
|
88
|
+
tsconfigPath: string | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function main(argv: string[]): Promise<void>;
|
|
92
|
+
export function collectAndRun(filePath: string, options?: Omit<RunOptions, 'maxWorkers'>): Promise<FileResult>;
|
|
93
|
+
export function runTests(files: string[], options?: RunOptions): Promise<RunResult>;
|
|
94
|
+
export function discoverTests(cwd: string, config: ThemisConfig): string[];
|
|
95
|
+
export function loadConfig(cwd: string): ThemisConfig;
|
|
96
|
+
export function initConfig(cwd: string): void;
|
|
97
|
+
export const DEFAULT_CONFIG: ThemisConfig;
|
|
98
|
+
|
|
99
|
+
export interface MockResult {
|
|
100
|
+
type: 'return' | 'throw';
|
|
101
|
+
value: unknown;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface MockState<TArgs extends unknown[] = unknown[]> {
|
|
105
|
+
calls: TArgs[];
|
|
106
|
+
results: MockResult[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface MockFunction<TArgs extends unknown[] = unknown[], TReturn = unknown> {
|
|
110
|
+
(...args: TArgs): TReturn;
|
|
111
|
+
mock: MockState<TArgs>;
|
|
112
|
+
mockImplementation(implementation: (...args: TArgs) => TReturn): MockFunction<TArgs, TReturn>;
|
|
113
|
+
mockReturnValue(value: TReturn): MockFunction<TArgs, TReturn>;
|
|
114
|
+
mockResolvedValue(value: Awaited<TReturn>): MockFunction<TArgs, TReturn>;
|
|
115
|
+
mockRejectedValue(value: unknown): MockFunction<TArgs, TReturn>;
|
|
116
|
+
mockClear(): MockFunction<TArgs, TReturn>;
|
|
117
|
+
mockReset(): MockFunction<TArgs, TReturn>;
|
|
118
|
+
mockRestore?(): MockFunction<TArgs, TReturn>;
|
|
119
|
+
getMockName(): string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ExpectMatchers<TReceived = unknown> {
|
|
123
|
+
toBe(expected: TReceived): void;
|
|
124
|
+
toEqual(expected: unknown): void;
|
|
125
|
+
toMatchObject(expected: unknown): void;
|
|
126
|
+
toBeTruthy(): void;
|
|
127
|
+
toBeFalsy(): void;
|
|
128
|
+
toBeDefined(): void;
|
|
129
|
+
toBeUndefined(): void;
|
|
130
|
+
toBeNull(): void;
|
|
131
|
+
toHaveLength(expected: number): void;
|
|
132
|
+
toContain(item: unknown): void;
|
|
133
|
+
toThrow(match?: string | RegExp): void;
|
|
134
|
+
toHaveBeenCalled(): void;
|
|
135
|
+
toHaveBeenCalledTimes(expected: number): void;
|
|
136
|
+
toHaveBeenCalledWith(...expectedArgs: unknown[]): void;
|
|
137
|
+
toMatchSnapshot(snapshotName?: string): void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type Expect = <TReceived = unknown>(received: TReceived) => ExpectMatchers<TReceived>;
|
|
141
|
+
export const expect: Expect;
|
|
142
|
+
export type Fn = <TArgs extends unknown[] = unknown[], TReturn = unknown>(
|
|
143
|
+
implementation?: (...args: TArgs) => TReturn
|
|
144
|
+
) => MockFunction<TArgs, TReturn>;
|
|
145
|
+
export type SpyOn = <TTarget extends Record<string, unknown>, TKey extends keyof TTarget>(
|
|
146
|
+
target: TTarget,
|
|
147
|
+
methodName: TKey
|
|
148
|
+
) => MockFunction<any[], any>;
|
|
149
|
+
export type MockModule = (request: string, factoryOrExports?: unknown | (() => unknown)) => void;
|
|
150
|
+
export type MockControl = () => void;
|
|
151
|
+
|
|
152
|
+
export type SuiteFn = () => void;
|
|
153
|
+
export type TestFn = () => Awaitable<void>;
|
|
154
|
+
export type Describe = (name: string, fn: SuiteFn) => void;
|
|
155
|
+
export type Test = (name: string, fn: TestFn) => void;
|
|
156
|
+
export type Hook = (fn: TestFn) => void;
|
|
157
|
+
|
|
158
|
+
export type IntentContext = Record<string, unknown>;
|
|
159
|
+
export type IntentPhase<TContext extends IntentContext = IntentContext> = (ctx: TContext) => Awaitable<void>;
|
|
160
|
+
export type IntentRegistrar<TContext extends IntentContext = IntentContext> = (
|
|
161
|
+
description: string | IntentPhase<TContext>,
|
|
162
|
+
fn?: IntentPhase<TContext>
|
|
163
|
+
) => void;
|
|
164
|
+
|
|
165
|
+
export interface IntentDSL<TContext extends IntentContext = IntentContext> {
|
|
166
|
+
context: IntentRegistrar<TContext>;
|
|
167
|
+
run: IntentRegistrar<TContext>;
|
|
168
|
+
verify: IntentRegistrar<TContext>;
|
|
169
|
+
cleanup: IntentRegistrar<TContext>;
|
|
170
|
+
|
|
171
|
+
arrange: IntentRegistrar<TContext>;
|
|
172
|
+
act: IntentRegistrar<TContext>;
|
|
173
|
+
assert: IntentRegistrar<TContext>;
|
|
174
|
+
given: IntentRegistrar<TContext>;
|
|
175
|
+
when: IntentRegistrar<TContext>;
|
|
176
|
+
then: IntentRegistrar<TContext>;
|
|
177
|
+
setup: IntentRegistrar<TContext>;
|
|
178
|
+
infer: IntentRegistrar<TContext>;
|
|
179
|
+
teardown: IntentRegistrar<TContext>;
|
|
180
|
+
finally: IntentRegistrar<TContext>;
|
|
181
|
+
cook: IntentRegistrar<TContext>;
|
|
182
|
+
yeet: IntentRegistrar<TContext>;
|
|
183
|
+
vibecheck: IntentRegistrar<TContext>;
|
|
184
|
+
wipe: IntentRegistrar<TContext>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export type Intent = <TContext extends IntentContext = IntentContext>(
|
|
188
|
+
name: string,
|
|
189
|
+
define: (dsl: IntentDSL<TContext>) => void
|
|
190
|
+
) => void;
|
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { main } = require('./src/cli');
|
|
2
|
+
const { collectAndRun } = require('./src/runtime');
|
|
3
|
+
const { runTests } = require('./src/runner');
|
|
4
|
+
const { discoverTests } = require('./src/discovery');
|
|
5
|
+
const { loadConfig, initConfig, DEFAULT_CONFIG } = require('./src/config');
|
|
6
|
+
const { expect } = require('./src/expect');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
main,
|
|
10
|
+
collectAndRun,
|
|
11
|
+
runTests,
|
|
12
|
+
discoverTests,
|
|
13
|
+
loadConfig,
|
|
14
|
+
initConfig,
|
|
15
|
+
DEFAULT_CONFIG,
|
|
16
|
+
expect
|
|
17
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vitronai/themis",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Intent-first unit test framework for AI agents in Node.js and TypeScript, powered by an AI verdict engine",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Vitron AI",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/vitron-ai/themis.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/vitron-ai/themis#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/vitron-ai/themis/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"testing",
|
|
17
|
+
"unit-test",
|
|
18
|
+
"test-runner",
|
|
19
|
+
"ai",
|
|
20
|
+
"agents",
|
|
21
|
+
"ai-agents",
|
|
22
|
+
"agentic",
|
|
23
|
+
"nodejs",
|
|
24
|
+
"typescript",
|
|
25
|
+
"tsx",
|
|
26
|
+
"esm",
|
|
27
|
+
"jsdom",
|
|
28
|
+
"react-testing",
|
|
29
|
+
"html-report",
|
|
30
|
+
"json-report",
|
|
31
|
+
"cli",
|
|
32
|
+
"verdict-engine",
|
|
33
|
+
"deterministic",
|
|
34
|
+
"rerun-failed",
|
|
35
|
+
"intent-dsl",
|
|
36
|
+
"node-test-runner"
|
|
37
|
+
],
|
|
38
|
+
"type": "commonjs",
|
|
39
|
+
"main": "index.js",
|
|
40
|
+
"types": "index.d.ts",
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./index.d.ts",
|
|
44
|
+
"require": "./index.js",
|
|
45
|
+
"default": "./index.js"
|
|
46
|
+
},
|
|
47
|
+
"./globals": {
|
|
48
|
+
"types": "./globals.d.ts",
|
|
49
|
+
"require": "./globals.js",
|
|
50
|
+
"default": "./globals.js"
|
|
51
|
+
},
|
|
52
|
+
"./package.json": "./package.json"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"bin",
|
|
56
|
+
"src/*.js",
|
|
57
|
+
"src/assets/*",
|
|
58
|
+
"docs",
|
|
59
|
+
"index.js",
|
|
60
|
+
"index.d.ts",
|
|
61
|
+
"globals.js",
|
|
62
|
+
"globals.d.ts",
|
|
63
|
+
"README.md",
|
|
64
|
+
"CHANGELOG.md",
|
|
65
|
+
"LICENSE",
|
|
66
|
+
"benchmark-gate.json"
|
|
67
|
+
],
|
|
68
|
+
"bin": {
|
|
69
|
+
"themis": "bin/themis.js"
|
|
70
|
+
},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"test": "node bin/themis.js test",
|
|
73
|
+
"validate": "npm test && npm run typecheck && npm run benchmark:gate",
|
|
74
|
+
"typecheck": "tsc -p tsconfig.json --pretty false",
|
|
75
|
+
"benchmark": "node scripts/benchmark.js",
|
|
76
|
+
"benchmark:gate": "node scripts/benchmark-gate.js",
|
|
77
|
+
"pack:check": "npm pack --dry-run",
|
|
78
|
+
"prepublishOnly": "npm test && npm run typecheck"
|
|
79
|
+
},
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"access": "public"
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=18"
|
|
85
|
+
},
|
|
86
|
+
"dependencies": {
|
|
87
|
+
"jsdom": "^26.1.0",
|
|
88
|
+
"typescript": "^5.9.3"
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/artifacts.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ARTIFACT_DIR = '.themis';
|
|
5
|
+
const LAST_RUN_FILE = 'last-run.json';
|
|
6
|
+
const FAILED_TESTS_FILE = 'failed-tests.json';
|
|
7
|
+
const RUN_DIFF_FILE = 'run-diff.json';
|
|
8
|
+
const RUN_HISTORY_FILE = 'run-history.json';
|
|
9
|
+
|
|
10
|
+
function writeRunArtifacts(cwd, result) {
|
|
11
|
+
const artifactDir = path.join(cwd, ARTIFACT_DIR);
|
|
12
|
+
fs.mkdirSync(artifactDir, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const runPath = path.join(artifactDir, LAST_RUN_FILE);
|
|
15
|
+
const previousRun = readJsonIfExists(runPath);
|
|
16
|
+
const runId = createRunId(result.meta?.startedAt || new Date().toISOString());
|
|
17
|
+
const comparison = buildRunComparison(previousRun, result);
|
|
18
|
+
const relativePaths = {
|
|
19
|
+
lastRun: path.join(ARTIFACT_DIR, LAST_RUN_FILE),
|
|
20
|
+
failedTests: path.join(ARTIFACT_DIR, FAILED_TESTS_FILE),
|
|
21
|
+
runDiff: path.join(ARTIFACT_DIR, RUN_DIFF_FILE),
|
|
22
|
+
runHistory: path.join(ARTIFACT_DIR, RUN_HISTORY_FILE)
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
result.artifacts = {
|
|
26
|
+
runId,
|
|
27
|
+
comparison,
|
|
28
|
+
paths: relativePaths
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
fs.writeFileSync(runPath, `${stringifyArtifact(result)}\n`, 'utf8');
|
|
32
|
+
|
|
33
|
+
const failedTests = [];
|
|
34
|
+
for (const fileEntry of result.files || []) {
|
|
35
|
+
for (const test of fileEntry.tests || []) {
|
|
36
|
+
if (test.status !== 'failed') {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
failedTests.push({
|
|
40
|
+
file: fileEntry.file,
|
|
41
|
+
name: test.name,
|
|
42
|
+
fullName: test.fullName,
|
|
43
|
+
durationMs: test.durationMs,
|
|
44
|
+
message: test.error ? test.error.message : '',
|
|
45
|
+
stack: test.error ? test.error.stack : ''
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const failuresPayload = {
|
|
51
|
+
schema: 'themis.failures.v1',
|
|
52
|
+
runId,
|
|
53
|
+
createdAt: new Date().toISOString(),
|
|
54
|
+
summary: result.summary,
|
|
55
|
+
failedTests
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const failuresPath = path.join(artifactDir, FAILED_TESTS_FILE);
|
|
59
|
+
fs.writeFileSync(failuresPath, `${stringifyArtifact(failuresPayload)}\n`, 'utf8');
|
|
60
|
+
|
|
61
|
+
const diffPayload = {
|
|
62
|
+
schema: 'themis.run.diff.v1',
|
|
63
|
+
runId,
|
|
64
|
+
...comparison
|
|
65
|
+
};
|
|
66
|
+
const diffPath = path.join(artifactDir, RUN_DIFF_FILE);
|
|
67
|
+
fs.writeFileSync(diffPath, `${stringifyArtifact(diffPayload)}\n`, 'utf8');
|
|
68
|
+
|
|
69
|
+
const historyPath = path.join(artifactDir, RUN_HISTORY_FILE);
|
|
70
|
+
const previousHistory = readJsonIfExists(historyPath);
|
|
71
|
+
const nextHistory = Array.isArray(previousHistory) ? previousHistory.slice(-19) : [];
|
|
72
|
+
nextHistory.push({
|
|
73
|
+
runId,
|
|
74
|
+
startedAt: result.meta?.startedAt || '',
|
|
75
|
+
finishedAt: result.meta?.finishedAt || '',
|
|
76
|
+
summary: result.summary,
|
|
77
|
+
failedTests: failedTests.map((entry) => entry.fullName),
|
|
78
|
+
comparison
|
|
79
|
+
});
|
|
80
|
+
fs.writeFileSync(historyPath, `${stringifyArtifact(nextHistory)}\n`, 'utf8');
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
runPath,
|
|
84
|
+
failuresPath,
|
|
85
|
+
diffPath,
|
|
86
|
+
historyPath,
|
|
87
|
+
failuresPayload,
|
|
88
|
+
comparison
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readFailedTestsArtifact(cwd) {
|
|
93
|
+
const failuresPath = path.join(cwd, ARTIFACT_DIR, FAILED_TESTS_FILE);
|
|
94
|
+
if (!fs.existsSync(failuresPath)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const raw = fs.readFileSync(failuresPath, 'utf8');
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(raw);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
failuresPath,
|
|
105
|
+
failedTests: [],
|
|
106
|
+
parseError: String(error?.message || error)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!parsed || !Array.isArray(parsed.failedTests)) {
|
|
111
|
+
return {
|
|
112
|
+
failuresPath,
|
|
113
|
+
failedTests: [],
|
|
114
|
+
parseError: 'Invalid artifact shape: expected "failedTests" to be an array'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
failuresPath,
|
|
120
|
+
failedTests: parsed.failedTests
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildRunComparison(previousRun, result) {
|
|
125
|
+
const currentFailures = collectFailureNames(result);
|
|
126
|
+
const currentFailureSet = new Set(currentFailures);
|
|
127
|
+
|
|
128
|
+
if (!previousRun) {
|
|
129
|
+
return {
|
|
130
|
+
status: 'baseline',
|
|
131
|
+
previousRunId: '',
|
|
132
|
+
previousRunAt: '',
|
|
133
|
+
currentRunAt: result.meta?.startedAt || '',
|
|
134
|
+
delta: {
|
|
135
|
+
total: Number(result.summary?.total || 0),
|
|
136
|
+
passed: Number(result.summary?.passed || 0),
|
|
137
|
+
failed: Number(result.summary?.failed || 0),
|
|
138
|
+
skipped: Number(result.summary?.skipped || 0),
|
|
139
|
+
durationMs: roundDuration(result.summary?.durationMs || 0)
|
|
140
|
+
},
|
|
141
|
+
newFailures: currentFailures,
|
|
142
|
+
resolvedFailures: []
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const previousFailures = collectFailureNames(previousRun);
|
|
147
|
+
const previousFailureSet = new Set(previousFailures);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
status: 'changed',
|
|
151
|
+
previousRunId: String(previousRun.artifacts?.runId || ''),
|
|
152
|
+
previousRunAt: String(previousRun.meta?.startedAt || ''),
|
|
153
|
+
currentRunAt: String(result.meta?.startedAt || ''),
|
|
154
|
+
delta: {
|
|
155
|
+
total: Number(result.summary?.total || 0) - Number(previousRun.summary?.total || 0),
|
|
156
|
+
passed: Number(result.summary?.passed || 0) - Number(previousRun.summary?.passed || 0),
|
|
157
|
+
failed: Number(result.summary?.failed || 0) - Number(previousRun.summary?.failed || 0),
|
|
158
|
+
skipped: Number(result.summary?.skipped || 0) - Number(previousRun.summary?.skipped || 0),
|
|
159
|
+
durationMs: roundDuration(Number(result.summary?.durationMs || 0) - Number(previousRun.summary?.durationMs || 0))
|
|
160
|
+
},
|
|
161
|
+
newFailures: currentFailures.filter((name) => !previousFailureSet.has(name)),
|
|
162
|
+
resolvedFailures: previousFailures.filter((name) => !currentFailureSet.has(name))
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function collectFailureNames(result) {
|
|
167
|
+
const names = [];
|
|
168
|
+
for (const fileEntry of result.files || []) {
|
|
169
|
+
for (const test of fileEntry.tests || []) {
|
|
170
|
+
if (test.status === 'failed') {
|
|
171
|
+
names.push(test.fullName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return names.sort();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function createRunId(startedAt) {
|
|
179
|
+
return String(startedAt || '')
|
|
180
|
+
.replace(/[:.]/g, '-')
|
|
181
|
+
.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readJsonIfExists(filePath) {
|
|
185
|
+
if (!fs.existsSync(filePath)) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function roundDuration(value) {
|
|
197
|
+
return Math.round(Number(value || 0) * 100) / 100;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stringifyArtifact(value) {
|
|
201
|
+
return JSON.stringify(value);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
writeRunArtifacts,
|
|
206
|
+
readFailedTestsArtifact
|
|
207
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|