@zohodesk/testinglibrary 0.0.84-n20-experimental → 0.0.86-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.
@@ -3,17 +3,15 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.FAILURE_TAG_SUFFIX = exports.FAILURE_TAG_METADATA_SCHEMA_VERSION = exports.FAILURE_TAG_METADATA_ATTACHMENT = exports.FAILURE_TAGS = void 0;
6
+ exports.FAILURE_TAG_SUFFIX = exports.FAILURE_TAG_METADATA_ATTACHMENT = exports.FAILURE_TAGS = void 0;
7
7
  const FAILURE_TAGS = exports.FAILURE_TAGS = {
8
8
  CASE: 'CASE',
9
9
  CODE: 'CODE',
10
10
  ENVIRONMENT: 'ENVIRONMENT',
11
- FLAKY: 'FLAKY',
12
11
  TOOL: 'TOOL'
13
12
  };
14
13
 
15
14
  // Suffix appended to a category when merging into the existing spec.tags array,
16
- // e.g. "CODE" -> "CODE_failure". The flaky category is emitted as-is ("FLAKY").
15
+ // e.g. "CODE" -> "CODE_failure".
17
16
  const FAILURE_TAG_SUFFIX = exports.FAILURE_TAG_SUFFIX = '_failure';
18
- const FAILURE_TAG_METADATA_ATTACHMENT = exports.FAILURE_TAG_METADATA_ATTACHMENT = 'failure-tag-metadata';
19
- const FAILURE_TAG_METADATA_SCHEMA_VERSION = exports.FAILURE_TAG_METADATA_SCHEMA_VERSION = 1;
17
+ const FAILURE_TAG_METADATA_ATTACHMENT = exports.FAILURE_TAG_METADATA_ATTACHMENT = 'failure-tag-metadata';
@@ -16,19 +16,20 @@ var _failureTagConstants = require("../constants/failureTagConstants");
16
16
  var _patterns = require("../patterns");
17
17
  var _readConfigFile = require("../readConfigFile");
18
18
  var _runtimeLogReader = require("./runtimeLogReader");
19
- // A failing test is tagged purely on presence: if its runtime log contains an
20
- // environment pattern it gets `ENVIRONMENT`, if it contains a code pattern it
21
- // gets `CODE`, and if its stdout/stderr contains a tool pattern it gets `TOOL`
22
- // or a case pattern it gets `CASE` (any may apply). A `CASE` match also adds
23
- // `CODE` for now via case-implied metadata. `FLAKY` comes from the Playwright
24
- // retry outcome, not the log.
19
+ // A failing test resolves to a SINGLE category tag by strict priority:
20
+ // CODE > CASE > TOOL > ENVIRONMENT. If several categories match, only the
21
+ // highest-priority one is emitted (e.g. CODE + ENVIRONMENT -> CODE only).
22
+ // CODE/ENVIRONMENT patterns are matched against the runtime log; TOOL/CASE
23
+ // patterns against stdout/stderr. If a failure matches no category at all it
24
+ // defaults to CODE. Retry-passed (flaky) outcomes are not tagged.
25
25
 
26
26
  const FAILING_RESULT_STATUSES = new Set(['failed', 'timedOut', 'interrupted']);
27
27
  const UNEXPECTED_OUTCOME = 'unexpected';
28
- const FLAKY_OUTCOME = 'flaky';
29
28
  const RUNTIME_LOG_SOURCE = 'runtime-log';
30
29
  const MAX_MATCH_TEXT_LENGTH = 500;
