@zohodesk/testinglibrary 0.0.85-n20-experimental → 0.0.87-n20-experimental

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.
@@ -6,18 +6,27 @@ var _os = _interopRequireDefault(require("os"));
6
6
  var _path = _interopRequireDefault(require("path"));
7
7
  var toolPatterns = _interopRequireWildcard(require("../../../../../core/playwright/patterns/toolPatterns"));
8
8
  var casePatterns = _interopRequireWildcard(require("../../../../../core/playwright/patterns/casePatterns"));
9
+ var codePatterns = _interopRequireWildcard(require("../../../../../core/playwright/patterns/codePatterns"));
10
+ var _readConfigFile = require("../../../../../core/playwright/readConfigFile");
9
11
  var _runtimeLogPath = require("../../../../../core/playwright/helpers/runtimeLogPath");
10
12
  var _failureTagClassifier = require("../../../../../core/playwright/helpers/failureTagClassifier");
13
+ var _runtimeLogAnnotator = require("../../../../../core/playwright/helpers/runtimeLogAnnotator");
11
14
  var _failureTagConstants = require("../../../../../core/playwright/constants/failureTagConstants");
12
15
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
16
+ jest.mock('../../../../../core/playwright/readConfigFile', () => ({
17
+ generateConfigFromFile: jest.fn()
18
+ }));
13
19
  const TEST_TITLE = 'environment-abort-test';
14
20
  let tempDir;
21
+ function runtimeLogsDir() {
22
+ return _path.default.join(tempDir, 'runtime-logs');
23
+ }
15
24
  function writeRuntimeLog(events, {
16
25
  workerIndex = 0,
17
26
  retry = 0,
18
27
  title = TEST_TITLE
19
28
  } = {}) {
20
- const file = (0, _runtimeLogPath.resolveRuntimeLogFilePath)(tempDir, {
29
+ const file = (0, _runtimeLogPath.resolveRuntimeLogFilePath)(runtimeLogsDir(), {
21
30
  workerIndex,
22
31
  retry,
23
32
  title
@@ -55,6 +64,9 @@ function failingDetailWithStdio({
55
64
  }
56
65
  beforeEach(() => {
57
66
  tempDir = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), 'failure-tag-'));
67
+ _readConfigFile.generateConfigFromFile.mockReturnValue({
68
+ uatDirectory: tempDir
69
+ });
58
70
  });
59
71
  afterEach(() => {
60
72
  _fs.default.rmSync(tempDir, {
@@ -63,12 +75,12 @@ afterEach(() => {
63
75
  });
64
76
  });
65
77
  describe('classifyFailureTags', () => {
66
- test('returns flaky and skips log analysis for flaky outcome', () => {
78
+ test('leaves retry-passed (flaky) outcomes untagged', () => {
67
79
  const tags = (0, _failureTagClassifier.classifyFailureTags)([{
68
80
  status: 'flaky',
69
81
  results: []
70
- }], TEST_TITLE, tempDir);
71
- expect(tags).toEqual(['FLAKY']);
82
+ }], TEST_TITLE);
83
+ expect(tags).toEqual([]);
72
84
  });
73
85
  test('tags environment when a document abort is present', () => {
74
86
  const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
@@ -76,7 +88,7 @@ describe('classifyFailureTags', () => {
76
88
  url: 'https://app/events/list/commentsubtabevent',
77
89
  failure: 'net::ERR_ABORTED',
78
90
  resourceType: 'document'
79
- }])], TEST_TITLE, tempDir);
91
+ }])], TEST_TITLE);
80
92
  expect(tags).toEqual(['ENVIRONMENT']);
81
93
  });
82
94
  test('tags environment for a network/cert failure', () => {
@@ -87,7 +99,7 @@ describe('classifyFailureTags', () => {
87
99
  resourceType: 'script'
88
100
  }], {
89
101
  title: 'cert-test'
90
- })], 'cert-test', tempDir);
102
+ })], 'cert-test');
91
103
  expect(tags).toEqual(['ENVIRONMENT']);
92
104
  });
93
105
  test('tags code when an app undefined-property error is present', () => {
@@ -97,10 +109,10 @@ describe('classifyFailureTags', () => {
97
109
  message: "Cannot read properties of undefined (reading 'id')"
98
110
  }], {
99
111
  title: 'code-test'
100
- })], 'code-test', tempDir);
112
+ })], 'code-test');
101
113
  expect(tags).toEqual(['CODE']);
