@bugzy-ai/bugzy 1.18.0 → 1.18.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.
Files changed (59) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +273 -273
  3. package/dist/cli/index.cjs +227 -5
  4. package/dist/cli/index.cjs.map +1 -1
  5. package/dist/cli/index.js +226 -4
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.cjs +214 -2
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.js +214 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/subagents/index.cjs.map +1 -1
  12. package/dist/subagents/index.js.map +1 -1
  13. package/dist/subagents/metadata.cjs.map +1 -1
  14. package/dist/subagents/metadata.js.map +1 -1
  15. package/dist/tasks/index.cjs +130 -2
  16. package/dist/tasks/index.cjs.map +1 -1
  17. package/dist/tasks/index.d.cts +1 -0
  18. package/dist/tasks/index.d.ts +1 -0
  19. package/dist/tasks/index.js +130 -2
  20. package/dist/tasks/index.js.map +1 -1
  21. package/package.json +95 -95
  22. package/templates/init/.bugzy/runtime/handlers/messages/feedback.md +178 -178
  23. package/templates/init/.bugzy/runtime/handlers/messages/question.md +122 -122
  24. package/templates/init/.bugzy/runtime/handlers/messages/status.md +146 -146
  25. package/templates/init/.bugzy/runtime/knowledge-base.md +61 -61
  26. package/templates/init/.bugzy/runtime/knowledge-maintenance-guide.md +97 -97
  27. package/templates/init/.bugzy/runtime/project-context.md +35 -35
  28. package/templates/init/.bugzy/runtime/subagent-memory-guide.md +87 -87
  29. package/templates/init/.bugzy/runtime/templates/event-examples.md +194 -194
  30. package/templates/init/.bugzy/runtime/templates/test-plan-template.md +50 -50
  31. package/templates/init/.bugzy/runtime/templates/test-result-schema.md +498 -498
  32. package/templates/init/.claude/settings.json +28 -28
  33. package/templates/init/.env.testdata +18 -18
  34. package/templates/init/.gitignore-template +24 -24
  35. package/templates/init/AGENTS.md +155 -155
  36. package/templates/init/CLAUDE.md +157 -157
  37. package/templates/init/test-runs/README.md +45 -45
  38. package/templates/init/tests/CLAUDE.md +193 -193
  39. package/templates/init/tests/docs/test-execution-strategy.md +535 -535
  40. package/templates/init/tests/docs/testing-best-practices.md +724 -724
  41. package/templates/playwright/BasePage.template.ts +190 -190
  42. package/templates/playwright/auth.setup.template.ts +89 -89
  43. package/templates/playwright/dataGenerators.helper.template.ts +148 -148
  44. package/templates/playwright/dateUtils.helper.template.ts +96 -96
  45. package/templates/playwright/pages.fixture.template.ts +50 -50
  46. package/templates/playwright/playwright.config.template.ts +97 -97
  47. package/templates/playwright/reporters/__tests__/bugzy-reporter-failure-classification.test.ts +299 -299
  48. package/templates/playwright/reporters/__tests__/bugzy-reporter-manifest-merge.test.ts +329 -329
  49. package/templates/playwright/reporters/__tests__/playwright.config.ts +5 -5
  50. package/templates/playwright/reporters/bugzy-reporter.ts +784 -784
  51. package/dist/templates/init/.bugzy/runtime/knowledge-base.md +0 -61
  52. package/dist/templates/init/.bugzy/runtime/knowledge-maintenance-guide.md +0 -97
  53. package/dist/templates/init/.bugzy/runtime/project-context.md +0 -35
  54. package/dist/templates/init/.bugzy/runtime/subagent-memory-guide.md +0 -87
  55. package/dist/templates/init/.bugzy/runtime/templates/test-plan-template.md +0 -50
  56. package/dist/templates/init/.bugzy/runtime/templates/test-result-schema.md +0 -498
  57. package/dist/templates/init/.bugzy/runtime/test-execution-strategy.md +0 -535
  58. package/dist/templates/init/.bugzy/runtime/testing-best-practices.md +0 -632
  59. package/dist/templates/init/.gitignore-template +0 -25
@@ -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
+ });