tewip-ai 0.17.1 → 0.17.3

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.
@@ -82,4 +82,4 @@ export function hasAssertionAfter(source, line) {
82
82
  * fixed-wait removals were refused as "nothing is asserted after the wait" when the very next line
83
83
  * was an assertion, which is how this was found. Python's bare `assert` counts for the same reason.
84
84
  */
85
- const ASSERTION = /\bexpect\s*\(|\.should\s*\(|\bassert\s+|\bassertThat\s*\(/;
85
+ const ASSERTION = /\bexpect\s*\(|\.should\s*\(|\bassert\s*[.(]|\bassert\s+|\bassertThat\s*\(/;
@@ -19,6 +19,10 @@
19
19
  // else Tewip must name something to wait for, or refuse.
20
20
  /** A step that waits by itself, so a fixed wait in front of it only adds time. */
21
21
  export function nextStepRetries(line, framework) {
22
+ // A step that is itself a wait needs no framework behind it: `await waitFor(() => ready)` blocks
23
+ // until the condition holds, so a fixed sleep in front of it is pure delay whatever the stack.
24
+ if (SELF_WAITING.test(line))
25
+ return true;
22
26
  if (framework === 'cypress') {
23
27
  // Every cy.* query retries until it resolves, except cy.wait itself.
24
28
  return /\bcy\.(?!wait\b)\w+\s*\(/.test(line);
@@ -38,7 +42,7 @@ export function nextStepRetries(line, framework) {
38
42
  // An action on a locator waits for that element to be actionable first.
39
43
  return ACTION_ON_LOCATOR.test(line);
40
44
  }
41
- const SELF_WAITING = /\.\s*waitFor(?:Selector|URL|LoadState|Function|Event|Response|Request|Navigation)?\s*\(|\.\s*goto\s*\(/;
45
+ const SELF_WAITING = /\.\s*waitFor(?:Selector|URL|LoadState|Function|Event|Response|Request|Navigation)?\s*\(|\.\s*goto\s*\(|\b(?:waitFor|waitUntil|wait_for|waitForCondition|until)\w*\s*\(/;
42
46
  const LOCATOR_EXPRESSION = /(?:page|frame|component|[\w$]+)\s*\.\s*(?:getBy\w+|locator|frameLocator)\s*\(|^\s*[\w$]+\s*$/;
43
47
  const ACTION_ON_LOCATOR = /(?:getBy\w+\([^;]*?\)|locator\([^;]*?\))(?:\s*\.\s*\w+\([^;]*?\))*?\s*\.\s*(?:click|fill|press|check|uncheck|selectOption|hover|type|tap|dragTo|setInputFiles|focus)\s*\(/;
44
48
  /**
@@ -47,7 +47,37 @@ function relativeFile(file, workspace) {
47
47
  return f.slice(ws.length);
48
48
  return f;
49
49
  }
50
+ /**
51
+ * Does this report come from a run that re-ran tests? Either the runner said so with a rerun
52
+ * element, or some test in it is written twice with a failure recorded - both of which pytest's
53
+ * rerun plugin does and an ordinary passing run never does.
54
+ */
55
+ function looksLikeReruns(root) {
56
+ let rerunElements = false;
57
+ let failureSeen = false;
58
+ const names = new Set();
59
+ let duplicateWithFailure = false;
60
+ const walk = (node) => {
61
+ for (const child of node.children) {
62
+ if (child.name === 'testcase') {
63
+ const key = `${child.attrs.classname ?? ''}::${child.attrs.name ?? ''}`;
64
+ const failed = child.children.some((c) => FAILURE.has(c.name));
65
+ if (child.children.some((c) => RETRY_FAILURE.has(c.name)))
66
+ rerunElements = true;
67
+ if (failed)
68
+ failureSeen = true;
69
+ if (names.has(key) && failureSeen)
70
+ duplicateWithFailure = true;
71
+ names.add(key);
72
+ }
73
+ walk(child);
74
+ }
75
+ };
76
+ walk(root);
77
+ return rerunElements || duplicateWithFailure;
78
+ }
50
79
  function junitCases(root, workspace) {
80
+ const rerunning = looksLikeReruns(root);
51
81
  const out = [];
52
82
  const walk = (e, suite) => {
53
83
  for (const c of e.children) {
@@ -70,10 +100,16 @@ function junitCases(root, workspace) {
70
100
  const previous = out.at(-1);
71
101
  if (previous?.testId === testId) {
72
102
  // Rerun plugins (pytest-rerunfailures and others) write every attempt as its own entry and
73
- // only re-run after a failure, so an earlier entry of the same test is a failed attempt even
74
- // when the plugin left its error out. Only adjacent entries are merged.
103
+ // only re-run after a failure, so an earlier entry of the same test can be a failed attempt
104
+ // whose error the plugin left out. Only adjacent entries are merged.
105
+ //
106
+ // The inference is only safe where the report shows this runner re-runs tests at all. Without
107
+ // that condition two adjacent entries of the same name were enough to invent a failure, and
108
+ // real suites produce those innocently: node:test repeats a name for a subtest and its
109
+ // parent, so piscina's own 152 passing tests came back as two failing ones, diagnosed as
110
+ // flaky. A test that passed must never be reported as having failed.
75
111
  const earlier = previous.attempts.at(-1);
76
- if (!earlier.failed)
112
+ if (!earlier.failed && rerunning)
77
113
  previous.attempts[previous.attempts.length - 1] = { failed: true, message: '', stack: '' };
78
114
  previous.attempts.push(...attempts);
79
115
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.17.1",
3
+ "version": "0.17.3",
4
4
  "description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
5
5
  "keywords": [
6
6
  "test-failures",