102
114
  });
103
- test('does not tag a non-document (xhr) abort as environment', () => {
115
+ test('defaults to code (not environment) for a non-document xhr abort', () => {
104
116
  const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
105
117
  kind: 'requestfailed',
106
118
  url: 'https://app/api/tickets',
@@ -108,17 +120,17 @@ describe('classifyFailureTags', () => {
108
120
  resourceType: 'xhr'
109
121
  }], {
110
122
  title: 'xhr-test'
111
- })], 'xhr-test', tempDir);
112
- expect(tags).toEqual([]);
123
+ })], 'xhr-test');
124
+ expect(tags).toEqual(['CODE']);
113
125
  });
114
- test('tags case and code when stderr reports DATA_GENERATION_ERROR', () => {
126
+ test('tags case when stderr reports DATA_GENERATION_ERROR', () => {
115
127
  const detail = failingDetailWithStdio({
116
128
  stderr: [{
117
129
  text: 'status : DATA_GENERATION_ERROR\n generator: comments'
118
130
  }]
119
131
  });
120
- const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
121
- expect(tags).toEqual(['CASE', 'CODE']);
132
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
133
+ expect(tags).toEqual(['CASE']);
122
134
  });
123
135
  test('tags tool for a known tool pattern in base64 stdout', () => {
124
136
  const detail = failingDetailWithStdio({
@@ -126,10 +138,10 @@ describe('classifyFailureTags', () => {
126
138
  buffer: Buffer.from('Error during login').toString('base64')
127
139
  }]
128
140
  });
129
- const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
141
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
130
142
  expect(tags).toEqual(['TOOL']);
131
143
  });
132
- test('tags case and code when stderr matches a case pattern', () => {
144
+ test('tags case when stderr matches a case pattern', () => {
133
145
  const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
134
146
  try {
135
147
  casePatterns.CASE_PATTERNS.push(/expect\(received\)\.toBe/i);
@@ -138,22 +150,37 @@ describe('classifyFailureTags', () => {
138
150
  text: 'Error: expect(received).toBe(expected)'
139
151
  }]
140
152
  });
141
- const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
142
- expect(tags).toEqual(['CASE', 'CODE']);
153
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
154
+ expect(tags).toEqual(['CASE']);
143
155
  } finally {
144
156
  casePatterns.CASE_PATTERNS.length = 0;
145
157
  casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
146
158
  }
147
159
  });
148
- test('does not tag tool for benign stdout/stderr', () => {
160
+ test('emits only the highest-priority tag (CODE) when CODE and ENVIRONMENT both match', () => {
161
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
162
+ kind: 'requestfailed',
163
+ url: 'https://app/events/list/commentsubtabevent',
164
+ failure: 'net::ERR_ABORTED',
165
+ resourceType: 'document'
166
+ }, {
167
+ kind: 'pageerror',
168
+ name: 'TypeError',
169
+ message: "Cannot read properties of undefined (reading 'id')"
170
+ }], {
171
+ title: 'code-over-env-test'
172
+ })], 'code-over-env-test');
173
+ expect(tags).toEqual(['CODE']);
174
+ });
175
+ test('defaults to code (not tool) for benign stdout/stderr', () => {
149
176
  const detail = failingDetailWithStdio({
150
177
  stdout: [{
151
178
  text: 'Generated 5 records successfully'
152
179
  }],
153
180
  stderr: []
154
181
  });
155
- const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
156
- expect(tags).toEqual([]);
182
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
183
+ expect(tags).toEqual(['CODE']);
157
184
  });
158
185
  test('does not throw for malformed stdio chunk entries', () => {
159
186
  const detail = failingDetailWithStdio({
@@ -162,10 +189,10 @@ describe('classifyFailureTags', () => {
162
189
  }],
163
190
  stderr: [false]
164
191
  });
165
- expect(() => (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir)).not.toThrow();
166
- expect((0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir)).toEqual([]);
192
+ expect(() => (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE)).not.toThrow();
193
+ expect((0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE)).toEqual(['CODE']);
167
194
  });