31
- const TAG_ORDER = [_failureTagConstants.FAILURE_TAGS.FLAKY, _failureTagConstants.FAILURE_TAGS.ENVIRONMENT, _failureTagConstants.FAILURE_TAGS.CASE, _failureTagConstants.FAILURE_TAGS.CODE, _failureTagConstants.FAILURE_TAGS.TOOL];
30
+
31
+ // Single-tag priority: the highest-ranked matched category wins.
32
+ const CATEGORY_PRIORITY = [_failureTagConstants.FAILURE_TAGS.CODE, _failureTagConstants.FAILURE_TAGS.CASE, _failureTagConstants.FAILURE_TAGS.TOOL, _failureTagConstants.FAILURE_TAGS.ENVIRONMENT];
32
33
  const RUNTIME_LOG_RULES = [{
33
34
  tag: _failureTagConstants.FAILURE_TAGS.ENVIRONMENT,
34
35
  patterns: _patterns.ENVIRONMENT_PATTERNS
@@ -41,13 +42,9 @@ const STDIO_RULES = [{
41
42
  patterns: _patterns.TOOL_PATTERNS
42
43
  }, {
43
44
  tag: _failureTagConstants.FAILURE_TAGS.CASE,
44
- patterns: _patterns.CASE_PATTERNS,
45
- impliesCode: true
45
+ patterns: _patterns.CASE_PATTERNS
46
46
  }];
47
- function resolveLogsDir(logsDir) {
48
- if (logsDir) {
49
- return logsDir;
50
- }
47
+ function resolveLogsDir() {
51
48
  const {
52
49
  uatDirectory
53
50
  } = (0, _readConfigFile.generateConfigFromFile)();
@@ -147,33 +144,18 @@ function collectRuntimeLogMatches(events, matches) {
147
144
  function collectStdioMatches(stdio, matches) {
148
145
  for (const rule of STDIO_RULES) {
149
146
  const match = findStdioMatch(stdio, rule.patterns, rule.tag);
150
- if (!match) {
151
- continue;
152
- }
153
- matches.push(match);
154
- if (rule.impliesCode) {
155
- implyCodeFromCase(matches, match);
147
+ if (match) {
148
+ matches.push(match);
156
149
  }
157
150
  }
158
151
  }
159
- function implyCodeFromCase(matches, caseMatch) {
160
- if (matches.some(match => match.tag === _failureTagConstants.FAILURE_TAGS.CODE)) {
161
- return;
162
- }
163
- matches.push({
164
- tag: _failureTagConstants.FAILURE_TAGS.CODE,
165
- pattern: null,
166
- source: 'case-implied',
167
- text: caseMatch.text
168
- });
169
- }
170
152
  function classifyFailureDetailsFromEvents(events = []) {
171
153
  const matches = [];
172
154
  collectRuntimeLogMatches(events, matches);
173
155
  return buildFailureTagMetadata(matches);
174
156
  }
175
- function classifyFailureDetailsFromResult(result, testTitle, logsDir) {
176
- const resolvedLogsDir = resolveLogsDir(logsDir);
157
+ function classifyFailureDetailsFromResult(result, testTitle) {
158
+ const resolvedLogsDir = resolveLogsDir();
177
159
  const events = (0, _runtimeLogReader.readRuntimeLogEventsForResult)(result, testTitle, resolvedLogsDir);
178
160
  const matches = [];
179
161
  collectRuntimeLogMatches(events, matches);
@@ -181,26 +163,24 @@ function classifyFailureDetailsFromResult(result, testTitle, logsDir) {
181
163
  return buildFailureTagMetadata(matches);
182
164
  }
183
165
  function buildFailureTagMetadata(matches = []) {
184
- const tags = TAG_ORDER.filter(tag => matches.some(match => match.tag === tag));
166
+ const tags = [];
167
+
168
+ // Collapse the matched categories to the single highest-priority winner.
169
+ // When a failure matched no category, fall back to CODE.
170
+ const winningCategory = CATEGORY_PRIORITY.find(tag => matches.some(match => match.tag === tag));
171
+ if (winningCategory) {
172
+ tags.push(winningCategory);
173
+ } else {
174
+ tags.push(_failureTagConstants.FAILURE_TAGS.CODE);
175
+ }
185
176
  return {
186
- schemaVersion: _failureTagConstants.FAILURE_TAG_METADATA_SCHEMA_VERSION,
187
177
  tags,
188
178
  matches
189
179
  };
190
180
  }
191
- function classifyFailureDetails(testDetails = [], testTitle = 'test', logsDir) {
192
- const resolvedLogsDir = resolveLogsDir(logsDir);
181
+ function classifyFailureDetails(testDetails = [], testTitle = 'test') {
193
182
  const matches = [];
194
183
  for (const detail of testDetails) {
195
- if (detail.status === FLAKY_OUTCOME) {
196
- matches.push({
197
- tag: _failureTagConstants.FAILURE_TAGS.FLAKY,
198
- pattern: null,
199
- source: 'retry-outcome',
200
- text: null
201
- });
202
- continue;
203
- }
204
184
  if (detail.status !== UNEXPECTED_OUTCOME) {
205
185
  continue;
206
186
  }
@@ -208,7 +188,7 @@ function classifyFailureDetails(testDetails = [], testTitle = 'test', logsDir) {
208
188
  if (!failingResult) {
209
189
  continue;
210
190
  }
211
- const resultDetails = classifyFailureDetailsFromResult(failingResult, testTitle, resolvedLogsDir);
191
+ const resultDetails = classifyFailureDetailsFromResult(failingResult, testTitle);
212
192
  matches.push(...resultDetails.matches);
213
193
  }
214
194
  const uniqueMatches = [];
@@ -217,10 +197,16 @@ function classifyFailureDetails(testDetails = [], testTitle = 'test', logsDir) {
217
197
  uniqueMatches.push(match);
218
198
  }
219
199
  }
200
+ if (!testDetails.some(detail => detail.status === UNEXPECTED_OUTCOME)) {
201
+ return {
202
+ tags: [],
203
+ matches: uniqueMatches
204
+ };
205
+ }
220
206
  return buildFailureTagMetadata(uniqueMatches);
221
207
  }
222
- function classifyFailureTags(testDetails = [], testTitle = 'test', logsDir) {
223
- return classifyFailureDetails(testDetails, testTitle, logsDir).tags;
208
+ function classifyFailureTags(testDetails = [], testTitle = 'test') {
209
+ return classifyFailureDetails(testDetails, testTitle).tags;
224
210
  }
225
211
  function pickFailingResult(results = []) {
226
212
  const failing = results.filter(result => FAILING_RESULT_STATUSES.has(result.status));
@@ -230,9 +216,6 @@ function pickFailingResult(results = []) {
230
216
  return failing[failing.length - 1];
231
217
  }
232
218
  function formatFailureTag(tag) {
233
- if (tag === _failureTagConstants.FAILURE_TAGS.FLAKY) {
234
- return tag;
235
- }
236
219
  return `${tag}${_failureTagConstants.FAILURE_TAG_SUFFIX}`;
237
220
  }
238
221
  function mergeFailureTags(existingTags = [], failureTags = []) {
@@ -20,15 +20,7 @@ const TOOL_PATTERNS = exports.TOOL_PATTERNS = [/Error during login/i, /fetch fai
20
20
  /Error while parsing cookies/i,
21
21
  // FileMutex
22
22
  /Watch timeout exceeded/i,
23
- // Runner
24
- /Method 'run\(\)' must be implemented\./i, /Invalid runner type/i,
25
- // Process
26
- /Child Process Exited with Code/i, /Terminating Playwright Process\.\.\./i, /Cleaning up\.\.\./i,
27
- // TestData
28
- /Error appending or creating the test data file:/i, /Error\(err\)/i, /Error while deleting the test data file:/i,
29
- // FeatureFile
30
- /Error while parsing feature files\. Please fix the above issues before running test cases/i,
31
23
  // RPC
32
- /Error: HTTP /i, /Invalid payload\. It must be a non-null object\./i, /Invalid payload\.arguments\.entityId\./i,
24
+ /Invalid payload\. It must be a non-null object\./i, /Invalid payload\.arguments\.entityId\./i,
33
25
  // Search
34
26
  /Invalid or missing data in dataTable/i, /JSONPath query/i];
@@ -6,18 +6,25 @@ 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 _readConfigFile = require("../../../../../core/playwright/readConfigFile");
9
10
  var _runtimeLogPath = require("../../../../../core/playwright/helpers/runtimeLogPath");
10
11
  var _failureTagClassifier = require("../../../../../core/playwright/helpers/failureTagClassifier");
11
12
  var _failureTagConstants = require("../../../../../core/playwright/constants/failureTagConstants");
12
13
  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); }
14
+ jest.mock('../../../../../core/playwright/readConfigFile', () => ({
15
+ generateConfigFromFile: jest.fn()
16
+ }));
13
17
  const TEST_TITLE = 'environment-abort-test';
14
18
  let tempDir;
19
+ function runtimeLogsDir() {
20
+ return _path.default.join(tempDir, 'runtime-logs');
21
+ }
15
22
  function writeRuntimeLog(events, {
16
23
  workerIndex = 0,
17
24
  retry = 0,
18
25
  title = TEST_TITLE
19
26
  } = {}) {
20
- const file = (0, _runtimeLogPath.resolveRuntimeLogFilePath)(tempDir, {
27
+ const file = (0, _runtimeLogPath.resolveRuntimeLogFilePath)(runtimeLogsDir(), {
21
28
  workerIndex,
22
29
  retry,
23
30
  title
@@ -55,6 +62,9 @@ function failingDetailWithStdio({
55
62
  }
56
63
  beforeEach(() => {
57
64
  tempDir = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), 'failure-tag-'));
65
+ _readConfigFile.generateConfigFromFile.mockReturnValue({
66
+ uatDirectory: tempDir
67
+ });
58
68
  });
59
69
  afterEach(() => {
60
70
  _fs.default.rmSync(tempDir, {
@@ -63,12 +73,12 @@ afterEach(() => {
63
73
  });
64
74
  });
65
75
  describe('classifyFailureTags', () => {
66
- test('returns flaky and skips log analysis for flaky outcome', () => {
76
+ test('leaves retry-passed (flaky) outcomes untagged', () => {
67
77
  const tags = (0, _failureTagClassifier.classifyFailureTags)([{
68
78
  status: 'flaky',
69
79
  results: []
70
- }], TEST_TITLE, tempDir);
71
- expect(tags).toEqual(['FLAKY']);
80
+ }], TEST_TITLE);
81
+ expect(tags).toEqual([]);
72
82
  });
73
83
  test('tags environment when a document abort is present', () => {
74
84
  const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
@@ -76,7 +86,7 @@ describe('classifyFailureTags', () => {
76
86
  url: 'https://app/events/list/commentsubtabevent',
77
87
  failure: 'net::ERR_ABORTED',
78
88
  resourceType: 'document'
79
- }])], TEST_TITLE, tempDir);
89
+ }])], TEST_TITLE);
80
90
  expect(tags).toEqual(['ENVIRONMENT']);
81
91
  });
82
92
  test('tags environment for a network/cert failure', () => {
@@ -87,7 +97,7 @@ describe('classifyFailureTags', () => {
87
97
  resourceType: 'script'
88
98
  }], {
89
99
  title: 'cert-test'
90
- })], 'cert-test', tempDir);
100
+ })], 'cert-test');
91
101
  expect(tags).toEqual(['ENVIRONMENT']);
92
102
  });
93
103
  test('tags code when an app undefined-property error is present', () => {
@@ -97,10 +107,10 @@ describe('classifyFailureTags', () => {
97
107
  message: "Cannot read properties of undefined (reading 'id')"
98
108
  }], {
99
109
  title: 'code-test'
100
- })], 'code-test', tempDir);
110
+ })], 'code-test');
101
111
  expect(tags).toEqual(['CODE']);
