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