168
- test('leaves untagged when no environment or code pattern is present', () => {
195
+ test('defaults to code when a failure matches no pattern', () => {
169
196
  const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
170
197
  kind: 'response',
171
198
  status: 200,
@@ -180,7 +207,18 @@ describe('classifyFailureTags', () => {
180
207
  }
181
208
  }], {
182
209
  title: 'neutral-test'
183
- })], 'neutral-test', tempDir);
210
+ })], 'neutral-test');
211
+ expect(tags).toEqual(['CODE']);
212
+ });
213
+ test('leaves a passing spec untagged (no default code)', () => {
214
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([{
215
+ status: 'expected',
216
+ results: [{
217
+ status: 'passed',
218
+ workerIndex: 0,
219
+ retry: 0
220
+ }]
221
+ }], 'passing-test');
184
222
  expect(tags).toEqual([]);
185
223
  });
186
224
  });
@@ -191,7 +229,7 @@ describe('classifyFailureDetails', () => {
191
229
  url: 'https://app/events/list/commentsubtabevent',
192
230
  failure: 'net::ERR_ABORTED',
193
231
  resourceType: 'document'
194
- }])], TEST_TITLE, tempDir);
232
+ }])], TEST_TITLE);
195
233
  expect(details.tags).toEqual(['ENVIRONMENT']);
196
234
  expect(details.matches).toHaveLength(1);
197
235
  expect(details.matches[0]).toMatchObject({
@@ -214,25 +252,17 @@ describe('classifyFailureDetails', () => {
214
252
  source: 'runtime-log'
215
253
  });
216
254
  });
217
- test('records flaky outcome without a regex pattern', () => {
218
- const details = (0, _failureTagClassifier.classifyFailureDetails)([{
219
- status: 'flaky',
220
- results: []
221
- }], TEST_TITLE, tempDir);
222
- expect(details.tags).toEqual(['FLAKY']);
223
- expect(details.matches[0]).toEqual({
224
- tag: 'FLAKY',
225
- pattern: null,
226
- source: 'retry-outcome',
227
- text: null
228
- });
255
+ test('defaults to code with no matches when events carry no recognised pattern', () => {
256
+ const details = (0, _failureTagClassifier.classifyFailureDetailsFromEvents)([]);
257
+ expect(details.tags).toEqual(['CODE']);
258
+ expect(details.matches).toEqual([]);
229
259
  });
230
260
  test('records tool pattern from stdio', () => {
231
261
  const details = (0, _failureTagClassifier.classifyFailureDetails)([failingDetailWithStdio({
232
262
  stderr: [{
233
263
  text: 'Error during login for actor admin'
234
264
  }]
235
- })], TEST_TITLE, tempDir);
265
+ })], TEST_TITLE);
236
266
  expect(details.tags).toEqual(['TOOL']);
237
267
  expect(details.matches[0]).toMatchObject({
238
268
  tag: 'TOOL',
@@ -240,7 +270,7 @@ describe('classifyFailureDetails', () => {
240
270
  source: 'stdio'
241
271
  });
242
272
  });
243
- test('records case pattern from stdio and implies code', () => {
273
+ test('records case pattern from stdio without implying code', () => {
244
274
  const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
245
275
  try {
246
276
  casePatterns.CASE_PATTERNS.push(/expect\(received\)\.toBe/i);
@@ -248,26 +278,20 @@ describe('classifyFailureDetails', () => {
248
278
  stderr: [{
249
279
  text: 'Error: expect(received).toBe(expected)'
250
280
  }]
251
- })], TEST_TITLE, tempDir);
252
- expect(details.tags).toEqual(['CASE', 'CODE']);
253
- expect(details.matches).toHaveLength(2);
281
+ })], TEST_TITLE);
282
+ expect(details.tags).toEqual(['CASE']);
283
+ expect(details.matches).toHaveLength(1);
254
284
  expect(details.matches[0]).toMatchObject({
255
285
  tag: 'CASE',
256
286
  pattern: 'expect\\(received\\)\\.toBe',
257
287
  source: 'stdio'
258
288
  });
259
- expect(details.matches[1]).toMatchObject({
260
- tag: 'CODE',
261
- pattern: null,
262
- source: 'case-implied'
263
- });
264
- expect(details.matches[1].text).toContain('expect(received).toBe(expected)');
265
289
  } finally {
266
290
  casePatterns.CASE_PATTERNS.length = 0;
267
291
  casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
268
292
  }
269
293
  });
