@bugzy-ai/bugzy 1.18.2 → 1.18.4
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 +21 -21
- package/README.md +273 -273
- package/dist/cli/index.cjs +295 -210
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +294 -209
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +291 -206
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +291 -206
- package/dist/index.js.map +1 -1
- package/dist/subagents/index.cjs +115 -137
- package/dist/subagents/index.cjs.map +1 -1
- package/dist/subagents/index.js +115 -137
- package/dist/subagents/index.js.map +1 -1
- package/dist/subagents/metadata.cjs +12 -18
- package/dist/subagents/metadata.cjs.map +1 -1
- package/dist/subagents/metadata.js +12 -18
- package/dist/subagents/metadata.js.map +1 -1
- package/dist/tasks/index.cjs +142 -54
- package/dist/tasks/index.cjs.map +1 -1
- package/dist/tasks/index.js +142 -54
- package/dist/tasks/index.js.map +1 -1
- package/dist/templates/init/.bugzy/runtime/knowledge-base.md +61 -0
- package/dist/templates/init/.bugzy/runtime/knowledge-maintenance-guide.md +97 -0
- package/dist/templates/init/.bugzy/runtime/project-context.md +35 -0
- package/dist/templates/init/.bugzy/runtime/subagent-memory-guide.md +87 -0
- package/dist/templates/init/.bugzy/runtime/templates/test-plan-template.md +50 -0
- package/dist/templates/init/.bugzy/runtime/templates/test-result-schema.md +498 -0
- package/dist/templates/init/.bugzy/runtime/test-execution-strategy.md +535 -0
- package/dist/templates/init/.bugzy/runtime/testing-best-practices.md +632 -0
- package/dist/templates/init/.gitignore-template +25 -0
- package/package.json +95 -95
- package/templates/init/.bugzy/runtime/knowledge-base.md +61 -61
- package/templates/init/.bugzy/runtime/knowledge-maintenance-guide.md +97 -97
- package/templates/init/.bugzy/runtime/project-context.md +35 -35
- package/templates/init/.bugzy/runtime/subagent-memory-guide.md +87 -87
- package/templates/init/.bugzy/runtime/templates/event-examples.md +194 -194
- package/templates/init/.bugzy/runtime/templates/test-plan-template.md +50 -50
- package/templates/init/.bugzy/runtime/templates/test-result-schema.md +498 -498
- package/templates/init/.claude/settings.json +28 -28
- package/templates/init/.env.testdata +18 -18
- package/templates/init/.gitignore-template +24 -24
- package/templates/init/AGENTS.md +155 -155
- package/templates/init/CLAUDE.md +157 -157
- package/templates/init/test-runs/README.md +45 -45
- package/templates/init/tests/CLAUDE.md +193 -193
- package/templates/init/tests/docs/test-execution-strategy.md +535 -535
- package/templates/init/tests/docs/testing-best-practices.md +724 -724
- package/templates/playwright/BasePage.template.ts +190 -190
- package/templates/playwright/auth.setup.template.ts +89 -89
- package/templates/playwright/dataGenerators.helper.template.ts +148 -148
- package/templates/playwright/dateUtils.helper.template.ts +96 -96
- package/templates/playwright/pages.fixture.template.ts +50 -50
- package/templates/playwright/playwright.config.template.ts +97 -97
- package/templates/playwright/reporters/__tests__/bugzy-reporter-failure-classification.test.ts +299 -299
- package/templates/playwright/reporters/__tests__/bugzy-reporter-manifest-merge.test.ts +329 -329
- package/templates/playwright/reporters/__tests__/playwright.config.ts +5 -5
- package/templates/playwright/reporters/bugzy-reporter.ts +784 -784
- package/templates/init/.bugzy/runtime/handlers/messages/feedback.md +0 -178
- package/templates/init/.bugzy/runtime/handlers/messages/question.md +0 -122
- package/templates/init/.bugzy/runtime/handlers/messages/status.md +0 -146
package/templates/playwright/reporters/__tests__/bugzy-reporter-failure-classification.test.ts
CHANGED
|
@@ -1,299 +1,299 @@
|
|
|
1
|
-
import { test, expect } from '@playwright/test';
|
|
2
|
-
import { classifyFailures } from '../bugzy-reporter';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
|
|
7
|
-
function makeManifest(overrides: Partial<{
|
|
8
|
-
timestamp: string;
|
|
9
|
-
testCases: Array<{
|
|
10
|
-
id: string;
|
|
11
|
-
name: string;
|
|
12
|
-
totalExecutions: number;
|
|
13
|
-
finalStatus: string;
|
|
14
|
-
executions: Array<{
|
|
15
|
-
number: number;
|
|
16
|
-
status: string;
|
|
17
|
-
duration: number;
|
|
18
|
-
videoFile: string | null;
|
|
19
|
-
hasTrace: boolean;
|
|
20
|
-
hasScreenshots: boolean;
|
|
21
|
-
error: string | null;
|
|
22
|
-
}>;
|
|
23
|
-
}>;
|
|
24
|
-
}> = {}) {
|
|
25
|
-
const testCases = overrides.testCases ?? [];
|
|
26
|
-
const totalExecutions = testCases.reduce((sum, tc) => sum + tc.executions.length, 0);
|
|
27
|
-
const passed = testCases.filter(tc => tc.finalStatus === 'passed').length;
|
|
28
|
-
const failed = testCases.length - passed;
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
bugzyExecutionId: 'local-test',
|
|
32
|
-
timestamp: overrides.timestamp ?? '20260216-120000',
|
|
33
|
-
startTime: '2026-02-16T12:00:00.000Z',
|
|
34
|
-
endTime: '2026-02-16T12:01:00.000Z',
|
|
35
|
-
status: failed > 0 ? 'failed' : 'passed',
|
|
36
|
-
stats: { totalTests: testCases.length, passed, failed, totalExecutions },
|
|
37
|
-
testCases,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function makeTestCase(id: string, finalStatus: string, error?: string) {
|
|
42
|
-
return {
|
|
43
|
-
id,
|
|
44
|
-
name: id.replace(/^TC-\d+-/, '').replace(/-/g, ' '),
|
|
45
|
-
totalExecutions: 1,
|
|
46
|
-
finalStatus,
|
|
47
|
-
executions: [{
|
|
48
|
-
number: 1,
|
|
49
|
-
status: finalStatus,
|
|
50
|
-
duration: 1000,
|
|
51
|
-
videoFile: null,
|
|
52
|
-
hasTrace: false,
|
|
53
|
-
hasScreenshots: false,
|
|
54
|
-
error: error ?? null,
|
|
55
|
-
}],
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function setupTestRunsDir(manifests: Array<{ timestamp: string; manifest: any }>) {
|
|
60
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bugzy-test-'));
|
|
61
|
-
const testRunsRoot = path.join(tmpDir, 'test-runs');
|
|
62
|
-
fs.mkdirSync(testRunsRoot, { recursive: true });
|
|
63
|
-
|
|
64
|
-
for (const { timestamp, manifest } of manifests) {
|
|
65
|
-
const runDir = path.join(testRunsRoot, timestamp);
|
|
66
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
67
|
-
fs.writeFileSync(
|
|
68
|
-
path.join(runDir, 'manifest.json'),
|
|
69
|
-
JSON.stringify(manifest, null, 2)
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return testRunsRoot;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
test.describe('classifyFailures', () => {
|
|
77
|
-
test('returns empty arrays when no failures', () => {
|
|
78
|
-
const manifest = makeManifest({
|
|
79
|
-
testCases: [makeTestCase('TC-001-login', 'passed')],
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const result = classifyFailures(manifest, '/nonexistent');
|
|
83
|
-
|
|
84
|
-
expect(result.newFailures).toHaveLength(0);
|
|
85
|
-
expect(result.knownFailures).toHaveLength(0);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('all failures are new when no previous runs exist', () => {
|
|
89
|
-
const manifest = makeManifest({
|
|
90
|
-
timestamp: '20260216-120000',
|
|
91
|
-
testCases: [
|
|
92
|
-
makeTestCase('TC-001-login', 'failed', 'timeout'),
|
|
93
|
-
makeTestCase('TC-002-checkout', 'failed', 'element not found'),
|
|
94
|
-
],
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const testRunsRoot = setupTestRunsDir([]);
|
|
98
|
-
|
|
99
|
-
const result = classifyFailures(manifest, testRunsRoot);
|
|
100
|
-
|
|
101
|
-
expect(result.newFailures).toHaveLength(2);
|
|
102
|
-
expect(result.knownFailures).toHaveLength(0);
|
|
103
|
-
expect(result.newFailures[0].id).toBe('TC-001-login');
|
|
104
|
-
expect(result.newFailures[0].error).toBe('timeout');
|
|
105
|
-
expect(result.newFailures[1].id).toBe('TC-002-checkout');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('failure is new when test passed in recent run', () => {
|
|
109
|
-
const previousManifest = makeManifest({
|
|
110
|
-
timestamp: '20260215-120000',
|
|
111
|
-
testCases: [
|
|
112
|
-
makeTestCase('TC-001-login', 'passed'),
|
|
113
|
-
makeTestCase('TC-002-checkout', 'passed'),
|
|
114
|
-
],
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const testRunsRoot = setupTestRunsDir([
|
|
118
|
-
{ timestamp: '20260215-120000', manifest: previousManifest },
|
|
119
|
-
]);
|
|
120
|
-
|
|
121
|
-
const currentManifest = makeManifest({
|
|
122
|
-
timestamp: '20260216-120000',
|
|
123
|
-
testCases: [
|
|
124
|
-
makeTestCase('TC-001-login', 'failed', 'timeout'),
|
|
125
|
-
],
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
129
|
-
|
|
130
|
-
expect(result.newFailures).toHaveLength(1);
|
|
131
|
-
expect(result.knownFailures).toHaveLength(0);
|
|
132
|
-
expect(result.newFailures[0].id).toBe('TC-001-login');
|
|
133
|
-
expect(result.newFailures[0].lastPassedRun).toBe('20260215-120000');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('failure is known when test failed in all previous runs', () => {
|
|
137
|
-
const prev1 = makeManifest({
|
|
138
|
-
timestamp: '20260215-120000',
|
|
139
|
-
testCases: [makeTestCase('TC-001-login', 'failed', 'timeout')],
|
|
140
|
-
});
|
|
141
|
-
const prev2 = makeManifest({
|
|
142
|
-
timestamp: '20260214-120000',
|
|
143
|
-
testCases: [makeTestCase('TC-001-login', 'failed', 'timeout')],
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const testRunsRoot = setupTestRunsDir([
|
|
147
|
-
{ timestamp: '20260215-120000', manifest: prev1 },
|
|
148
|
-
{ timestamp: '20260214-120000', manifest: prev2 },
|
|
149
|
-
]);
|
|
150
|
-
|
|
151
|
-
const currentManifest = makeManifest({
|
|
152
|
-
timestamp: '20260216-120000',
|
|
153
|
-
testCases: [makeTestCase('TC-001-login', 'failed', 'timeout')],
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
157
|
-
|
|
158
|
-
expect(result.newFailures).toHaveLength(0);
|
|
159
|
-
expect(result.knownFailures).toHaveLength(1);
|
|
160
|
-
expect(result.knownFailures[0].id).toBe('TC-001-login');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('mixed new and known failures', () => {
|
|
164
|
-
const previousManifest = makeManifest({
|
|
165
|
-
timestamp: '20260215-120000',
|
|
166
|
-
testCases: [
|
|
167
|
-
makeTestCase('TC-001-login', 'passed'),
|
|
168
|
-
makeTestCase('TC-002-checkout', 'failed', 'always broken'),
|
|
169
|
-
],
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const testRunsRoot = setupTestRunsDir([
|
|
173
|
-
{ timestamp: '20260215-120000', manifest: previousManifest },
|
|
174
|
-
]);
|
|
175
|
-
|
|
176
|
-
const currentManifest = makeManifest({
|
|
177
|
-
timestamp: '20260216-120000',
|
|
178
|
-
testCases: [
|
|
179
|
-
makeTestCase('TC-001-login', 'failed', 'new regression'),
|
|
180
|
-
makeTestCase('TC-002-checkout', 'failed', 'still broken'),
|
|
181
|
-
],
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
185
|
-
|
|
186
|
-
expect(result.newFailures).toHaveLength(1);
|
|
187
|
-
expect(result.newFailures[0].id).toBe('TC-001-login');
|
|
188
|
-
expect(result.newFailures[0].lastPassedRun).toBe('20260215-120000');
|
|
189
|
-
|
|
190
|
-
expect(result.knownFailures).toHaveLength(1);
|
|
191
|
-
expect(result.knownFailures[0].id).toBe('TC-002-checkout');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('new test not in history is treated as new failure', () => {
|
|
195
|
-
const previousManifest = makeManifest({
|
|
196
|
-
timestamp: '20260215-120000',
|
|
197
|
-
testCases: [makeTestCase('TC-001-login', 'passed')],
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const testRunsRoot = setupTestRunsDir([
|
|
201
|
-
{ timestamp: '20260215-120000', manifest: previousManifest },
|
|
202
|
-
]);
|
|
203
|
-
|
|
204
|
-
const currentManifest = makeManifest({
|
|
205
|
-
timestamp: '20260216-120000',
|
|
206
|
-
testCases: [
|
|
207
|
-
makeTestCase('TC-003-new-feature', 'failed', 'new test fails'),
|
|
208
|
-
],
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
212
|
-
|
|
213
|
-
expect(result.newFailures).toHaveLength(1);
|
|
214
|
-
expect(result.newFailures[0].id).toBe('TC-003-new-feature');
|
|
215
|
-
expect(result.newFailures[0].lastPassedRun).toBeNull();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test('respects BUGZY_FAILURE_LOOKBACK env var', () => {
|
|
219
|
-
// Set lookback to 1
|
|
220
|
-
const origEnv = process.env.BUGZY_FAILURE_LOOKBACK;
|
|
221
|
-
process.env.BUGZY_FAILURE_LOOKBACK = '1';
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
// Run 1: test passed
|
|
225
|
-
// Run 2: test failed
|
|
226
|
-
// Run 3 (current): test failed
|
|
227
|
-
// With lookback=1, only run 2 is checked (most recent)
|
|
228
|
-
const run1 = makeManifest({
|
|
229
|
-
timestamp: '20260213-120000',
|
|
230
|
-
testCases: [makeTestCase('TC-001-login', 'passed')],
|
|
231
|
-
});
|
|
232
|
-
const run2 = makeManifest({
|
|
233
|
-
timestamp: '20260214-120000',
|
|
234
|
-
testCases: [makeTestCase('TC-001-login', 'failed', 'broken')],
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const testRunsRoot = setupTestRunsDir([
|
|
238
|
-
{ timestamp: '20260213-120000', manifest: run1 },
|
|
239
|
-
{ timestamp: '20260214-120000', manifest: run2 },
|
|
240
|
-
]);
|
|
241
|
-
|
|
242
|
-
const currentManifest = makeManifest({
|
|
243
|
-
timestamp: '20260215-120000',
|
|
244
|
-
testCases: [makeTestCase('TC-001-login', 'failed', 'still broken')],
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
248
|
-
|
|
249
|
-
// With lookback=1, only sees run2 where test failed → known failure
|
|
250
|
-
expect(result.knownFailures).toHaveLength(1);
|
|
251
|
-
expect(result.newFailures).toHaveLength(0);
|
|
252
|
-
} finally {
|
|
253
|
-
if (origEnv !== undefined) {
|
|
254
|
-
process.env.BUGZY_FAILURE_LOOKBACK = origEnv;
|
|
255
|
-
} else {
|
|
256
|
-
delete process.env.BUGZY_FAILURE_LOOKBACK;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test('handles timedOut status as failure', () => {
|
|
262
|
-
const previousManifest = makeManifest({
|
|
263
|
-
timestamp: '20260215-120000',
|
|
264
|
-
testCases: [makeTestCase('TC-001-login', 'passed')],
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const testRunsRoot = setupTestRunsDir([
|
|
268
|
-
{ timestamp: '20260215-120000', manifest: previousManifest },
|
|
269
|
-
]);
|
|
270
|
-
|
|
271
|
-
const currentManifest = makeManifest({
|
|
272
|
-
timestamp: '20260216-120000',
|
|
273
|
-
testCases: [makeTestCase('TC-001-login', 'timedOut', 'Test timeout')],
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
277
|
-
|
|
278
|
-
expect(result.newFailures).toHaveLength(1);
|
|
279
|
-
expect(result.newFailures[0].id).toBe('TC-001-login');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('skips current run timestamp when reading previous manifests', () => {
|
|
283
|
-
// Only the current run exists - should be treated as first run
|
|
284
|
-
const currentManifest = makeManifest({
|
|
285
|
-
timestamp: '20260216-120000',
|
|
286
|
-
testCases: [makeTestCase('TC-001-login', 'failed', 'error')],
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const testRunsRoot = setupTestRunsDir([
|
|
290
|
-
{ timestamp: '20260216-120000', manifest: currentManifest },
|
|
291
|
-
]);
|
|
292
|
-
|
|
293
|
-
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
294
|
-
|
|
295
|
-
// First run - all failures are new
|
|
296
|
-
expect(result.newFailures).toHaveLength(1);
|
|
297
|
-
expect(result.knownFailures).toHaveLength(0);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { classifyFailures } from '../bugzy-reporter';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
function makeManifest(overrides: Partial<{
|
|
8
|
+
timestamp: string;
|
|
9
|
+
testCases: Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
totalExecutions: number;
|
|
13
|
+
finalStatus: string;
|
|
14
|
+
executions: Array<{
|
|
15
|
+
number: number;
|
|
16
|
+
status: string;
|
|
17
|
+
duration: number;
|
|
18
|
+
videoFile: string | null;
|
|
19
|
+
hasTrace: boolean;
|
|
20
|
+
hasScreenshots: boolean;
|
|
21
|
+
error: string | null;
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
}> = {}) {
|
|
25
|
+
const testCases = overrides.testCases ?? [];
|
|
26
|
+
const totalExecutions = testCases.reduce((sum, tc) => sum + tc.executions.length, 0);
|
|
27
|
+
const passed = testCases.filter(tc => tc.finalStatus === 'passed').length;
|
|
28
|
+
const failed = testCases.length - passed;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
bugzyExecutionId: 'local-test',
|
|
32
|
+
timestamp: overrides.timestamp ?? '20260216-120000',
|
|
33
|
+
startTime: '2026-02-16T12:00:00.000Z',
|
|
34
|
+
endTime: '2026-02-16T12:01:00.000Z',
|
|
35
|
+
status: failed > 0 ? 'failed' : 'passed',
|
|
36
|
+
stats: { totalTests: testCases.length, passed, failed, totalExecutions },
|
|
37
|
+
testCases,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeTestCase(id: string, finalStatus: string, error?: string) {
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
name: id.replace(/^TC-\d+-/, '').replace(/-/g, ' '),
|
|
45
|
+
totalExecutions: 1,
|
|
46
|
+
finalStatus,
|
|
47
|
+
executions: [{
|
|
48
|
+
number: 1,
|
|
49
|
+
status: finalStatus,
|
|
50
|
+
duration: 1000,
|
|
51
|
+
videoFile: null,
|
|
52
|
+
hasTrace: false,
|
|
53
|
+
hasScreenshots: false,
|
|
54
|
+
error: error ?? null,
|
|
55
|
+
}],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setupTestRunsDir(manifests: Array<{ timestamp: string; manifest: any }>) {
|
|
60
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bugzy-test-'));
|
|
61
|
+
const testRunsRoot = path.join(tmpDir, 'test-runs');
|
|
62
|
+
fs.mkdirSync(testRunsRoot, { recursive: true });
|
|
63
|
+
|
|
64
|
+
for (const { timestamp, manifest } of manifests) {
|
|
65
|
+
const runDir = path.join(testRunsRoot, timestamp);
|
|
66
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(runDir, 'manifest.json'),
|
|
69
|
+
JSON.stringify(manifest, null, 2)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return testRunsRoot;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test.describe('classifyFailures', () => {
|
|
77
|
+
test('returns empty arrays when no failures', () => {
|
|
78
|
+
const manifest = makeManifest({
|
|
79
|
+
testCases: [makeTestCase('TC-001-login', 'passed')],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const result = classifyFailures(manifest, '/nonexistent');
|
|
83
|
+
|
|
84
|
+
expect(result.newFailures).toHaveLength(0);
|
|
85
|
+
expect(result.knownFailures).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('all failures are new when no previous runs exist', () => {
|
|
89
|
+
const manifest = makeManifest({
|
|
90
|
+
timestamp: '20260216-120000',
|
|
91
|
+
testCases: [
|
|
92
|
+
makeTestCase('TC-001-login', 'failed', 'timeout'),
|
|
93
|
+
makeTestCase('TC-002-checkout', 'failed', 'element not found'),
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const testRunsRoot = setupTestRunsDir([]);
|
|
98
|
+
|
|
99
|
+
const result = classifyFailures(manifest, testRunsRoot);
|
|
100
|
+
|
|
101
|
+
expect(result.newFailures).toHaveLength(2);
|
|
102
|
+
expect(result.knownFailures).toHaveLength(0);
|
|
103
|
+
expect(result.newFailures[0].id).toBe('TC-001-login');
|
|
104
|
+
expect(result.newFailures[0].error).toBe('timeout');
|
|
105
|
+
expect(result.newFailures[1].id).toBe('TC-002-checkout');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('failure is new when test passed in recent run', () => {
|
|
109
|
+
const previousManifest = makeManifest({
|
|
110
|
+
timestamp: '20260215-120000',
|
|
111
|
+
testCases: [
|
|
112
|
+
makeTestCase('TC-001-login', 'passed'),
|
|
113
|
+
makeTestCase('TC-002-checkout', 'passed'),
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const testRunsRoot = setupTestRunsDir([
|
|
118
|
+
{ timestamp: '20260215-120000', manifest: previousManifest },
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
const currentManifest = makeManifest({
|
|
122
|
+
timestamp: '20260216-120000',
|
|
123
|
+
testCases: [
|
|
124
|
+
makeTestCase('TC-001-login', 'failed', 'timeout'),
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
129
|
+
|
|
130
|
+
expect(result.newFailures).toHaveLength(1);
|
|
131
|
+
expect(result.knownFailures).toHaveLength(0);
|
|
132
|
+
expect(result.newFailures[0].id).toBe('TC-001-login');
|
|
133
|
+
expect(result.newFailures[0].lastPassedRun).toBe('20260215-120000');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('failure is known when test failed in all previous runs', () => {
|
|
137
|
+
const prev1 = makeManifest({
|
|
138
|
+
timestamp: '20260215-120000',
|
|
139
|
+
testCases: [makeTestCase('TC-001-login', 'failed', 'timeout')],
|
|
140
|
+
});
|
|
141
|
+
const prev2 = makeManifest({
|
|
142
|
+
timestamp: '20260214-120000',
|
|
143
|
+
testCases: [makeTestCase('TC-001-login', 'failed', 'timeout')],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const testRunsRoot = setupTestRunsDir([
|
|
147
|
+
{ timestamp: '20260215-120000', manifest: prev1 },
|
|
148
|
+
{ timestamp: '20260214-120000', manifest: prev2 },
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const currentManifest = makeManifest({
|
|
152
|
+
timestamp: '20260216-120000',
|
|
153
|
+
testCases: [makeTestCase('TC-001-login', 'failed', 'timeout')],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
157
|
+
|
|
158
|
+
expect(result.newFailures).toHaveLength(0);
|
|
159
|
+
expect(result.knownFailures).toHaveLength(1);
|
|
160
|
+
expect(result.knownFailures[0].id).toBe('TC-001-login');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('mixed new and known failures', () => {
|
|
164
|
+
const previousManifest = makeManifest({
|
|
165
|
+
timestamp: '20260215-120000',
|
|
166
|
+
testCases: [
|
|
167
|
+
makeTestCase('TC-001-login', 'passed'),
|
|
168
|
+
makeTestCase('TC-002-checkout', 'failed', 'always broken'),
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const testRunsRoot = setupTestRunsDir([
|
|
173
|
+
{ timestamp: '20260215-120000', manifest: previousManifest },
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
const currentManifest = makeManifest({
|
|
177
|
+
timestamp: '20260216-120000',
|
|
178
|
+
testCases: [
|
|
179
|
+
makeTestCase('TC-001-login', 'failed', 'new regression'),
|
|
180
|
+
makeTestCase('TC-002-checkout', 'failed', 'still broken'),
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
185
|
+
|
|
186
|
+
expect(result.newFailures).toHaveLength(1);
|
|
187
|
+
expect(result.newFailures[0].id).toBe('TC-001-login');
|
|
188
|
+
expect(result.newFailures[0].lastPassedRun).toBe('20260215-120000');
|
|
189
|
+
|
|
190
|
+
expect(result.knownFailures).toHaveLength(1);
|
|
191
|
+
expect(result.knownFailures[0].id).toBe('TC-002-checkout');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('new test not in history is treated as new failure', () => {
|
|
195
|
+
const previousManifest = makeManifest({
|
|
196
|
+
timestamp: '20260215-120000',
|
|
197
|
+
testCases: [makeTestCase('TC-001-login', 'passed')],
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const testRunsRoot = setupTestRunsDir([
|
|
201
|
+
{ timestamp: '20260215-120000', manifest: previousManifest },
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
const currentManifest = makeManifest({
|
|
205
|
+
timestamp: '20260216-120000',
|
|
206
|
+
testCases: [
|
|
207
|
+
makeTestCase('TC-003-new-feature', 'failed', 'new test fails'),
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
212
|
+
|
|
213
|
+
expect(result.newFailures).toHaveLength(1);
|
|
214
|
+
expect(result.newFailures[0].id).toBe('TC-003-new-feature');
|
|
215
|
+
expect(result.newFailures[0].lastPassedRun).toBeNull();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('respects BUGZY_FAILURE_LOOKBACK env var', () => {
|
|
219
|
+
// Set lookback to 1
|
|
220
|
+
const origEnv = process.env.BUGZY_FAILURE_LOOKBACK;
|
|
221
|
+
process.env.BUGZY_FAILURE_LOOKBACK = '1';
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
// Run 1: test passed
|
|
225
|
+
// Run 2: test failed
|
|
226
|
+
// Run 3 (current): test failed
|
|
227
|
+
// With lookback=1, only run 2 is checked (most recent)
|
|
228
|
+
const run1 = makeManifest({
|
|
229
|
+
timestamp: '20260213-120000',
|
|
230
|
+
testCases: [makeTestCase('TC-001-login', 'passed')],
|
|
231
|
+
});
|
|
232
|
+
const run2 = makeManifest({
|
|
233
|
+
timestamp: '20260214-120000',
|
|
234
|
+
testCases: [makeTestCase('TC-001-login', 'failed', 'broken')],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const testRunsRoot = setupTestRunsDir([
|
|
238
|
+
{ timestamp: '20260213-120000', manifest: run1 },
|
|
239
|
+
{ timestamp: '20260214-120000', manifest: run2 },
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
const currentManifest = makeManifest({
|
|
243
|
+
timestamp: '20260215-120000',
|
|
244
|
+
testCases: [makeTestCase('TC-001-login', 'failed', 'still broken')],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
248
|
+
|
|
249
|
+
// With lookback=1, only sees run2 where test failed → known failure
|
|
250
|
+
expect(result.knownFailures).toHaveLength(1);
|
|
251
|
+
expect(result.newFailures).toHaveLength(0);
|
|
252
|
+
} finally {
|
|
253
|
+
if (origEnv !== undefined) {
|
|
254
|
+
process.env.BUGZY_FAILURE_LOOKBACK = origEnv;
|
|
255
|
+
} else {
|
|
256
|
+
delete process.env.BUGZY_FAILURE_LOOKBACK;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('handles timedOut status as failure', () => {
|
|
262
|
+
const previousManifest = makeManifest({
|
|
263
|
+
timestamp: '20260215-120000',
|
|
264
|
+
testCases: [makeTestCase('TC-001-login', 'passed')],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const testRunsRoot = setupTestRunsDir([
|
|
268
|
+
{ timestamp: '20260215-120000', manifest: previousManifest },
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
const currentManifest = makeManifest({
|
|
272
|
+
timestamp: '20260216-120000',
|
|
273
|
+
testCases: [makeTestCase('TC-001-login', 'timedOut', 'Test timeout')],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
277
|
+
|
|
278
|
+
expect(result.newFailures).toHaveLength(1);
|
|
279
|
+
expect(result.newFailures[0].id).toBe('TC-001-login');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('skips current run timestamp when reading previous manifests', () => {
|
|
283
|
+
// Only the current run exists - should be treated as first run
|
|
284
|
+
const currentManifest = makeManifest({
|
|
285
|
+
timestamp: '20260216-120000',
|
|
286
|
+
testCases: [makeTestCase('TC-001-login', 'failed', 'error')],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const testRunsRoot = setupTestRunsDir([
|
|
290
|
+
{ timestamp: '20260216-120000', manifest: currentManifest },
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
const result = classifyFailures(currentManifest, testRunsRoot);
|
|
294
|
+
|
|
295
|
+
// First run - all failures are new
|
|
296
|
+
expect(result.newFailures).toHaveLength(1);
|
|
297
|
+
expect(result.knownFailures).toHaveLength(0);
|
|
298
|
+
});
|
|
299
|
+
});
|