102
112
  });
103
- test('does not tag a non-document (xhr) abort as environment', () => {
113
+ test('defaults to code (not environment) for a non-document xhr abort', () => {
104
114
  const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
105
115
  kind: 'requestfailed',
106
116
  url: 'https://app/api/tickets',
@@ -108,17 +118,17 @@ describe('classifyFailureTags', () => {
108
118
  resourceType: 'xhr'
109
119
  }], {
110
120
  title: 'xhr-test'
111
- })], 'xhr-test', tempDir);
112
- expect(tags).toEqual([]);
121
+ })], 'xhr-test');
122
+ expect(tags).toEqual(['CODE']);
113
123
  });
114
- test('tags case and code when stderr reports DATA_GENERATION_ERROR', () => {
124
+ test('tags case when stderr reports DATA_GENERATION_ERROR', () => {
115
125
  const detail = failingDetailWithStdio({
116
126
  stderr: [{
117
127
  text: 'status : DATA_GENERATION_ERROR\n generator: comments'
118
128
  }]
119
129
  });
120
- const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
121
- expect(tags).toEqual(['CASE', 'CODE']);
130
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
131
+ expect(tags).toEqual(['CASE']);
122
132
  });
123
133
  test('tags tool for a known tool pattern in base64 stdout', () => {
124
134
  const detail = failingDetailWithStdio({
@@ -126,10 +136,10 @@ describe('classifyFailureTags', () => {
126
136
  buffer: Buffer.from('Error during login').toString('base64')
127
137
  }]
128
138
  });
129
- const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
139
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
130
140
  expect(tags).toEqual(['TOOL']);
131
141
  });
132
- test('tags case and code when stderr matches a case pattern', () => {
142
+ test('tags case when stderr matches a case pattern', () => {
133
143
  const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
134
144
  try {
135
145
  casePatterns.CASE_PATTERNS.push(/expect\(received\)\.toBe/i);
@@ -138,22 +148,37 @@ describe('classifyFailureTags', () => {
138
148
  text: 'Error: expect(received).toBe(expected)'
139
149
  }]
140
150
  });
141
- const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
142
- expect(tags).toEqual(['CASE', 'CODE']);
151
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
152
+ expect(tags).toEqual(['CASE']);
143
153
  } finally {
144
154
  casePatterns.CASE_PATTERNS.length = 0;
145
155
  casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
146
156
  }
147
157
  });
148
- test('does not tag tool for benign stdout/stderr', () => {
158
+ test('emits only the highest-priority tag (CODE) when CODE and ENVIRONMENT both match', () => {
159
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
160
+ kind: 'requestfailed',
161
+ url: 'https://app/events/list/commentsubtabevent',
162
+ failure: 'net::ERR_ABORTED',
163
+ resourceType: 'document'
164
+ }, {
165
+ kind: 'pageerror',
166
+ name: 'TypeError',
167
+ message: "Cannot read properties of undefined (reading 'id')"
168
+ }], {
169
+ title: 'code-over-env-test'
170
+ })], 'code-over-env-test');
171
+ expect(tags).toEqual(['CODE']);
172
+ });
173
+ test('defaults to code (not tool) for benign stdout/stderr', () => {
149
174
  const detail = failingDetailWithStdio({
150
175
  stdout: [{
151
176
  text: 'Generated 5 records successfully'
152
177
  }],
153
178
  stderr: []
154
179
  });
155
- const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
156
- expect(tags).toEqual([]);
180
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
181
+ expect(tags).toEqual(['CODE']);
157
182
  });