270
- test('keeps runtime-log code match when case also matches', () => {
294
+ test('resolves to CODE when a runtime-log code match outranks a stdio case match', () => {
271
295
  const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
272
296
  try {
273
297
  casePatterns.CASE_PATTERNS.push(/expect\(received\)\.toBe/i);
@@ -288,13 +312,14 @@ describe('classifyFailureDetails', () => {
288
312
  text: 'Error: expect(received).toBe(expected)'
289
313
  }]
290
314
  }]
291
- }], 'case-with-runtime-code', tempDir);
292
- expect(details.tags).toEqual(['CASE', 'CODE']);
293
- expect(details.matches).toHaveLength(2);
294
- expect(details.matches.find(match => match.tag === 'CASE')).toMatchObject({
295
- source: 'stdio'
296
- });
297
- expect(details.matches.find(match => match.tag === 'CODE')).toMatchObject({
315
+ }], 'case-with-runtime-code');
316
+
317
+ // CODE outranks CASE, so only CODE surfaces as the tag and as matches.
318
+ expect(details.tags).toEqual(['CODE']);
319
+ expect(details.matches).toHaveLength(1);
320
+ expect(details.matches.find(match => match.tag === 'CASE')).toBeUndefined();
321
+ expect(details.matches[0]).toMatchObject({
322
+ tag: 'CODE',
298
323
  source: 'runtime-log',
299
324
  pattern: 'Cannot read properties of (undefined|null)'
300
325
  });
@@ -303,7 +328,7 @@ describe('classifyFailureDetails', () => {
303
328
  casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
304
329
  }
305
330
  });
306
- test('implies code from case when no runtime log is present', () => {
331
+ test('keeps case as the tag and does not add code when no runtime log is present', () => {
307
332
  const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
308
333
  try {
309
334
  casePatterns.CASE_PATTERNS.push(/Timed out \d+ms waiting for/i);
@@ -311,17 +336,174 @@ describe('classifyFailureDetails', () => {
311
336
  stderr: [{
312
337
  text: 'Timed out 5000ms waiting for locator("#save")'
313
338
  }]
314
- })], TEST_TITLE, tempDir);
315
- expect(details.tags).toEqual(['CASE', 'CODE']);
316
- expect(details.matches.find(match => match.tag === 'CODE')).toMatchObject({
317
- source: 'case-implied',
318
- pattern: null
319
- });
339
+ })], TEST_TITLE);
340
+ expect(details.tags).toEqual(['CASE']);
341
+ expect(details.matches.find(match => match.tag === 'CODE')).toBeUndefined();
320
342
  } finally {
321
343
  casePatterns.CASE_PATTERNS.length = 0;
322
344
  casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
323
345
  }
324
346
  });
