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