158
183
  test('does not throw for malformed stdio chunk entries', () => {
159
184
  const detail = failingDetailWithStdio({
@@ -162,10 +187,10 @@ describe('classifyFailureTags', () => {
162
187
  }],
163
188
  stderr: [false]
164
189
  });
165
- expect(() => (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir)).not.toThrow();
166
- expect((0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir)).toEqual([]);
190
+ expect(() => (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE)).not.toThrow();
191
+ expect((0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE)).toEqual(['CODE']);
167
192
  });
168
- test('leaves untagged when no environment or code pattern is present', () => {
193
+ test('defaults to code when a failure matches no pattern', () => {
169
194
  const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
170
195
  kind: 'response',
171
196
  status: 200,
@@ -180,7 +205,18 @@ describe('classifyFailureTags', () => {
180
205
  }
181
206
  }], {
182
207
  title: 'neutral-test'
183
- })], 'neutral-test', tempDir);
208
+ })], 'neutral-test');
209
+ expect(tags).toEqual(['CODE']);
210
+ });
211
+ test('leaves a passing spec untagged (no default code)', () => {
212
+ const tags = (0, _failureTagClassifier.classifyFailureTags)([{
213
+ status: 'expected',
214
+ results: [{
215
+ status: 'passed',
216
+ workerIndex: 0,
217
+ retry: 0
218
+ }]
219
+ }], 'passing-test');
184
220
  expect(tags).toEqual([]);
185
221
  });