347
+ test('records every CODE pattern hit from the runtime log', () => {
348
+ const details = (0, _failureTagClassifier.classifyFailureDetailsFromEvents)([{
349
+ kind: 'pageerror',
350
+ name: 'TypeError',
351
+ message: "Cannot read properties of undefined (reading 'id')"
352
+ }, {
353
+ kind: 'pageerror',
354
+ name: 'TypeError',
355
+ message: 'foo is not a function'
356
+ }, {
357
+ kind: 'console',
358
+ level: 'error',
359
+ text: 'bar is not defined',
360
+ location: {
361
+ url: 'https://app/page'
362
+ }
363
+ }]);
364
+ expect(details.tags).toEqual(['CODE']);
365
+ expect(details.matches.filter(match => match.tag === 'CODE')).toHaveLength(3);
366
+ expect(details.matches.map(match => match.pattern)).toEqual(expect.arrayContaining(['Cannot read properties of (undefined|null)', '\\bis not a function\\b', '\\bis not defined\\b']));
367
+ });
368
+ test('keeps only winning CODE matches when CODE and ENVIRONMENT both match', () => {
369
+ const details = (0, _failureTagClassifier.classifyFailureDetails)([failingDetail([{
370
+ kind: 'requestfailed',
371
+ url: 'https://app/events/list/commentsubtabevent',
372
+ failure: 'net::ERR_ABORTED',
373
+ resourceType: 'document'
374
+ }, {
375
+ kind: 'requestfailed',
376
+ url: 'https://cdn/app.js',
377
+ failure: 'net::ERR_CONNECTION_REFUSED',
378
+ resourceType: 'script'
379
+ }, {
380
+ kind: 'pageerror',
381
+ name: 'TypeError',
382
+ message: "Cannot read properties of undefined (reading 'id')"
383
+ }, {
384
+ kind: 'pageerror',
385
+ name: 'TypeError',
386
+ message: 'handler is not a function'
387
+ }], {
388
+ title: 'multi-code-env-test'
389
+ })], 'multi-code-env-test');
390
+ expect(details.tags).toEqual(['CODE']);
391
+ expect(details.matches).toHaveLength(2);
392
+ expect(details.matches.every(match => match.tag === 'CODE')).toBe(true);
393
+ expect(details.matches.find(match => match.tag === 'ENVIRONMENT')).toBeUndefined();
394
+ });
395
+ test('dedupes identical winning-category matches', () => {
396
+ const details = (0, _failureTagClassifier.classifyFailureDetailsFromEvents)([{
397
+ kind: 'pageerror',
398
+ name: 'TypeError',
399
+ message: "Cannot read properties of undefined (reading 'id')"
400
+ }, {
401
+ kind: 'pageerror',
402
+ name: 'TypeError',
403
+ message: "Cannot read properties of undefined (reading 'id')"
404
+ }, {
405
+ kind: 'pageerror',
406
+ name: 'TypeError',
407
+ message: "Cannot read properties of undefined (reading 'id')"
408
+ }, {
409
+ kind: 'pageerror',
410
+ name: 'TypeError',
411
+ message: 'a is not a function'
412
+ }, {
413
+ kind: 'pageerror',
414
+ name: 'TypeError',
415
+ message: 'e.apply is not a function'
416
+ }, {
417
+ kind: 'pageerror',
418
+ name: 'TypeError',
419
+ message: 'a is not a function'
420
+ }]);
421
+ expect(details.tags).toEqual(['CODE']);
422
+ expect(details.matches).toHaveLength(3);
423
+ expect(details.matches.map(match => match.text)).toEqual(["pageerror TypeError Cannot read properties of undefined (reading 'id')", 'pageerror TypeError a is not a function', 'pageerror TypeError e.apply is not a function']);
424
+ });
425
+ test('dedupes annotated dual-match JSONL lines before classifying from disk', () => {
426
+ writeRuntimeLog([{
427
+ pattern: 'CODE',
428
+ kind: 'pageerror',
429
+ name: 'TypeError',
430
+ message: "Cannot read properties of undefined (reading 'id')"
431
+ }, {
432
+ pattern: 'ENVIRONMENT',
433
+ kind: 'pageerror',
434
+ name: 'TypeError',
435
+ message: "Cannot read properties of undefined (reading 'id')"
436
+ }], {
437
+ title: 'annotated-dual-test'
438
+ });
439
+ const details = (0, _failureTagClassifier.classifyFailureDetails)([{
440
+ status: 'unexpected',
441
+ results: [{
442
+ status: 'failed',
443
+ workerIndex: 0,
444
+ retry: 0
445
+ }]
446
+ }], 'annotated-dual-test');
447
+ expect(details.tags).toEqual(['CODE']);
448
+ expect(details.matches.filter(match => match.tag === 'CODE')).toHaveLength(1);
449
+ });
450
+ });
451
+ describe('annotateRuntimeLogEvents', () => {
452
+ test('adds pattern CODE on matching events and leaves unmatched events unchanged', () => {
453
+ const lines = (0, _runtimeLogAnnotator.annotateRuntimeLogEvents)([{
454
+ ts: 1,
455
+ kind: 'pageerror',
456
+ name: 'TypeError',
457
+ message: "Cannot read properties of undefined (reading 'id')"
458
+ }, {
459
+ ts: 2,
460
+ kind: 'console',
461
+ level: 'error',
462
+ text: 'Some benign log line',
463
+ location: {
464
+ url: 'https://app/page'
465
+ }
466
+ }]);
467
+ expect(lines).toHaveLength(2);
468
+ expect(lines[0]).toMatchObject({
469
+ pattern: 'CODE',
470
+ ts: 1,
471
+ kind: 'pageerror'
472
+ });
473
+ expect(lines[1]).toEqual({
474
+ ts: 2,
475
+ kind: 'console',
476
+ level: 'error',
477
+ text: 'Some benign log line',
478
+ location: {
479
+ url: 'https://app/page'
480
+ }
481
+ });
482
+ expect(lines[1].pattern).toBeUndefined();
483
+ });
484
+ test('emits one line per matched category when an event hits CODE and ENVIRONMENT', () => {
485
+ const originalCode = codePatterns.CODE_PATTERNS.slice();
486
+ try {
487
+ // Force a dual match on one environment event by also treating ERR_ABORTED as CODE.
488
+ codePatterns.CODE_PATTERNS.push(/ERR_ABORTED/i);
489
+ const event = {
490
+ ts: 1783961014561,
491
+ kind: 'requestfailed',
492
+ url: 'https://app/events/list/commentsubtabevent',
493
+ failure: 'net::ERR_ABORTED',
494
+ resourceType: 'document',
495
+ method: 'GET'
496
+ };
497
+ const lines = (0, _runtimeLogAnnotator.annotateRuntimeLogEvents)([event]);
498
+ expect(lines).toHaveLength(2);
499
+ expect(lines.map(line => line.pattern).sort()).toEqual(['CODE', 'ENVIRONMENT']);
500
+ expect(lines[0].ts).toBe(1783961014561);
501
+ expect(lines[1].ts).toBe(1783961014561);
502
+ } finally {
503
+ codePatterns.CODE_PATTERNS.length = 0;
504
+ codePatterns.CODE_PATTERNS.push(...originalCode);
505
+ }
506
+ });
325
507
  });
