@flakiness/vitest 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Degu Labs, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # Flakiness.io Vitest Reporter
2
+
3
+ A custom Vitest reporter that generates Flakiness Reports from your Vitest test runs. The reporter automatically converts Vitest test results into the standardized [Flakiness JSON format](https://github.com/flakiness/flakiness-report), capturing test outcomes, system utilization, and environment information.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Requirements](#requirements)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Uploading Reports](#uploading-reports)
11
+ - [Viewing Reports](#viewing-reports)
12
+ - [Features](#features)
13
+ - [Test Location Tracking](#test-location-tracking)
14
+ - [Duplicate Test Detection](#duplicate-test-detection)
15
+ - [Environment Detection](#environment-detection)
16
+ - [CI Integration](#ci-integration)
17
+ - [Configuration Options](#configuration-options)
18
+ - [`flakinessProject?: string`](#flakinessproject-string)
19
+ - [`endpoint?: string`](#endpoint-string)
20
+ - [`token?: string`](#token-string)
21
+ - [`outputFolder?: string`](#outputfolder-string)
22
+ - [`open?: 'always' | 'never' | 'on-failure'`](#open-always--never--on-failure)
23
+ - [`disableUpload?: boolean`](#disableupload-boolean)
24
+ - [Environment Variables](#environment-variables)
25
+ - [Example Configuration](#example-configuration)
26
+
27
+ ## Requirements
28
+
29
+ - Vitest 4.0 or higher
30
+ - Node.js project with a git repository (for commit information)
31
+ - Valid Flakiness.io access token (for uploads)
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install @flakiness/vitest
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ Add the reporter to your `vitest.config.ts`:
42
+
43
+ ```typescript
44
+ import { defineConfig } from 'vitest/config';
45
+
46
+ export default defineConfig({
47
+ test: {
48
+ includeTaskLocation: true,
49
+ reporters: [
50
+ ['@flakiness/vitest']
51
+ ],
52
+ },
53
+ });
54
+ ```
55
+
56
+ > **Note:** Setting `includeTaskLocation: true` is recommended to enable test locations the report.
57
+
58
+ Run your tests. The report will be automatically generated in the `./flakiness-report` folder:
59
+
60
+ ```bash
61
+ npx vitest run
62
+ ```
63
+
64
+ View the interactive report:
65
+
66
+ ```bash
67
+ npx flakiness show ./flakiness-report
68
+ ```
69
+
70
+ ## Uploading Reports
71
+
72
+ Reports are automatically uploaded to Flakiness.io after test completion. Authentication can be done in two ways:
73
+
74
+ - **Access token**: Provide a token via the `token` option or the `FLAKINESS_ACCESS_TOKEN` environment variable.
75
+ - **GitHub OIDC**: When running in GitHub Actions, the reporter can authenticate using GitHub's OIDC token - no access token needed. This requires two conditions:
76
+ 1. The `flakinessProject` option must be set to your Flakiness.io project identifier (`org/project`).
77
+ 2. The Flakiness.io project must be bound to the GitHub repository that runs the GitHub Actions workflow.
78
+
79
+ If upload fails, the report is still available locally in the output folder.
80
+
81
+ ## Viewing Reports
82
+
83
+ After test execution, you can view the report using:
84
+
85
+ ```bash
86
+ npx flakiness show ./flakiness-report
87
+ ```
88
+
89
+ ## Features
90
+
91
+ ### Test Location Tracking
92
+
93
+ When `includeTaskLocation: true` is set in your Vitest config, the reporter records the exact file, line, and column for each test. This enables precise navigation from the Flakiness.io dashboard back to your source code.
94
+
95
+ ### Duplicate Test Detection
96
+
97
+ Vitest allows multiple tests to share the same name:
98
+
99
+ ```typescript
100
+ it('should work', () => { /* ... */ });
101
+ it('should work', () => { /* ... */ });
102
+ ```
103
+
104
+ Flakiness.io relies on full test names (all parent suite names + test name) to construct test history, so each test must have a unique full name. The reporter detects duplicates, issues warnings, and marks them as failed with a descriptive error message. Rename your tests to have unique full names to resolve this.
105
+
106
+ ### Environment Detection
107
+
108
+ For each Vitest project, the reporter creates a unique environment. If a project has a name, it will be used as the environment name in Flakiness.io. Tests run in different projects get separate history timelines.
109
+
110
+ Environment variables prefixed with `FK_ENV_` are automatically included in the environment metadata. The prefix is stripped and the key is converted to lowercase.
111
+
112
+ **Example:**
113
+
114
+ ```bash
115
+ export FK_ENV_DEPLOYMENT=staging
116
+ export FK_ENV_REGION=us-east-1
117
+ ```
118
+
119
+ This will result in the environment containing:
120
+ ```json
121
+ {
122
+ "metadata": {
123
+ "deployment": "staging",
124
+ "region": "us-east-1"
125
+ }
126
+ }
127
+ ```
128
+
129
+ Flakiness.io will create a dedicated history for tests executed in each unique environment. This means tests run with `FK_ENV_DEPLOYMENT=staging` will have a separate timeline from tests run with `FK_ENV_DEPLOYMENT=production`, allowing you to track flakiness patterns specific to each deployment environment.
130
+
131
+ ### CI Integration
132
+
133
+ The reporter automatically detects CI environments and includes:
134
+ - CI run URLs (GitHub Actions, Azure DevOps, Jenkins, GitLab CI)
135
+ - Git commit information
136
+ - System environment data
137
+
138
+ ## Configuration Options
139
+
140
+ The reporter accepts the following options:
141
+
142
+ ### `flakinessProject?: string`
143
+
144
+ The Flakiness.io project identifier in `org/project` format. Used for GitHub OIDC authentication — when set, and the Flakiness.io project is bound to the GitHub repository running the workflow, the reporter authenticates uploads via GitHub Actions OIDC token with no access token required.
145
+
146
+ ```typescript
147
+ reporters: [
148
+ ['@flakiness/vitest', { flakinessProject: 'my-org/my-project' }]
149
+ ]
150
+ ```
151
+
152
+ ### `endpoint?: string`
153
+
154
+ Custom Flakiness.io endpoint URL for uploading reports. Defaults to the `FLAKINESS_ENDPOINT` environment variable, or `https://flakiness.io` if not set.
155
+
156
+ Use this option to point to a custom or self-hosted Flakiness.io instance.
157
+
158
+ ```typescript
159
+ reporters: [
160
+ ['@flakiness/vitest', { endpoint: 'https://custom.flakiness.io' }]
161
+ ]
162
+ ```
163
+
164
+ ### `token?: string`
165
+
166
+ Access token for authenticating with Flakiness.io when uploading reports. Defaults to the `FLAKINESS_ACCESS_TOKEN` environment variable.
167
+
168
+ If no token is provided, reporter will attempt to authenticate using Github OIDC.
169
+
170
+ ```typescript
171
+ reporters: [
172
+ ['@flakiness/vitest', { token: 'your-access-token' }]
173
+ ]
174
+ ```
175
+
176
+ ### `outputFolder?: string`
177
+
178
+ Directory path where the Flakiness report will be written. Defaults to `flakiness-report` in the current working directory, or the `FLAKINESS_OUTPUT_DIR` environment variable if set.
179
+
180
+ ```typescript
181
+ reporters: [
182
+ ['@flakiness/vitest', { outputFolder: './test-results/flakiness' }]
183
+ ]
184
+ ```
185
+
186
+ ### `open?: 'always' | 'never' | 'on-failure'`
187
+
188
+ Controls when the report viewer should automatically open in your browser after test completion.
189
+
190
+ - **`'on-failure'`** (default): Opens the report only if tests failed and running in an interactive terminal (not in CI)
191
+ - **`'always'`**: Always opens the report after test completion (when running in an interactive terminal)
192
+ - **`'never'`**: Never automatically opens the report
193
+
194
+ ```typescript
195
+ reporters: [
196
+ ['@flakiness/vitest', { open: 'always' }]
197
+ ]
198
+ ```
199
+
200
+ ### `disableUpload?: boolean`
201
+
202
+ When set to `true`, prevents uploading the report to Flakiness.io. The report is still generated locally. Can also be controlled via the `FLAKINESS_DISABLE_UPLOAD` environment variable.
203
+
204
+ ```typescript
205
+ reporters: [
206
+ ['@flakiness/vitest', { disableUpload: true }]
207
+ ]
208
+ ```
209
+
210
+ ## Environment Variables
211
+
212
+ The reporter respects the following environment variables:
213
+
214
+ - **`FLAKINESS_ACCESS_TOKEN`**: Access token for Flakiness.io uploads (equivalent to `token` option)
215
+ - **`FLAKINESS_ENDPOINT`**: Custom Flakiness.io endpoint URL (equivalent to `endpoint` option)
216
+ - **`FLAKINESS_OUTPUT_DIR`**: Output directory for reports (equivalent to `outputFolder` option)
217
+ - **`FLAKINESS_DISABLE_UPLOAD`**: When set, disables report uploads (equivalent to `disableUpload` option)
218
+
219
+ ## Example Configuration
220
+
221
+ Here's a complete example with all options:
222
+
223
+ ```typescript
224
+ import { defineConfig } from 'vitest/config';
225
+
226
+ export default defineConfig({
227
+ test: {
228
+ includeTaskLocation: true,
229
+ reporters: [
230
+ ['@flakiness/vitest', {
231
+ flakinessProject: 'my-org/my-project',
232
+ endpoint: process.env.FLAKINESS_ENDPOINT,
233
+ token: process.env.FLAKINESS_ACCESS_TOKEN,
234
+ outputFolder: './flakiness-report',
235
+ open: 'on-failure',
236
+ disableUpload: false,
237
+ }]
238
+ ],
239
+ },
240
+ });
241
+ ```
@@ -0,0 +1,376 @@
1
+ import { FlakinessReport as FK } from "@flakiness/flakiness-report";
2
+ import { CIUtils, CPUUtilization, GitWorktree, RAMUtilization, ReportUtils, showReport, uploadReport, writeReport } from "@flakiness/sdk";
3
+ import chalk from "chalk";
4
+ import crypto from "crypto";
5
+ import assert from "node:assert";
6
+ import path from "node:path";
7
+ class FKVitestReporter {
8
+ constructor(_options) {
9
+ this._options = _options;
10
+ }
11
+ _impl;
12
+ _logger = {
13
+ warn: (txt) => console.warn(chalk.yellow(txt)),
14
+ error: (txt) => console.error(chalk.red(txt)),
15
+ log: (txt) => console.log(txt)
16
+ };
17
+ setLoggerForTest(logger) {
18
+ this._logger = logger;
19
+ }
20
+ onUserConsoleLog(log) {
21
+ this._impl?.onUserConsoleLog(log);
22
+ }
23
+ onInit(vitest) {
24
+ this._impl = ReporterImpl.create(vitest.config.root, this._options, this._logger);
25
+ this._impl?.onInit(vitest);
26
+ }
27
+ /**
28
+ * Called when annotation is added via the `task.annotate` API.
29
+ */
30
+ onTestCaseAnnotate(testCase, annotation) {
31
+ this._impl?.onTestCaseAnnotate(testCase, annotation);
32
+ }
33
+ onTestRunStart() {
34
+ this._impl?.onTestRunStart();
35
+ }
36
+ async onTestCaseResult(testCase) {
37
+ this._impl?.onTestCaseResult(testCase);
38
+ }
39
+ async onTestRunEnd(testModules, unhandledErrors, reason) {
40
+ this._impl?.onTestRunEnd(testModules, unhandledErrors, reason);
41
+ }
42
+ }
43
+ class ReporterImpl {
44
+ constructor(_worktree, _commitId, _options, _logger) {
45
+ this._worktree = _worktree;
46
+ this._commitId = _commitId;
47
+ this._options = _options;
48
+ this._logger = _logger;
49
+ this._sampleSystem = this._sampleSystem.bind(this);
50
+ }
51
+ _telemetryTimer;
52
+ _cpuUtilization = new CPUUtilization({ precision: 10 });
53
+ _ramUtilization = new RAMUtilization({ precision: 10 });
54
+ _startTimestamp = Date.now();
55
+ _testCaseIdToTest = /* @__PURE__ */ new Map();
56
+ _testToTestCaseId = /* @__PURE__ */ new Map();
57
+ _stdio = /* @__PURE__ */ new Map();
58
+ _annotations = /* @__PURE__ */ new Map();
59
+ // In Vitest, all projects MUST HAVE UNIQUE NAMES.
60
+ // So we create environments per project name.
61
+ _environments = [];
62
+ _allSuites = /* @__PURE__ */ new Map();
63
+ _fileSuites = /* @__PURE__ */ new Map();
64
+ _configPath;
65
+ static create(rootPath, options, logger) {
66
+ let commitId;
67
+ let worktree;
68
+ try {
69
+ worktree = GitWorktree.create(rootPath);
70
+ commitId = worktree.headCommitId();
71
+ return new ReporterImpl(worktree, commitId, options, logger);
72
+ } catch (e) {
73
+ logger.warn(`[flakiness.io] Failed to fetch commit info - is this a git repo?`);
74
+ logger.error(`[flakiness.io] Report is NOT generated.`);
75
+ return;
76
+ }
77
+ }
78
+ _sampleSystem() {
79
+ this._cpuUtilization.sample();
80
+ this._ramUtilization.sample();
81
+ this._telemetryTimer = setTimeout(this._sampleSystem, 1e3);
82
+ }
83
+ onInit(vitest) {
84
+ this._configPath = vitest.config.config;
85
+ }
86
+ onUserConsoleLog(log) {
87
+ if (!log.taskId)
88
+ return;
89
+ let entries = this._stdio.get(log.taskId);
90
+ if (!entries) {
91
+ entries = [];
92
+ this._stdio.set(log.taskId, entries);
93
+ }
94
+ entries.push(log);
95
+ }
96
+ onTestCaseAnnotate(testCase, annotation) {
97
+ let entries = this._annotations.get(testCase.id);
98
+ if (!entries) {
99
+ entries = [];
100
+ this._annotations.set(testCase.id, entries);
101
+ }
102
+ entries.push(annotation);
103
+ }
104
+ onTestRunStart() {
105
+ this._startTimestamp = Date.now();
106
+ this._sampleSystem();
107
+ }
108
+ _ensureFKSuite(p) {
109
+ let suite = this._allSuites.get(p.id);
110
+ if (suite)
111
+ return suite;
112
+ if ("name" in p) {
113
+ const parent = this._ensureFKSuite(p.parent);
114
+ suite = {
115
+ type: "suite",
116
+ title: p.name,
117
+ location: p.location ? {
118
+ file: this._worktree.gitPath(p.module.moduleId),
119
+ column: p.location.column,
120
+ line: p.location.line
121
+ } : void 0
122
+ };
123
+ parent.suites ??= [];
124
+ parent.suites.push(suite);
125
+ } else {
126
+ suite = {
127
+ type: "file",
128
+ title: p.relativeModuleId,
129
+ location: {
130
+ file: this._worktree.gitPath(p.moduleId),
131
+ column: 0,
132
+ line: 0
133
+ }
134
+ };
135
+ this._fileSuites.set(p.id, suite);
136
+ }
137
+ this._allSuites.set(p.id, suite);
138
+ return suite;
139
+ }
140
+ _ensureTest(testCase) {
141
+ let fkTest = this._testCaseIdToTest.get(testCase.id);
142
+ if (!fkTest) {
143
+ fkTest = {
144
+ attempts: [],
145
+ title: testCase.name,
146
+ location: testCase.location ? {
147
+ file: this._worktree.gitPath(testCase.module.moduleId),
148
+ column: testCase.location.column,
149
+ line: testCase.location.line
150
+ } : void 0
151
+ };
152
+ this._testToTestCaseId.set(fkTest, testCase.id);
153
+ this._testCaseIdToTest.set(testCase.id, fkTest);
154
+ const parent = this._ensureFKSuite(testCase.parent);
155
+ parent.tests ??= [];
156
+ parent.tests.push(fkTest);
157
+ }
158
+ return fkTest;
159
+ }
160
+ _ensureEnvironmentIdx(testCase) {
161
+ let idx = this._environments.findIndex((env) => env.name === testCase.project.name);
162
+ if (idx === -1) {
163
+ idx = this._environments.length;
164
+ this._environments.push(ReportUtils.createEnvironment({
165
+ name: testCase.project.name
166
+ }));
167
+ }
168
+ return idx;
169
+ }
170
+ _errorLocation(stacks, testFile) {
171
+ const frame = stacks?.find((frame2) => this._worktree.gitPath(frame2.file) === testFile) ?? stacks?.find((frame2) => !frame2.file.includes("node_modules"));
172
+ return frame ? {
173
+ file: this._worktree.gitPath(frame.file),
174
+ line: frame.line,
175
+ column: frame.column
176
+ } : void 0;
177
+ }
178
+ async onTestCaseResult(testCase) {
179
+ const environmentIdx = this._ensureEnvironmentIdx(testCase);
180
+ const fkTest = this._ensureTest(testCase);
181
+ const result = testCase.result();
182
+ if (result.state === "skipped") {
183
+ fkTest.attempts.push({
184
+ environmentIdx,
185
+ startTimestamp: Date.now(),
186
+ duration: 0,
187
+ status: "skipped",
188
+ expectedStatus: "skipped"
189
+ });
190
+ return;
191
+ }
192
+ const diag = testCase.diagnostic();
193
+ assert(diag, `Diagnostic must be present in finished test cases`);
194
+ ;
195
+ const stdio = [];
196
+ let ts = diag.startTime;
197
+ for (const entry of this._stdio.get(testCase.id) ?? []) {
198
+ stdio.push({
199
+ text: entry.content,
200
+ stream: entry.type === "stdout" ? FK.STREAM_STDOUT : FK.STREAM_STDERR,
201
+ dts: entry.time - ts
202
+ });
203
+ ts = entry.time;
204
+ }
205
+ this._stdio.delete(testCase.id);
206
+ const testFile = this._worktree.gitPath(testCase.module.moduleId);
207
+ const errors = (result.errors ?? []).map((error) => ({
208
+ message: error.message,
209
+ stack: error.stack,
210
+ location: this._errorLocation(error.stacks, testFile)
211
+ }));
212
+ const annotations = (this._annotations.get(testCase.id) ?? []).map((annotation) => ({
213
+ type: annotation.type,
214
+ description: annotation.message,
215
+ location: annotation.location ? {
216
+ file: this._worktree.gitPath(annotation.location.file),
217
+ line: annotation.location.line,
218
+ column: annotation.location.column
219
+ } : void 0
220
+ }));
221
+ const expectedStatus = testCase.options.fails ? "failed" : "passed";
222
+ const oppositeStatus = expectedStatus === "failed" ? "passed" : "failed";
223
+ for (let i = 0; i < diag.retryCount; ++i) {
224
+ fkTest.attempts.push({
225
+ environmentIdx,
226
+ startTimestamp: diag.startTime,
227
+ duration: 0,
228
+ // retries have an opposite status from expected status to
229
+ // trigger retry.
230
+ status: oppositeStatus,
231
+ expectedStatus,
232
+ // TODO: ideally, we can differentiate STDIO between attempts.
233
+ // However, vitest doesn't let us do so easily.
234
+ stdio,
235
+ errors,
236
+ annotations
237
+ });
238
+ }
239
+ fkTest.attempts.push({
240
+ environmentIdx,
241
+ startTimestamp: diag.startTime,
242
+ duration: diag.duration,
243
+ status: result.state === "failed" ? oppositeStatus : expectedStatus,
244
+ expectedStatus,
245
+ // TODO: ideally, we can differentiate STDIO between attempts.
246
+ // However, vitest doesn't let us do so easily.
247
+ stdio,
248
+ errors,
249
+ annotations
250
+ });
251
+ }
252
+ _detectAndHandleTestDuplicates() {
253
+ const TITLE_SEPARATOR = crypto.randomBytes(128).toString("base64");
254
+ const testIdToTests = /* @__PURE__ */ new Map();
255
+ const testFullNames = /* @__PURE__ */ new Map();
256
+ function visitSuite(suite, parentTitles = []) {
257
+ parentTitles.push(suite.title);
258
+ for (const childSuite of suite.suites ?? [])
259
+ visitSuite(childSuite, parentTitles);
260
+ for (const test of suite.tests ?? []) {
261
+ const envs = Array.from(new Set(test.attempts.map((attempt) => attempt.environmentIdx ?? 0))).sort((a, b) => a - b);
262
+ const testId = [`[${envs.join(", ")}]`, ...parentTitles, test.title].join(TITLE_SEPARATOR);
263
+ testFullNames.set(test, [...parentTitles, test.title].join(" > "));
264
+ let tests = testIdToTests.get(testId);
265
+ if (!tests) {
266
+ tests = [];
267
+ testIdToTests.set(testId, tests);
268
+ }
269
+ tests.push(test);
270
+ }
271
+ parentTitles.pop();
272
+ }
273
+ for (const fileSuite of this._fileSuites.values())
274
+ visitSuite(fileSuite);
275
+ if (Array.from(testIdToTests.values()).every((tests) => tests.length <= 1))
276
+ return;
277
+ const warnMessages = [];
278
+ for (const [testId, tests] of testIdToTests) {
279
+ if (tests.length <= 1)
280
+ continue;
281
+ const testFullName = testFullNames.get(tests[0]);
282
+ const warnMessage = `${tests.length} tests: ${testFullName}`;
283
+ warnMessages.push(warnMessage);
284
+ const test = tests[0];
285
+ const envs = new Set(test.attempts.map((a) => a.environmentIdx ?? 0));
286
+ test.attempts = Array.from(envs).map((envIdx) => ({
287
+ environmentIdx: envIdx,
288
+ startTimestamp: Date.now(),
289
+ duration: 0,
290
+ expectedStatus: "passed",
291
+ status: "failed",
292
+ errors: [{
293
+ message: [
294
+ `Flakiness.io detected ${tests.length} tests with identical full name "${testFullName}"`,
295
+ `Please rename tests to ensure they all have unique full names.`
296
+ ].join("\n")
297
+ }],
298
+ annotations: [{
299
+ type: "duplicates",
300
+ description: [
301
+ `Flakiness.io failed to process this test because there are ${tests.length} tests with`,
302
+ `identical full name: "${testFullName}".`,
303
+ `Please make sure that all your tests have unique full names.`
304
+ ].join("\n")
305
+ }]
306
+ }));
307
+ for (let i = 1; i < tests.length; ++i)
308
+ tests[i].attempts = [];
309
+ }
310
+ if (warnMessages.length) {
311
+ this._logger.warn(`[flakiness.io] \u26A0 Detected test with duplicate names!`);
312
+ for (const warnMessage of warnMessages)
313
+ this._logger.warn(`[flakiness.io] - ${warnMessage}`);
314
+ this._logger.warn(`[flakiness.io] Please rename tests so that they all have unique full names.`);
315
+ }
316
+ }
317
+ async onTestRunEnd(testModules, unhandledErrors, reason) {
318
+ clearTimeout(this._telemetryTimer);
319
+ this._cpuUtilization.sample();
320
+ this._ramUtilization.sample();
321
+ this._detectAndHandleTestDuplicates();
322
+ const duration = Date.now() - this._startTimestamp;
323
+ const report = ReportUtils.normalizeReport({
324
+ url: CIUtils.runUrl(),
325
+ flakinessProject: this._options.flakinessProject,
326
+ category: "vitest",
327
+ configPath: this._configPath ? this._worktree.gitPath(this._configPath) : void 0,
328
+ commitId: this._commitId,
329
+ environments: this._environments,
330
+ startTimestamp: this._startTimestamp,
331
+ duration,
332
+ suites: Array.from(this._fileSuites.values()),
333
+ unattributedErrors: unhandledErrors.map((error) => ({
334
+ message: error.message,
335
+ stack: error.stack,
336
+ location: this._errorLocation(error.stacks)
337
+ }))
338
+ });
339
+ await ReportUtils.collectSources(this._worktree, report);
340
+ this._cpuUtilization.enrich(report);
341
+ this._ramUtilization.enrich(report);
342
+ const outputFolder = this._options.outputFolder ?? path.join(
343
+ process.cwd(),
344
+ process.env.FLAKINESS_OUTPUT_DIR ?? "flakiness-report"
345
+ );
346
+ await writeReport(report, [], outputFolder);
347
+ const disableUpload = !!this._options.disableUpload || !!process.env.FLAKINESS_DISABLE_UPLOAD;
348
+ if (!disableUpload) {
349
+ await uploadReport(report, [], {
350
+ flakinessAccessToken: this._options.token,
351
+ flakinessEndpoint: this._options.endpoint,
352
+ logger: this._logger
353
+ });
354
+ }
355
+ const openMode = this._options.open ?? "on-failure";
356
+ const shouldOpen = process.stdin.isTTY && !process.env.CI && (openMode === "always" || openMode === "on-failure" && reason === "failed");
357
+ if (shouldOpen) {
358
+ await showReport(outputFolder);
359
+ } else {
360
+ const defaultOutputFolder = path.join(process.cwd(), "flakiness-report");
361
+ const folder = defaultOutputFolder === outputFolder ? "" : path.relative(process.cwd(), outputFolder);
362
+ this._logger.log(`
363
+ To open last Flakiness report, run:
364
+
365
+ ${chalk.cyan(`npx flakiness show ${folder}`)}
366
+ `);
367
+ }
368
+ }
369
+ }
370
+ function dupeSuffix(dupeIndex) {
371
+ return ` \u2013 dupe #${dupeIndex}`;
372
+ }
373
+ export {
374
+ FKVitestReporter as default
375
+ };
376
+ //# sourceMappingURL=reporter.js.map
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@flakiness/vitest",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "keywords": [],
7
+ "author": "",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/flakiness/vitest"
12
+ },
13
+ "type": "module",
14
+ "devDependencies": {
15
+ "@types/node": "^25.3.3",
16
+ "@vitest/utils": "^4.0.18",
17
+ "esbuild": "^0.27.3",
18
+ "kubik": "^0.24.0",
19
+ "tsx": "^4.21.0",
20
+ "typescript": "^5.9.3",
21
+ "vitest": "^4.0.18"
22
+ },
23
+ "dependencies": {
24
+ "@flakiness/flakiness-report": "^0.27.0",
25
+ "@flakiness/sdk": "^2.2.0",
26
+ "chalk": "^5.6.2"
27
+ },
28
+ "scripts": {
29
+ "build": "kubik build.mts",
30
+ "test": "vitest",
31
+ "test:run": "vitest run"
32
+ }
33
+ }
@@ -0,0 +1,65 @@
1
+ import type { SerializedError, TestCase, TestModule, TestRunEndReason, Vitest } from 'vitest/node';
2
+ import type { Reporter } from 'vitest/reporters';
3
+ interface UserConsoleLog {
4
+ content: string;
5
+ origin?: string;
6
+ browser?: boolean;
7
+ type: "stdout" | "stderr";
8
+ taskId?: string;
9
+ time: number;
10
+ size: number;
11
+ }
12
+ interface TestArtifactLocation {
13
+ /** Line number in the source file (1-indexed) */
14
+ line: number;
15
+ /** Column number in the line (1-indexed) */
16
+ column: number;
17
+ /** Path to the source file */
18
+ file: string;
19
+ }
20
+ interface TestAttachment {
21
+ /** MIME type of the attachment (e.g., 'image/png', 'text/plain') */
22
+ contentType?: string;
23
+ /** File system path to the attachment */
24
+ path?: string;
25
+ /** Inline attachment content as a string or raw binary data */
26
+ body?: string | Uint8Array;
27
+ }
28
+ interface TestAnnotation {
29
+ message: string;
30
+ type: string;
31
+ location?: TestArtifactLocation;
32
+ attachment?: TestAttachment;
33
+ }
34
+ export type OpenMode = 'always' | 'never' | 'on-failure';
35
+ export type FKVitestReporterOptions = {
36
+ disableUpload?: boolean;
37
+ flakinessProject?: string;
38
+ endpoint?: string;
39
+ token?: string;
40
+ outputFolder?: string;
41
+ open?: OpenMode;
42
+ };
43
+ export interface FKVitestLogger {
44
+ log(txt: string): void;
45
+ warn(txt: string): void;
46
+ error(txt: string): void;
47
+ }
48
+ export default class FKVitestReporter implements Reporter {
49
+ private _options;
50
+ private _impl?;
51
+ private _logger;
52
+ constructor(_options: FKVitestReporterOptions);
53
+ setLoggerForTest(logger: FKVitestLogger): void;
54
+ onUserConsoleLog(log: UserConsoleLog): void;
55
+ onInit(vitest: Vitest): void;
56
+ /**
57
+ * Called when annotation is added via the `task.annotate` API.
58
+ */
59
+ onTestCaseAnnotate(testCase: TestCase, annotation: TestAnnotation): void;
60
+ onTestRunStart(): void;
61
+ onTestCaseResult(testCase: TestCase): Promise<void>;
62
+ onTestRunEnd(testModules: ReadonlyArray<TestModule>, unhandledErrors: ReadonlyArray<SerializedError>, reason: TestRunEndReason): Promise<void>;
63
+ }
64
+ export {};
65
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/reporter.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAa,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIjD,UAAU,cAAc;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AACD,UAAU,oBAAoB;IAC7B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;CACb;AACD,UAAU,cAAc;IACvB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;CAC3B;AACD,UAAU,cAAc;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;AAEzD,MAAM,MAAM,uBAAuB,GAAG;IACpC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,OAAO,gBAAiB,YAAW,QAAQ;IAQ3C,OAAO,CAAC,QAAQ;IAP5B,OAAO,CAAC,KAAK,CAAC,CAAe;IAC7B,OAAO,CAAC,OAAO,CAId;gBAEmB,QAAQ,EAAE,uBAAuB;IAGrD,gBAAgB,CAAC,MAAM,EAAE,cAAc;IAIvC,gBAAgB,CAAC,GAAG,EAAE,cAAc;IAIpC,MAAM,CAAC,MAAM,EAAE,MAAM;IAKtB;;MAEE;IACF,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc;IAIhE,cAAc;IAIR,gBAAgB,CAAC,QAAQ,EAAE,QAAQ;IAInC,YAAY,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,eAAe,EAAE,aAAa,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,gBAAgB;CAGrI"}