186
222
  });
@@ -191,7 +227,7 @@ describe('classifyFailureDetails', () => {
191
227
  url: 'https://app/events/list/commentsubtabevent',
192
228
  failure: 'net::ERR_ABORTED',
193
229
  resourceType: 'document'
194
- }])], TEST_TITLE, tempDir);
230
+ }])], TEST_TITLE);
195
231
  expect(details.tags).toEqual(['ENVIRONMENT']);
196
232
  expect(details.matches).toHaveLength(1);
197
233
  expect(details.matches[0]).toMatchObject({
@@ -214,25 +250,17 @@ describe('classifyFailureDetails', () => {
214
250
  source: 'runtime-log'
215
251
  });
216
252
  });
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
- });
253
+ test('defaults to code with no matches when events carry no recognised pattern', () => {
254
+ const details = (0, _failureTagClassifier.classifyFailureDetailsFromEvents)([]);
255
+ expect(details.tags).toEqual(['CODE']);
256
+ expect(details.matches).toEqual([]);
229
257
  });
230
258
  test('records tool pattern from stdio', () => {
231
259
  const details = (0, _failureTagClassifier.classifyFailureDetails)([failingDetailWithStdio({
232
260
  stderr: [{
233
261
  text: 'Error during login for actor admin'
234
262
  }]
235
- })], TEST_TITLE, tempDir);
263
+ })], TEST_TITLE);
236
264
  expect(details.tags).toEqual(['TOOL']);
237
265
  expect(details.matches[0]).toMatchObject({
238
266
  tag: 'TOOL',
@@ -240,7 +268,7 @@ describe('classifyFailureDetails', () => {
240
268
  source: 'stdio'
241
269
  });
242
270
  });
