@bugzy-ai/bugzy 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.cjs +322 -9
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +322 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +311 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +311 -9
- package/dist/index.js.map +1 -1
- package/dist/tasks/index.cjs +114 -9
- package/dist/tasks/index.cjs.map +1 -1
- package/dist/tasks/index.d.cts +1 -0
- package/dist/tasks/index.d.ts +1 -0
- package/dist/tasks/index.js +114 -9
- package/dist/tasks/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/init/.bugzy/runtime/handlers/messages/feedback.md +178 -0
- package/templates/init/.bugzy/runtime/handlers/messages/question.md +122 -0
- package/templates/init/.bugzy/runtime/handlers/messages/status.md +146 -0
- package/templates/init/.bugzy/runtime/templates/event-examples.md +195 -0
- package/templates/init/.claude/settings.json +28 -0
- package/templates/playwright/reporters/__tests__/bugzy-reporter-manifest-merge.test.ts +329 -0
- package/templates/playwright/reporters/__tests__/playwright.config.ts +5 -0
- package/templates/playwright/reporters/bugzy-reporter.ts +163 -4
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { mergeManifests } from '../bugzy-reporter';
|
|
3
|
+
|
|
4
|
+
function makeExecution(overrides: Partial<{
|
|
5
|
+
number: number;
|
|
6
|
+
status: string;
|
|
7
|
+
duration: number;
|
|
8
|
+
videoFile: string | null;
|
|
9
|
+
hasTrace: boolean;
|
|
10
|
+
hasScreenshots: boolean;
|
|
11
|
+
error: string | null;
|
|
12
|
+
}> = {}) {
|
|
13
|
+
return {
|
|
14
|
+
number: 1,
|
|
15
|
+
status: 'passed',
|
|
16
|
+
duration: 1000,
|
|
17
|
+
videoFile: 'video.webm',
|
|
18
|
+
hasTrace: false,
|
|
19
|
+
hasScreenshots: false,
|
|
20
|
+
error: null,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeTestCase(id: string, executions: ReturnType<typeof makeExecution>[], finalStatus?: string) {
|
|
26
|
+
const lastExec = executions[executions.length - 1];
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
name: id.replace(/^TC-\d+-/, '').replace(/-/g, ' '),
|
|
30
|
+
totalExecutions: executions.length,
|
|
31
|
+
finalStatus: finalStatus ?? lastExec.status,
|
|
32
|
+
executions,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeManifest(overrides: Partial<{
|
|
37
|
+
bugzyExecutionId: string;
|
|
38
|
+
timestamp: string;
|
|
39
|
+
startTime: string;
|
|
40
|
+
endTime: string;
|
|
41
|
+
status: string;
|
|
42
|
+
stats: { totalTests: number; passed: number; failed: number; totalExecutions: number };
|
|
43
|
+
testCases: ReturnType<typeof makeTestCase>[];
|
|
44
|
+
}> = {}) {
|
|
45
|
+
const testCases = overrides.testCases ?? [];
|
|
46
|
+
const totalExecutions = testCases.reduce((sum, tc) => sum + tc.executions.length, 0);
|
|
47
|
+
const passed = testCases.filter(tc => tc.finalStatus === 'passed').length;
|
|
48
|
+
const failed = testCases.length - passed;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
bugzyExecutionId: 'local-20260127-060129',
|
|
52
|
+
timestamp: '20260127-060129',
|
|
53
|
+
startTime: '2026-01-27T06:01:29.000Z',
|
|
54
|
+
endTime: '2026-01-27T06:02:00.000Z',
|
|
55
|
+
status: 'passed',
|
|
56
|
+
stats: {
|
|
57
|
+
totalTests: testCases.length,
|
|
58
|
+
passed,
|
|
59
|
+
failed,
|
|
60
|
+
totalExecutions,
|
|
61
|
+
...overrides.stats,
|
|
62
|
+
},
|
|
63
|
+
...overrides,
|
|
64
|
+
testCases,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test.describe('mergeManifests', () => {
|
|
69
|
+
test('returns current manifest unchanged when existing is null', () => {
|
|
70
|
+
const current = makeManifest({
|
|
71
|
+
testCases: [makeTestCase('TC-001-login', [makeExecution()])],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const result = mergeManifests(null, current);
|
|
75
|
+
|
|
76
|
+
expect(result).toEqual(current);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('merges test cases from both manifests', () => {
|
|
80
|
+
const existing = makeManifest({
|
|
81
|
+
testCases: [
|
|
82
|
+
makeTestCase('TC-001-login', [makeExecution({ number: 1 })]),
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const current = makeManifest({
|
|
87
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
88
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
89
|
+
testCases: [
|
|
90
|
+
makeTestCase('TC-002-checkout', [makeExecution({ number: 1 })]),
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const result = mergeManifests(existing, current);
|
|
95
|
+
|
|
96
|
+
expect(result.testCases).toHaveLength(2);
|
|
97
|
+
expect(result.testCases.map(tc => tc.id)).toContain('TC-001-login');
|
|
98
|
+
expect(result.testCases.map(tc => tc.id)).toContain('TC-002-checkout');
|
|
99
|
+
expect(result.stats.totalTests).toBe(2);
|
|
100
|
+
expect(result.stats.totalExecutions).toBe(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('merges executions for the same test case across runs', () => {
|
|
104
|
+
const existing = makeManifest({
|
|
105
|
+
testCases: [
|
|
106
|
+
makeTestCase('TC-001-login', [
|
|
107
|
+
makeExecution({ number: 1, status: 'failed', error: 'timeout' }),
|
|
108
|
+
], 'failed'),
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const current = makeManifest({
|
|
113
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
114
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
115
|
+
testCases: [
|
|
116
|
+
makeTestCase('TC-001-login', [
|
|
117
|
+
makeExecution({ number: 2, status: 'passed' }),
|
|
118
|
+
]),
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = mergeManifests(existing, current);
|
|
123
|
+
|
|
124
|
+
expect(result.testCases).toHaveLength(1);
|
|
125
|
+
const tc = result.testCases[0];
|
|
126
|
+
expect(tc.executions).toHaveLength(2);
|
|
127
|
+
expect(tc.executions[0].number).toBe(1);
|
|
128
|
+
expect(tc.executions[0].status).toBe('failed');
|
|
129
|
+
expect(tc.executions[1].number).toBe(2);
|
|
130
|
+
expect(tc.executions[1].status).toBe('passed');
|
|
131
|
+
expect(tc.totalExecutions).toBe(2);
|
|
132
|
+
expect(tc.finalStatus).toBe('passed'); // Latest execution status
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('current run wins on execution number collision', () => {
|
|
136
|
+
const existing = makeManifest({
|
|
137
|
+
testCases: [
|
|
138
|
+
makeTestCase('TC-001-login', [
|
|
139
|
+
makeExecution({ number: 3, status: 'failed', duration: 500 }),
|
|
140
|
+
], 'failed'),
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const current = makeManifest({
|
|
145
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
146
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
147
|
+
testCases: [
|
|
148
|
+
makeTestCase('TC-001-login', [
|
|
149
|
+
makeExecution({ number: 3, status: 'passed', duration: 1200 }),
|
|
150
|
+
]),
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = mergeManifests(existing, current);
|
|
155
|
+
|
|
156
|
+
const tc = result.testCases[0];
|
|
157
|
+
expect(tc.executions).toHaveLength(1);
|
|
158
|
+
expect(tc.executions[0].status).toBe('passed');
|
|
159
|
+
expect(tc.executions[0].duration).toBe(1200);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('preserves test cases that only exist in existing manifest', () => {
|
|
163
|
+
const existing = makeManifest({
|
|
164
|
+
testCases: [
|
|
165
|
+
makeTestCase('TC-001-login', [makeExecution({ number: 1 })]),
|
|
166
|
+
makeTestCase('TC-002-checkout', [makeExecution({ number: 1 })]),
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const current = makeManifest({
|
|
171
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
172
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
173
|
+
testCases: [
|
|
174
|
+
makeTestCase('TC-001-login', [makeExecution({ number: 2 })]),
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const result = mergeManifests(existing, current);
|
|
179
|
+
|
|
180
|
+
expect(result.testCases).toHaveLength(2);
|
|
181
|
+
const checkout = result.testCases.find(tc => tc.id === 'TC-002-checkout');
|
|
182
|
+
expect(checkout).toBeDefined();
|
|
183
|
+
expect(checkout!.executions).toHaveLength(1);
|
|
184
|
+
expect(checkout!.executions[0].number).toBe(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('recalculates stats correctly from merged data', () => {
|
|
188
|
+
const existing = makeManifest({
|
|
189
|
+
testCases: [
|
|
190
|
+
makeTestCase('TC-001-login', [
|
|
191
|
+
makeExecution({ number: 1, status: 'failed' }),
|
|
192
|
+
], 'failed'),
|
|
193
|
+
makeTestCase('TC-002-checkout', [
|
|
194
|
+
makeExecution({ number: 1, status: 'passed' }),
|
|
195
|
+
]),
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const current = makeManifest({
|
|
200
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
201
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
202
|
+
testCases: [
|
|
203
|
+
makeTestCase('TC-001-login', [
|
|
204
|
+
makeExecution({ number: 2, status: 'passed' }),
|
|
205
|
+
]),
|
|
206
|
+
makeTestCase('TC-003-profile', [
|
|
207
|
+
makeExecution({ number: 1, status: 'failed' }),
|
|
208
|
+
], 'failed'),
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const result = mergeManifests(existing, current);
|
|
213
|
+
|
|
214
|
+
expect(result.stats.totalTests).toBe(3);
|
|
215
|
+
// TC-001: exec-1 (failed) + exec-2 (passed) = 2 execs, finalStatus=passed
|
|
216
|
+
// TC-002: exec-1 (passed) = 1 exec, finalStatus=passed
|
|
217
|
+
// TC-003: exec-1 (failed) = 1 exec, finalStatus=failed
|
|
218
|
+
expect(result.stats.totalExecutions).toBe(4);
|
|
219
|
+
expect(result.stats.passed).toBe(2); // TC-001 and TC-002
|
|
220
|
+
expect(result.stats.failed).toBe(1); // TC-003
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('uses earliest startTime and latest endTime', () => {
|
|
224
|
+
const existing = makeManifest({
|
|
225
|
+
startTime: '2026-01-27T06:01:00.000Z',
|
|
226
|
+
endTime: '2026-01-27T06:02:00.000Z',
|
|
227
|
+
testCases: [makeTestCase('TC-001-login', [makeExecution()])],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const current = makeManifest({
|
|
231
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
232
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
233
|
+
testCases: [makeTestCase('TC-001-login', [makeExecution({ number: 2 })])],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const result = mergeManifests(existing, current);
|
|
237
|
+
|
|
238
|
+
expect(result.startTime).toBe('2026-01-27T06:01:00.000Z');
|
|
239
|
+
expect(result.endTime).toBe('2026-01-27T06:06:00.000Z');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('sets status to failed if any test case has failed finalStatus', () => {
|
|
243
|
+
const existing = makeManifest({
|
|
244
|
+
status: 'passed',
|
|
245
|
+
testCases: [
|
|
246
|
+
makeTestCase('TC-001-login', [makeExecution({ number: 1, status: 'passed' })]),
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const current = makeManifest({
|
|
251
|
+
status: 'passed',
|
|
252
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
253
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
254
|
+
testCases: [
|
|
255
|
+
makeTestCase('TC-002-checkout', [
|
|
256
|
+
makeExecution({ number: 1, status: 'failed' }),
|
|
257
|
+
], 'failed'),
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const result = mergeManifests(existing, current);
|
|
262
|
+
|
|
263
|
+
expect(result.status).toBe('failed');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('preserves original session timestamp from existing manifest', () => {
|
|
267
|
+
const existing = makeManifest({
|
|
268
|
+
timestamp: '20260127-060129',
|
|
269
|
+
testCases: [makeTestCase('TC-001-login', [makeExecution()])],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const current = makeManifest({
|
|
273
|
+
timestamp: '20260127-060500',
|
|
274
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
275
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
276
|
+
testCases: [makeTestCase('TC-001-login', [makeExecution({ number: 2 })])],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const result = mergeManifests(existing, current);
|
|
280
|
+
|
|
281
|
+
expect(result.timestamp).toBe('20260127-060129');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('handles timedOut status as failure in merged status', () => {
|
|
285
|
+
const existing = makeManifest({
|
|
286
|
+
status: 'passed',
|
|
287
|
+
testCases: [
|
|
288
|
+
makeTestCase('TC-001-login', [
|
|
289
|
+
makeExecution({ number: 1, status: 'timedOut' }),
|
|
290
|
+
], 'timedOut'),
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const current = makeManifest({
|
|
295
|
+
status: 'passed',
|
|
296
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
297
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
298
|
+
testCases: [
|
|
299
|
+
makeTestCase('TC-002-checkout', [makeExecution({ number: 1 })]),
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = mergeManifests(existing, current);
|
|
304
|
+
|
|
305
|
+
expect(result.status).toBe('failed');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('does not mutate input manifests', () => {
|
|
309
|
+
const existingExec = makeExecution({ number: 1, status: 'failed' });
|
|
310
|
+
const existing = makeManifest({
|
|
311
|
+
testCases: [makeTestCase('TC-001-login', [existingExec], 'failed')],
|
|
312
|
+
});
|
|
313
|
+
const existingSnapshot = JSON.parse(JSON.stringify(existing));
|
|
314
|
+
|
|
315
|
+
const current = makeManifest({
|
|
316
|
+
startTime: '2026-01-27T06:05:00.000Z',
|
|
317
|
+
endTime: '2026-01-27T06:06:00.000Z',
|
|
318
|
+
testCases: [
|
|
319
|
+
makeTestCase('TC-001-login', [makeExecution({ number: 2, status: 'passed' })]),
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
const currentSnapshot = JSON.parse(JSON.stringify(current));
|
|
323
|
+
|
|
324
|
+
mergeManifests(existing, current);
|
|
325
|
+
|
|
326
|
+
expect(existing).toEqual(existingSnapshot);
|
|
327
|
+
expect(current).toEqual(currentSnapshot);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -24,6 +24,142 @@ interface StepData {
|
|
|
24
24
|
duration?: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Manifest execution entry
|
|
29
|
+
*/
|
|
30
|
+
interface ManifestExecution {
|
|
31
|
+
number: number;
|
|
32
|
+
status: string;
|
|
33
|
+
duration: number;
|
|
34
|
+
videoFile: string | null;
|
|
35
|
+
hasTrace: boolean;
|
|
36
|
+
hasScreenshots: boolean;
|
|
37
|
+
error: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Manifest test case entry
|
|
42
|
+
*/
|
|
43
|
+
interface ManifestTestCase {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
totalExecutions: number;
|
|
47
|
+
finalStatus: string;
|
|
48
|
+
executions: ManifestExecution[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Manifest structure for test run sessions
|
|
53
|
+
*/
|
|
54
|
+
interface Manifest {
|
|
55
|
+
bugzyExecutionId: string;
|
|
56
|
+
timestamp: string;
|
|
57
|
+
startTime: string;
|
|
58
|
+
endTime: string;
|
|
59
|
+
status: string;
|
|
60
|
+
stats: {
|
|
61
|
+
totalTests: number;
|
|
62
|
+
passed: number;
|
|
63
|
+
failed: number;
|
|
64
|
+
totalExecutions: number;
|
|
65
|
+
};
|
|
66
|
+
testCases: ManifestTestCase[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Merge an existing manifest with the current run's manifest.
|
|
71
|
+
* If existing is null, returns current as-is.
|
|
72
|
+
* Deduplicates executions by number (current run wins on collision).
|
|
73
|
+
* Recalculates stats from the merged data.
|
|
74
|
+
*/
|
|
75
|
+
export function mergeManifests(existing: Manifest | null, current: Manifest): Manifest {
|
|
76
|
+
if (!existing) {
|
|
77
|
+
return current;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build map of test cases by id from existing manifest
|
|
81
|
+
const testCaseMap = new Map<string, ManifestTestCase>();
|
|
82
|
+
for (const tc of existing.testCases) {
|
|
83
|
+
testCaseMap.set(tc.id, { ...tc, executions: [...tc.executions] });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Merge current run's test cases
|
|
87
|
+
for (const tc of current.testCases) {
|
|
88
|
+
const existingTc = testCaseMap.get(tc.id);
|
|
89
|
+
if (existingTc) {
|
|
90
|
+
// Merge executions: build a map keyed by execution number
|
|
91
|
+
const execMap = new Map<number, ManifestExecution>();
|
|
92
|
+
for (const exec of existingTc.executions) {
|
|
93
|
+
execMap.set(exec.number, exec);
|
|
94
|
+
}
|
|
95
|
+
// Current run's executions overwrite on collision
|
|
96
|
+
for (const exec of tc.executions) {
|
|
97
|
+
execMap.set(exec.number, exec);
|
|
98
|
+
}
|
|
99
|
+
// Sort by execution number
|
|
100
|
+
const mergedExecs = Array.from(execMap.values()).sort((a, b) => a.number - b.number);
|
|
101
|
+
const finalStatus = mergedExecs[mergedExecs.length - 1].status;
|
|
102
|
+
|
|
103
|
+
testCaseMap.set(tc.id, {
|
|
104
|
+
id: tc.id,
|
|
105
|
+
name: tc.name,
|
|
106
|
+
totalExecutions: mergedExecs.length,
|
|
107
|
+
finalStatus,
|
|
108
|
+
executions: mergedExecs,
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
// New test case from current run
|
|
112
|
+
testCaseMap.set(tc.id, { ...tc, executions: [...tc.executions] });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build merged test cases array
|
|
117
|
+
const mergedTestCases = Array.from(testCaseMap.values());
|
|
118
|
+
|
|
119
|
+
// Recalculate stats
|
|
120
|
+
let totalTests = 0;
|
|
121
|
+
let totalExecutions = 0;
|
|
122
|
+
let passedTests = 0;
|
|
123
|
+
let failedTests = 0;
|
|
124
|
+
|
|
125
|
+
for (const tc of mergedTestCases) {
|
|
126
|
+
totalTests++;
|
|
127
|
+
totalExecutions += tc.executions.length;
|
|
128
|
+
if (tc.finalStatus === 'passed') {
|
|
129
|
+
passedTests++;
|
|
130
|
+
} else {
|
|
131
|
+
failedTests++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Use earliest startTime, latest endTime
|
|
136
|
+
const startTime = new Date(existing.startTime) < new Date(current.startTime)
|
|
137
|
+
? existing.startTime
|
|
138
|
+
: current.startTime;
|
|
139
|
+
const endTime = new Date(existing.endTime) > new Date(current.endTime)
|
|
140
|
+
? existing.endTime
|
|
141
|
+
: current.endTime;
|
|
142
|
+
|
|
143
|
+
// Status: if any test case failed, overall is failed
|
|
144
|
+
const hasFailure = mergedTestCases.some(tc => tc.finalStatus === 'failed' || tc.finalStatus === 'timedOut');
|
|
145
|
+
const status = hasFailure ? 'failed' : current.status;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
bugzyExecutionId: current.bugzyExecutionId,
|
|
149
|
+
timestamp: existing.timestamp, // Keep original session timestamp
|
|
150
|
+
startTime,
|
|
151
|
+
endTime,
|
|
152
|
+
status,
|
|
153
|
+
stats: {
|
|
154
|
+
totalTests,
|
|
155
|
+
passed: passedTests,
|
|
156
|
+
failed: failedTests,
|
|
157
|
+
totalExecutions,
|
|
158
|
+
},
|
|
159
|
+
testCases: mergedTestCases,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
27
163
|
/**
|
|
28
164
|
* Bugzy Custom Playwright Reporter
|
|
29
165
|
*
|
|
@@ -393,8 +529,8 @@ class BugzyReporter implements Reporter {
|
|
|
393
529
|
});
|
|
394
530
|
}
|
|
395
531
|
|
|
396
|
-
//
|
|
397
|
-
const
|
|
532
|
+
// Build current run's manifest
|
|
533
|
+
const currentManifest: Manifest = {
|
|
398
534
|
bugzyExecutionId: this.bugzyExecutionId,
|
|
399
535
|
timestamp: this.timestamp,
|
|
400
536
|
startTime: this.startTime.toISOString(),
|
|
@@ -409,14 +545,37 @@ class BugzyReporter implements Reporter {
|
|
|
409
545
|
testCases,
|
|
410
546
|
};
|
|
411
547
|
|
|
548
|
+
// Read existing manifest for merge (if session is being reused)
|
|
412
549
|
const manifestPath = path.join(this.testRunDir, 'manifest.json');
|
|
413
|
-
|
|
550
|
+
let existingManifest: Manifest | null = null;
|
|
551
|
+
if (fs.existsSync(manifestPath)) {
|
|
552
|
+
try {
|
|
553
|
+
existingManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
554
|
+
} catch (err) {
|
|
555
|
+
console.warn(`⚠️ Could not parse existing manifest, will overwrite: ${err}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
414
558
|
|
|
415
|
-
|
|
559
|
+
// Merge with existing manifest data
|
|
560
|
+
const merged = mergeManifests(existingManifest, currentManifest);
|
|
561
|
+
|
|
562
|
+
// Write atomically (temp file + rename)
|
|
563
|
+
const tmpPath = manifestPath + '.tmp';
|
|
564
|
+
fs.writeFileSync(tmpPath, JSON.stringify(merged, null, 2));
|
|
565
|
+
fs.renameSync(tmpPath, manifestPath);
|
|
566
|
+
|
|
567
|
+
console.log(`\n📊 Test Run Summary (this run):`);
|
|
416
568
|
console.log(` Total tests: ${totalTests}`);
|
|
417
569
|
console.log(` Passed: ${passedTests}`);
|
|
418
570
|
console.log(` Failed: ${failedTests}`);
|
|
419
571
|
console.log(` Total executions: ${totalExecutions}`);
|
|
572
|
+
|
|
573
|
+
if (existingManifest) {
|
|
574
|
+
console.log(`\n🔗 Merged with previous session data:`);
|
|
575
|
+
console.log(` Session total tests: ${merged.stats.totalTests}`);
|
|
576
|
+
console.log(` Session total executions: ${merged.stats.totalExecutions}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
420
579
|
console.log(` Manifest: ${manifestPath}\n`);
|
|
421
580
|
}
|
|
422
581
|
|