326
508
  describe('failure tag metadata attachment', () => {
327
509
  test('builds a small JSON attachment when tags are present', () => {
@@ -376,8 +558,8 @@ describe('mergeFailureTags', () => {
376
558
  test('places suffixed failure tags first, then existing tags', () => {
377
559
  expect((0, _failureTagClassifier.mergeFailureTags)(['@comments', '@calls'], ['CODE', 'ENVIRONMENT'])).toEqual(['CODE_failure', 'ENVIRONMENT_failure', '@comments', '@calls']);
378
560
  });
379
- test('emits the flaky category as FLAKY without a suffix', () => {
380
- expect((0, _failureTagClassifier.mergeFailureTags)([], ['FLAKY', 'CODE'])).toEqual(['FLAKY', 'CODE_failure']);
561
+ test('formats all failure tags with suffix', () => {
562
+ expect((0, _failureTagClassifier.mergeFailureTags)([], ['CODE', 'TOOL'])).toEqual(['CODE_failure', 'TOOL_failure']);
381
563
  });
382
564
  test('does not duplicate an already-present failure tag', () => {
383
565
  expect((0, _failureTagClassifier.mergeFailureTags)(['CODE_failure'], ['CODE'])).toEqual(['CODE_failure']);
@@ -399,8 +581,8 @@ describe('regex safety', () => {
399
581
  text: 'status : DATA_GENERATION_ERROR'
400
582
  }]
401
583
  });
402
- const first = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
403
- const second = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
584
+ const first = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
585
+ const second = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
404
586
  expect(first).toEqual(['TOOL']);
405
587
  expect(second).toEqual(['TOOL']);
406
588
  } finally {
@@ -114,10 +114,34 @@ describe('LogCollector baseline-noise stripping', () => {
114
114
  });
115
115
  const filePath = collector.flushToFile(flushDir, 'w0-test-0');
116
116
  const contents = _fs.default.readFileSync(filePath, 'utf8');
117
+ const line = JSON.parse(contents.trim());
117
118
  expect(filePath).toContain('w0-test-0.jsonl');
118
- expect(contents).toContain('"kind":"pageerror"');
119
+ expect(line).toMatchObject({
120
+ pattern: 'CODE',
121
+ kind: 'pageerror'
122
+ });
119
123
  expect(contents).toContain("Cannot read properties of undefined (reading 'id')");
120
124
  expect(contents.endsWith('\n')).toBe(true);
125
+ // In-memory events stay unannotated.
126
+ expect(collector.events[0].pattern).toBeUndefined();
127
+ } finally {
128
+ _fs.default.rmSync(flushDir, {
129
+ recursive: true,
130
+ force: true
131
+ });
132
+ }
133
+ });
134
+ test('flushes ENVIRONMENT pattern annotation for document aborts', () => {
135
+ const flushDir = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), 'log-collector-flush-'));
136
+ try {
137
+ context.emit('requestfailed', failedRequest('https://app/agent/events/list/commentsubtabevent', 'net::ERR_ABORTED', 'document'));
138
+ const filePath = collector.flushToFile(flushDir, 'w0-env-0');
139
+ const line = JSON.parse(_fs.default.readFileSync(filePath, 'utf8').trim());
140
+ expect(line).toMatchObject({
141
+ pattern: 'ENVIRONMENT',
142
+ kind: 'requestfailed',
143
+ resourceType: 'document'
144
+ });
121
145
  } finally {
122
146
  _fs.default.rmSync(flushDir, {
123
147
  recursive: true,