243
- test('records case pattern from stdio and implies code', () => {
271
+ test('records case pattern from stdio without implying code', () => {
244
272
  const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
245
273
  try {
246
274
  casePatterns.CASE_PATTERNS.push(/expect\(received\)\.toBe/i);
@@ -248,26 +276,20 @@ describe('classifyFailureDetails', () => {
248
276
  stderr: [{
249
277
  text: 'Error: expect(received).toBe(expected)'
250
278
  }]
251
- })], TEST_TITLE, tempDir);
252
- expect(details.tags).toEqual(['CASE', 'CODE']);
253
- expect(details.matches).toHaveLength(2);
279
+ })], TEST_TITLE);
280
+ expect(details.tags).toEqual(['CASE']);
281
+ expect(details.matches).toHaveLength(1);
254
282
  expect(details.matches[0]).toMatchObject({
255
283
  tag: 'CASE',
256
284
  pattern: 'expect\\(received\\)\\.toBe',
257
285
  source: 'stdio'
258
286
  });
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
287
  } finally {
266
288
  casePatterns.CASE_PATTERNS.length = 0;
267
289
  casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
268
290
  }
269
291
  });
270
- test('keeps runtime-log code match when case also matches', () => {
292
+ test('resolves to CODE when a runtime-log code match outranks a stdio case match', () => {
271
293
  const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
272
294
  try {
273
295
  casePatterns.CASE_PATTERNS.push(/expect\(received\)\.toBe/i);
@@ -288,8 +310,10 @@ describe('classifyFailureDetails', () => {
288
310
  text: 'Error: expect(received).toBe(expected)'
289
311
  }]
290
312
  }]
291
- }], 'case-with-runtime-code', tempDir);
292
- expect(details.tags).toEqual(['CASE', 'CODE']);
313
+ }], 'case-with-runtime-code');
314
+
315
+ // CODE outranks CASE, so only CODE surfaces as the tag; both remain as evidence.
316
+ expect(details.tags).toEqual(['CODE']);
293
317
  expect(details.matches).toHaveLength(2);
294
318
  expect(details.matches.find(match => match.tag === 'CASE')).toMatchObject({
295
319
  source: 'stdio'
@@ -303,7 +327,7 @@ describe('classifyFailureDetails', () => {
303
327
  casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
304
328
  }
305
329
  });
306
- test('implies code from case when no runtime log is present', () => {
330
+ test('keeps case as the tag and does not add code when no runtime log is present', () => {
307
331
  const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
308
332
  try {
309
333
  casePatterns.CASE_PATTERNS.push(/Timed out \d+ms waiting for/i);
@@ -311,12 +335,9 @@ describe('classifyFailureDetails', () => {
311
335
  stderr: [{
312
336
  text: 'Timed out 5000ms waiting for locator("#save")'
313
337
  }]
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
- });
338
+ })], TEST_TITLE);
339
+ expect(details.tags).toEqual(['CASE']);
340
+ expect(details.matches.find(match => match.tag === 'CODE')).toBeUndefined();
320
341
  } finally {
321
342
  casePatterns.CASE_PATTERNS.length = 0;
322
343
  casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
@@ -376,8 +397,8 @@ describe('mergeFailureTags', () => {
376
397
  test('places suffixed failure tags first, then existing tags', () => {
377
398
  expect((0, _failureTagClassifier.mergeFailureTags)(['@comments', '@calls'], ['CODE', 'ENVIRONMENT'])).toEqual(['CODE_failure', 'ENVIRONMENT_failure', '@comments', '@calls']);
378
399
  });
379
- test('emits the flaky category as FLAKY without a suffix', () => {
380
- expect((0, _failureTagClassifier.mergeFailureTags)([], ['FLAKY', 'CODE'])).toEqual(['FLAKY', 'CODE_failure']);
400
+ test('formats all failure tags with suffix', () => {
401
+ expect((0, _failureTagClassifier.mergeFailureTags)([], ['CODE', 'TOOL'])).toEqual(['CODE_failure', 'TOOL_failure']);
381
402
  });
382
403
  test('does not duplicate an already-present failure tag', () => {
383
404
  expect((0, _failureTagClassifier.mergeFailureTags)(['CODE_failure'], ['CODE'])).toEqual(['CODE_failure']);
@@ -399,8 +420,8 @@ describe('regex safety', () => {
399
420
  text: 'status : DATA_GENERATION_ERROR'
400
421
  }]
401
422
  });
402
- const first = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
403
- const second = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
423
+ const first = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
424
+ const second = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE);
404
425
  expect(first).toEqual(['TOOL']);
405
426
  expect(second).toEqual(['TOOL']);
406
427
  } finally {