e2e 0.16.0 → 0.17.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/.build.json +1 -1
- package/dist/agent/act.js +7 -4
- package/dist/agent/action-dispatcher.js +4 -5
- package/dist/agent/actions.d.ts +32 -23
- package/dist/agent/actions.js +38 -24
- package/dist/agent/model/sdk.js +2 -0
- package/dist/agent/observation-feed.d.ts +5 -6
- package/dist/agent/observation-feed.js +5 -7
- package/dist/agent/observation.d.ts +40 -11
- package/dist/agent/observation.js +109 -59
- package/dist/agent/point-tap.d.ts +8 -8
- package/dist/agent/point-tap.js +1 -1
- package/dist/agent/replay.d.ts +19 -8
- package/dist/agent/replay.js +11 -14
- package/dist/agent/scroll-target.d.ts +2 -3
- package/dist/agent/step-cache.d.ts +41 -14
- package/dist/agent/step-cache.js +147 -62
- package/dist/agent/tool-loop.js +2 -0
- package/dist/cache/anchors.d.ts +72 -23
- package/dist/cache/anchors.js +233 -60
- package/dist/cache/context.d.ts +25 -14
- package/dist/cache/context.js +50 -8
- package/dist/cache/decide.d.ts +2 -2
- package/dist/cache/decide.js +4 -6
- package/dist/cache/identity.d.ts +18 -4
- package/dist/cache/identity.js +9 -4
- package/dist/cache/recorder.d.ts +15 -1
- package/dist/cache/recorder.js +36 -23
- package/dist/cache/rekeyed.d.ts +46 -0
- package/dist/cache/rekeyed.js +89 -0
- package/dist/cache/relocate.d.ts +14 -14
- package/dist/cache/relocate.js +33 -25
- package/dist/cache/route.d.ts +32 -33
- package/dist/cache/route.js +94 -50
- package/dist/cache/store.d.ts +6 -0
- package/dist/cache/store.js +21 -1
- package/dist/cache/trace.d.ts +53 -14
- package/dist/cache/trace.js +70 -28
- package/dist/cli/index.js +8 -8
- package/dist/cli/init/engines.d.ts +1 -3
- package/dist/cli/init/engines.js +2 -2
- package/dist/cli/init/gateways.d.ts +1 -1
- package/dist/cli/init/gateways.js +11 -1
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/cli/init/versions.d.ts +0 -8
- package/dist/cli/init/versions.js +1 -10
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.js +33 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/expect/async.js +2 -1
- package/dist/expect/poll-scope.d.ts +52 -0
- package/dist/expect/poll-scope.js +113 -0
- package/dist/expect/poll.js +31 -4
- package/dist/expect/values.js +11 -4
- package/dist/internal/abandoned.d.ts +11 -0
- package/dist/internal/abandoned.js +23 -0
- package/dist/internal/brands.d.ts +2 -0
- package/dist/internal/brands.js +2 -0
- package/dist/internal/client-identity.d.ts +9 -0
- package/dist/internal/client-identity.js +14 -0
- package/dist/internal/redact.d.ts +7 -4
- package/dist/internal/redact.js +116 -23
- package/dist/internal/time.d.ts +6 -0
- package/dist/internal/time.js +25 -13
- package/dist/internal/urls.d.ts +9 -5
- package/dist/internal/urls.js +19 -10
- package/dist/locator/engine.d.ts +12 -2
- package/dist/locator/engine.js +8 -3
- package/dist/locator/screen.js +28 -4
- package/dist/mcp/catalog.d.ts +3 -2
- package/dist/mcp/catalog.js +6 -6
- package/dist/mcp/session.js +1 -1
- package/dist/oauth/chatgpt.js +3 -23
- package/dist/oauth/cli.js +1 -0
- package/dist/oauth/copilot.d.ts +5 -4
- package/dist/oauth/copilot.js +80 -7
- package/dist/oauth/grok.js +2 -1
- package/dist/oauth/models.js +2 -1
- package/dist/oauth/opencode-console.d.ts +10 -0
- package/dist/oauth/opencode-console.js +133 -0
- package/dist/oauth/providers/github-copilot.d.ts +12 -0
- package/dist/oauth/providers/github-copilot.js +77 -23
- package/dist/oauth/providers/opencode-console.d.ts +31 -0
- package/dist/oauth/providers/opencode-console.js +148 -0
- package/dist/oauth/providers.d.ts +2 -6
- package/dist/oauth/providers.js +3 -7
- package/dist/oauth/responses.d.ts +9 -0
- package/dist/oauth/responses.js +23 -0
- package/dist/report/build.d.ts +26 -0
- package/dist/report/build.js +16 -8
- package/dist/report/failure-text.d.ts +6 -1
- package/dist/report/failure-text.js +14 -4
- package/dist/report/format.d.ts +10 -5
- package/dist/report/format.js +12 -5
- package/dist/report/junit.js +11 -12
- package/dist/report/list.d.ts +1 -0
- package/dist/report/list.js +38 -18
- package/dist/report/markdown.d.ts +1 -1
- package/dist/report/markdown.js +37 -14
- package/dist/report/repeats.d.ts +6 -1
- package/dist/report/repeats.js +12 -3
- package/dist/run/artifacts.d.ts +18 -0
- package/dist/run/artifacts.js +68 -1
- package/dist/run/execute.d.ts +9 -0
- package/dist/run/execute.js +49 -24
- package/dist/run/extended-fixtures.js +6 -2
- package/dist/run/failure-evidence.js +4 -2
- package/dist/run/fixtures.js +6 -8
- package/dist/run/in-process.d.ts +2 -0
- package/dist/run/in-process.js +1 -0
- package/dist/run/last-run.d.ts +42 -8
- package/dist/run/last-run.js +121 -14
- package/dist/run/realm.d.ts +4 -1
- package/dist/run/realm.js +20 -6
- package/dist/run/records.d.ts +16 -0
- package/dist/run/records.js +26 -0
- package/dist/run/retry.d.ts +3 -1
- package/dist/run/retry.js +4 -2
- package/dist/run/runner.d.ts +1 -1
- package/dist/run/runner.js +69 -30
- package/dist/run/serial.d.ts +2 -0
- package/dist/run/serial.js +8 -5
- package/dist/run/steps.d.ts +0 -6
- package/dist/run/steps.js +3 -20
- package/dist/run/worker/entry.js +5 -4
- package/dist/run/worker/protocol.d.ts +2 -0
- package/dist/run/worker/session.d.ts +2 -0
- package/dist/run/worker/session.js +1 -0
- package/dist/telemetry/events.js +1 -0
- package/dist/types.d.ts +9 -3
- package/docs/agents.mdx +6 -3
- package/docs/assertions.mdx +94 -0
- package/docs/authentication.mdx +3 -1
- package/docs/browser.mdx +42 -6
- package/docs/cache.mdx +64 -18
- package/docs/ci.mdx +11 -7
- package/docs/debugging.mdx +6 -3
- package/docs/executors.mdx +4 -2
- package/docs/github.mdx +12 -4
- package/docs/{writing-tests.mdx → goals.mdx} +21 -101
- package/docs/index.mdx +4 -4
- package/docs/integrations/kernel.mdx +1 -1
- package/docs/locators.mdx +64 -0
- package/docs/migrate/cypress.mdx +2 -2
- package/docs/migrate/maestro.mdx +2 -2
- package/docs/migrate/playwright.mdx +19 -16
- package/docs/migrate/selenium.mdx +2 -2
- package/docs/mobile.mdx +21 -4
- package/docs/models.mdx +53 -7
- package/docs/physical-devices.mdx +76 -0
- package/docs/quickstart.mdx +4 -3
- package/docs/reference/app.mdx +1 -1
- package/docs/reference/cli.mdx +30 -13
- package/docs/reference/config.mdx +25 -7
- package/docs/reference/engine.mdx +3 -3
- package/docs/reference/environment.mdx +8 -1
- package/docs/reference/errors.mdx +5 -5
- package/docs/reference/expect.mdx +34 -12
- package/docs/reference/mobile.mdx +23 -13
- package/docs/reference/reporters.mdx +8 -2
- package/docs/reference/screen.mdx +30 -7
- package/docs/reference/test.mdx +11 -4
- package/docs/reference/web.mdx +125 -21
- package/docs/security.mdx +26 -16
- package/docs/subscriptions.mdx +91 -12
- package/docs/telemetry.mdx +1 -1
- package/package.json +16 -3
- package/schema/report-v1.schema.json +14 -1
- package/skills/e2e/SKILL.md +2 -2
- package/skills/e2e/references/agent.md +22 -11
- package/skills/e2e/references/debugging.md +4 -4
- package/skills/e2e/references/running.md +13 -8
- package/skills/e2e/references/setup.md +36 -15
- package/skills/e2e/references/writing-tests.md +34 -17
package/dist/run/last-run.js
CHANGED
|
@@ -8,7 +8,16 @@ const UNDELIVERED_SKIPS = new Set([
|
|
|
8
8
|
'serial-predecessor-failed',
|
|
9
9
|
'hook-failed',
|
|
10
10
|
'infrastructure-unavailable',
|
|
11
|
+
'failure-limit',
|
|
11
12
|
]);
|
|
13
|
+
/** The id `--last-failed` names a result by: its test, target, and agent, whichever its repeat. */
|
|
14
|
+
function rerunId(result) {
|
|
15
|
+
return resultId(result.testId, result.targetId, result.agent);
|
|
16
|
+
}
|
|
17
|
+
/** Whether a run error is a failed `beforeAll` or `afterAll`, the failures `--last-failed` runs the scope of again. */
|
|
18
|
+
function isSuiteHookFailure(error) {
|
|
19
|
+
return error.phase === 'beforeAll' || error.phase === 'afterAll';
|
|
20
|
+
}
|
|
12
21
|
/** Whether a result is one `--last-failed` runs again: it did not pass, or the run never got to it. */
|
|
13
22
|
function didNotPass(result) {
|
|
14
23
|
switch (result.status) {
|
|
@@ -72,19 +81,88 @@ export async function readLastRun(reportPath) {
|
|
|
72
81
|
}
|
|
73
82
|
/**
|
|
74
83
|
* The result ids of a report's tests that did not pass: failed, timed out,
|
|
75
|
-
* interrupted, or skipped because a setup, a serial predecessor, a hook,
|
|
76
|
-
* the
|
|
77
|
-
* `afterAll` that failed, until a run where that hook
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
84
|
+
* interrupted, or skipped because a setup, a serial predecessor, a hook, the
|
|
85
|
+
* worker, or the failure limit stopped it, every selected test in the scope
|
|
86
|
+
* of a `beforeAll` or `afterAll` that failed, until a run where that hook
|
|
87
|
+
* passes, and every test the report carries from an earlier run that it did
|
|
88
|
+
* not run again. A report older than `selected` counts every result as
|
|
89
|
+
* selected. An explicit skip and a filtered test passed in the sense that
|
|
90
|
+
* matters here: nothing to run again. The ids are
|
|
91
|
+
* `resultId(testId, target, agent)`, so a test is named per target and agent
|
|
92
|
+
* whichever of its `--repeat-each` runs did not pass.
|
|
83
93
|
*/
|
|
84
94
|
export function lastFailedIds(document) {
|
|
85
|
-
const hookFailures = document.run.errors.filter(
|
|
95
|
+
const hookFailures = document.run.errors.filter(isSuiteHookFailure);
|
|
86
96
|
const rerun = (result) => didNotPass(result) || (result.selected !== false && hookFailures.some((error) => inHookScope(result, error)));
|
|
87
|
-
|
|
97
|
+
const ids = new Set(document.run.results.filter(rerun).map(rerunId));
|
|
98
|
+
for (const result of document.run.carried?.results ?? [])
|
|
99
|
+
ids.add(rerunId(result));
|
|
100
|
+
return ids;
|
|
101
|
+
}
|
|
102
|
+
/** Whether two suite hook failures are the same hook's: one phase, one scope. */
|
|
103
|
+
function sameHook(a, b) {
|
|
104
|
+
if (a.phase !== b.phase)
|
|
105
|
+
return false;
|
|
106
|
+
if (a.scope === undefined || b.scope === undefined)
|
|
107
|
+
return a.scope === b.scope;
|
|
108
|
+
return (a.scope.file === b.scope.file &&
|
|
109
|
+
a.scope.targetId === b.scope.targetId &&
|
|
110
|
+
a.scope.titlePath.length === b.scope.titlePath.length &&
|
|
111
|
+
a.scope.titlePath.every((title, index) => b.scope.titlePath[index] === title));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* What a `--last-failed` rerun carries forward from the report it selected
|
|
115
|
+
* from: every result that report owed a rerun and this run did not run, as
|
|
116
|
+
* the run that last ran it reported it; the serial groups those results'
|
|
117
|
+
* attempts live on; and the suite hook failures whose scope still holds one
|
|
118
|
+
* of them. A result is owed when it did not pass, or when it is in the scope
|
|
119
|
+
* of a failed `beforeAll` or `afterAll` this run did not see pass: a hook
|
|
120
|
+
* whose scope this run ran again without it failing is resolved, and one
|
|
121
|
+
* that failed again is this run's own error, which still makes its left-out
|
|
122
|
+
* tests owed. A test this run did not run is one it collected (`collected`,
|
|
123
|
+
* by test id), on a target the config still has, that it did not select:
|
|
124
|
+
* left out by a filter, on a target or agent the run did not ask for, or a
|
|
125
|
+
* setup no selected test needed, which the report lists no row for. A test in
|
|
126
|
+
* a file the run found but could not collect (`uncollectedFiles`, a narrowed
|
|
127
|
+
* run past a file that fails to import) is still there and stays carried. A
|
|
128
|
+
* test the run no longer collects cannot run again and is dropped. A test
|
|
129
|
+
* this run selected is no longer carried, whatever its outcome: its own
|
|
130
|
+
* result says it now. Undefined when nothing is carried.
|
|
131
|
+
*/
|
|
132
|
+
export function carryForward(lastRun, current, collected) {
|
|
133
|
+
const selected = new Set(current.run.results.filter((result) => result.selected !== false).map(rerunId));
|
|
134
|
+
const targets = new Set(current.run.targets.map((target) => target.id));
|
|
135
|
+
const stillThere = (result) => collected.testIds.has(result.testId) || collected.uncollectedFiles.has(result.file);
|
|
136
|
+
const notRun = (result) => !selected.has(rerunId(result)) && stillThere(result) && targets.has(result.targetId);
|
|
137
|
+
const failedAgain = current.run.errors.filter(isSuiteHookFailure);
|
|
138
|
+
const hooks = [...lastRun.run.errors.filter(isSuiteHookFailure), ...(lastRun.run.carried?.errors ?? [])];
|
|
139
|
+
const reportedAgain = (error) => failedAgain.some((again) => sameHook(again, error));
|
|
140
|
+
const passedAgain = (error) => !reportedAgain(error) &&
|
|
141
|
+
current.run.results.some((result) => result.selected !== false && result.status !== 'skipped' && inHookScope(result, error));
|
|
142
|
+
const unresolved = hooks.filter((error) => !passedAgain(error));
|
|
143
|
+
// A test the run before left out is known by the row it carried, not by its filtered one.
|
|
144
|
+
const history = [...lastRun.run.results.filter((result) => result.selected !== false), ...(lastRun.run.carried?.results ?? [])];
|
|
145
|
+
const results = history.filter((result) => notRun(result) && (didNotPass(result) || unresolved.some((error) => inHookScope(result, error))));
|
|
146
|
+
if (results.length === 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
const groupIds = new Set(results.flatMap((result) => (result.serialGroupId === undefined ? [] : [result.serialGroupId])));
|
|
149
|
+
const currentGroupIds = new Set(current.run.serialGroups.map((group) => group.id));
|
|
150
|
+
const serialGroups = [...lastRun.run.serialGroups, ...(lastRun.run.carried?.serialGroups ?? [])].filter((group) => groupIds.has(group.id) && !currentGroupIds.has(group.id));
|
|
151
|
+
const errors = unresolved.filter((error) => !reportedAgain(error) && results.some((result) => inHookScope(result, error)));
|
|
152
|
+
return { results, serialGroups, errors };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The artifact paths a report names, relative to the artifacts root: every
|
|
156
|
+
* attempt's of its results and serial groups, and of what it carries. What a
|
|
157
|
+
* `--last-failed` rerun keeps of the tree when it starts.
|
|
158
|
+
*/
|
|
159
|
+
export function reportArtifactPaths(document) {
|
|
160
|
+
const parts = [document.run, ...(document.run.carried === undefined ? [] : [document.run.carried])];
|
|
161
|
+
const attempts = parts.flatMap((part) => [
|
|
162
|
+
...part.results.flatMap((result) => result.attempts),
|
|
163
|
+
...part.serialGroups.flatMap((group) => group.attempts),
|
|
164
|
+
]);
|
|
165
|
+
return new Set(attempts.flatMap((attempt) => attempt.artifacts.flatMap((artifact) => (artifact.path === undefined ? [] : [artifact.path]))));
|
|
88
166
|
}
|
|
89
167
|
/**
|
|
90
168
|
* The shape the read relies on: a report-1 envelope whose every result has
|
|
@@ -96,10 +174,38 @@ function isReport(value) {
|
|
|
96
174
|
return false;
|
|
97
175
|
const document = value;
|
|
98
176
|
return (document.schemaVersion === 'report-1' &&
|
|
99
|
-
|
|
100
|
-
document.run.
|
|
101
|
-
|
|
102
|
-
|
|
177
|
+
isRunPart(document.run) &&
|
|
178
|
+
(document.run.carried === undefined || isRunPart(document.run.carried)));
|
|
179
|
+
}
|
|
180
|
+
/** A run, or what one carries: results and errors of the shape read here, and its serial groups. */
|
|
181
|
+
function isRunPart(value) {
|
|
182
|
+
if (typeof value !== 'object' || value === null)
|
|
183
|
+
return false;
|
|
184
|
+
const part = value;
|
|
185
|
+
return (Array.isArray(part.results) &&
|
|
186
|
+
part.results.every(isResult) &&
|
|
187
|
+
Array.isArray(part.errors) &&
|
|
188
|
+
part.errors.every(isError) &&
|
|
189
|
+
Array.isArray(part.serialGroups) &&
|
|
190
|
+
part.serialGroups.every((group) => typeof group === 'object' && group !== null && hasAttempts(group)));
|
|
191
|
+
}
|
|
192
|
+
/** Whether a result or serial group lists its attempts, each with its artifacts, the way a rerun reads them. */
|
|
193
|
+
function hasAttempts(value) {
|
|
194
|
+
const { attempts } = value;
|
|
195
|
+
return Array.isArray(attempts) && attempts.every(hasArtifacts);
|
|
196
|
+
}
|
|
197
|
+
/** Whether an attempt lists its artifacts as records whose path, when there is one, is a string. */
|
|
198
|
+
function hasArtifacts(attempt) {
|
|
199
|
+
if (typeof attempt !== 'object' || attempt === null)
|
|
200
|
+
return false;
|
|
201
|
+
const { artifacts } = attempt;
|
|
202
|
+
return (Array.isArray(artifacts) &&
|
|
203
|
+
artifacts.every((artifact) => {
|
|
204
|
+
if (typeof artifact !== 'object' || artifact === null)
|
|
205
|
+
return false;
|
|
206
|
+
const { path } = artifact;
|
|
207
|
+
return path === undefined || typeof path === 'string';
|
|
208
|
+
}));
|
|
103
209
|
}
|
|
104
210
|
function isResult(value) {
|
|
105
211
|
if (typeof value !== 'object' || value === null)
|
|
@@ -110,6 +216,7 @@ function isResult(value) {
|
|
|
110
216
|
typeof result.status === 'string' &&
|
|
111
217
|
typeof result.file === 'string' &&
|
|
112
218
|
Array.isArray(result.titlePath) &&
|
|
219
|
+
hasAttempts(result) &&
|
|
113
220
|
(skip === undefined || (typeof skip === 'object' && skip !== null && typeof skip.cause === 'string')));
|
|
114
221
|
}
|
|
115
222
|
function isError(value) {
|
package/dist/run/realm.d.ts
CHANGED
|
@@ -33,7 +33,10 @@ export declare function findRegistered(realm: Realm, test: CollectedTest): Regis
|
|
|
33
33
|
/**
|
|
34
34
|
* Runs one hook, or a fixture teardown, within its budget. One that overruns
|
|
35
35
|
* fails with the runner's timeout error naming `label`; `onTimeout` lets the
|
|
36
|
-
* caller cancel what it started.
|
|
36
|
+
* caller cancel what it started. The hook owns the `expect.poll` calls it
|
|
37
|
+
* starts: one still running when it returns fails it with
|
|
38
|
+
* `STEP_NOT_AWAITED` and is cancelled, and one left by a hook that failed is
|
|
39
|
+
* cancelled beside that failure.
|
|
37
40
|
*/
|
|
38
41
|
export declare function runHook(label: string, run: () => void | Promise<void>, timeoutMs: number, onTimeout?: () => void): Promise<void>;
|
|
39
42
|
export interface RealmManagerOptions {
|
package/dist/run/realm.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Suite realm lifecycle: module re-import, beforeAll/afterAll scope tracking. */
|
|
2
2
|
import { importModule } from '../config/load.js';
|
|
3
|
+
import { PollScope, runInPollScope } from '../expect/poll-scope.js';
|
|
3
4
|
import { classifyError, E2EError, serializeError, TestTimeoutError, } from '../internal/errors.js';
|
|
4
5
|
import { DebugTrace } from '../internal/debug.js';
|
|
5
6
|
import { titlePathKey } from '../internal/ids.js';
|
|
@@ -12,13 +13,26 @@ export function findRegistered(realm, test) {
|
|
|
12
13
|
/**
|
|
13
14
|
* Runs one hook, or a fixture teardown, within its budget. One that overruns
|
|
14
15
|
* fails with the runner's timeout error naming `label`; `onTimeout` lets the
|
|
15
|
-
* caller cancel what it started.
|
|
16
|
+
* caller cancel what it started. The hook owns the `expect.poll` calls it
|
|
17
|
+
* starts: one still running when it returns fails it with
|
|
18
|
+
* `STEP_NOT_AWAITED` and is cancelled, and one left by a hook that failed is
|
|
19
|
+
* cancelled beside that failure.
|
|
16
20
|
*/
|
|
17
|
-
export function runHook(label, run, timeoutMs, onTimeout) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
export async function runHook(label, run, timeoutMs, onTimeout) {
|
|
22
|
+
const polls = new PollScope(`the ${label}`);
|
|
23
|
+
try {
|
|
24
|
+
await withTimeout(runInPollScope(polls, () => Promise.resolve().then(run)), timeoutMs, () => {
|
|
25
|
+
onTimeout?.();
|
|
26
|
+
return new TestTimeoutError(`${label} timed out`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (cause) {
|
|
30
|
+
polls.close();
|
|
31
|
+
throw cause;
|
|
32
|
+
}
|
|
33
|
+
const notAwaited = polls.close();
|
|
34
|
+
if (notAwaited !== undefined)
|
|
35
|
+
throw notAwaited;
|
|
22
36
|
}
|
|
23
37
|
/** Creates realms and runs suite-scope hooks with per-realm entry tracking. */
|
|
24
38
|
export class RealmManager {
|
package/dist/run/records.d.ts
CHANGED
|
@@ -148,6 +148,22 @@ export interface ResultRecord {
|
|
|
148
148
|
* every other field is plain data.
|
|
149
149
|
*/
|
|
150
150
|
export type WireResultRecord = Omit<ResultRecord, 'target'>;
|
|
151
|
+
/** The failure a runtime skip followed, with the evidence its attempt captured. */
|
|
152
|
+
export interface FailureBeforeSkip {
|
|
153
|
+
readonly error: SerializedError;
|
|
154
|
+
readonly failure: FailureEvidence | undefined;
|
|
155
|
+
/** The failing attempt's artifacts; for a serial member, the group attempt's. */
|
|
156
|
+
readonly artifacts: readonly ArtifactRecord[];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The failure a test's `test.skip(...)` followed: the last attempt that
|
|
160
|
+
* failed before a skipped retry, or a soft failure the skipping attempt kept.
|
|
161
|
+
* Engine cleanup diagnostics after the skip are not one. A serial member's
|
|
162
|
+
* attempts live on its group, which `groupOf` looks up only for such a member.
|
|
163
|
+
*/
|
|
164
|
+
export declare function failureBeforeSkip(result: Pick<ResultRecord, 'status' | 'skip' | 'attempts' | 'serialGroupId' | 'test'>, groupOf: (id: string) => SerialGroupRecord | undefined): FailureBeforeSkip | undefined;
|
|
165
|
+
/** Whether any skipped result followed a failure: what `failOnSkippedFailure` fails a run for. */
|
|
166
|
+
export declare function someSkippedAfterFailure(results: readonly ResultRecord[], serialGroups: readonly SerialGroupRecord[]): boolean;
|
|
151
167
|
/** Strips the live target from a result for transport. */
|
|
152
168
|
export declare function encodeResult(record: ResultRecord): WireResultRecord;
|
|
153
169
|
export interface RunError {
|
package/dist/run/records.js
CHANGED
|
@@ -7,6 +7,32 @@
|
|
|
7
7
|
export function isFailedStatus(status) {
|
|
8
8
|
return status !== 'passed' && status !== 'skipped' && status !== 'flaky';
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* The failure a test's `test.skip(...)` followed: the last attempt that
|
|
12
|
+
* failed before a skipped retry, or a soft failure the skipping attempt kept.
|
|
13
|
+
* Engine cleanup diagnostics after the skip are not one. A serial member's
|
|
14
|
+
* attempts live on its group, which `groupOf` looks up only for such a member.
|
|
15
|
+
*/
|
|
16
|
+
export function failureBeforeSkip(result, groupOf) {
|
|
17
|
+
if (result.status !== 'skipped' || result.skip?.cause !== 'explicit')
|
|
18
|
+
return undefined;
|
|
19
|
+
const runs = result.serialGroupId === undefined
|
|
20
|
+
? result.attempts
|
|
21
|
+
: (groupOf(result.serialGroupId)?.attempts ?? []).flatMap((attempt) => attempt.members
|
|
22
|
+
.filter((member) => member.testId === result.test.id)
|
|
23
|
+
.map((member) => ({ ...member, artifacts: attempt.artifacts })));
|
|
24
|
+
for (const run of runs.toReversed()) {
|
|
25
|
+
const error = run.error ?? run.secondaryErrors.find((secondary) => secondary.phase !== 'cleanup');
|
|
26
|
+
if (error !== undefined)
|
|
27
|
+
return { error, failure: run.failure, artifacts: run.artifacts };
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Whether any skipped result followed a failure: what `failOnSkippedFailure` fails a run for. */
|
|
32
|
+
export function someSkippedAfterFailure(results, serialGroups) {
|
|
33
|
+
const groups = new Map(serialGroups.map((group) => [group.id, group]));
|
|
34
|
+
return results.some((result) => failureBeforeSkip(result, (id) => groups.get(id)) !== undefined);
|
|
35
|
+
}
|
|
10
36
|
/** Strips the live target from a result for transport. */
|
|
11
37
|
export function encodeResult(record) {
|
|
12
38
|
const { target: _target, ...rest } = record;
|
package/dist/run/retry.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export declare function isRetryEligible(attempt: RetryAttempt): boolean;
|
|
|
12
12
|
* Runs up to `maxAttempts` attempts, stopping on pass, interrupt, or a
|
|
13
13
|
* failure that does not consume retry budget. `runOnce` returns the
|
|
14
14
|
* finished attempt, or undefined when the attempt could not start (the
|
|
15
|
-
* loop stops and the last observed status stands).
|
|
15
|
+
* loop stops and the last observed status stands). An interrupt during a
|
|
16
|
+
* retry keeps the verdict the failed attempt before it reached: the test
|
|
17
|
+
* did fail, and only its second chance was cut short.
|
|
16
18
|
*/
|
|
17
19
|
export declare function runWithRetries(maxAttempts: number, interruptSignal: AbortSignal, runOnce: (attemptIndex: number) => Promise<RetryAttempt | undefined>): Promise<ResultStatus>;
|
package/dist/run/retry.js
CHANGED
|
@@ -8,7 +8,9 @@ export function isRetryEligible(attempt) {
|
|
|
8
8
|
* Runs up to `maxAttempts` attempts, stopping on pass, interrupt, or a
|
|
9
9
|
* failure that does not consume retry budget. `runOnce` returns the
|
|
10
10
|
* finished attempt, or undefined when the attempt could not start (the
|
|
11
|
-
* loop stops and the last observed status stands).
|
|
11
|
+
* loop stops and the last observed status stands). An interrupt during a
|
|
12
|
+
* retry keeps the verdict the failed attempt before it reached: the test
|
|
13
|
+
* did fail, and only its second chance was cut short.
|
|
12
14
|
*/
|
|
13
15
|
export async function runWithRetries(maxAttempts, interruptSignal, runOnce) {
|
|
14
16
|
let finalStatus = 'failed';
|
|
@@ -23,7 +25,7 @@ export async function runWithRetries(maxAttempts, interruptSignal, runOnce) {
|
|
|
23
25
|
if (attempt.status === 'passed')
|
|
24
26
|
return attemptCount > 1 ? 'flaky' : 'passed';
|
|
25
27
|
if (attempt.status === 'interrupted')
|
|
26
|
-
return 'interrupted';
|
|
28
|
+
return attemptCount > 1 ? finalStatus : 'interrupted';
|
|
27
29
|
// A body that skipped itself has decided; a retry would only ask again.
|
|
28
30
|
if (attempt.status === 'skipped')
|
|
29
31
|
return 'skipped';
|
package/dist/run/runner.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type Shard, type TagMode } from '../collect/select.ts';
|
|
|
5
5
|
import type { ExploreProgress } from '../explore/progress.ts';
|
|
6
6
|
import { type Report1Document, type ReportExplore } from '../report/build.ts';
|
|
7
7
|
import { type RunEventSink, type RunExitCode, type RunStatus } from './events.ts';
|
|
8
|
-
import type
|
|
8
|
+
import { type ResultRecord } from './records.ts';
|
|
9
9
|
import type { BuiltinReporter, E2EConfig, RecordingMode } from '../types.ts';
|
|
10
10
|
export interface RunOptions {
|
|
11
11
|
cwd?: string | undefined;
|
package/dist/run/runner.js
CHANGED
|
@@ -18,11 +18,13 @@ import { writeJsonReport } from '../report/write.js';
|
|
|
18
18
|
import { createRunEventEmitter, toEventResult } from './events.js';
|
|
19
19
|
import { allocateAppPorts } from './app-ports.js';
|
|
20
20
|
import { inProcessSpawner } from './in-process.js';
|
|
21
|
+
import { someSkippedAfterFailure } from './records.js';
|
|
21
22
|
import { runUnits } from './scheduler.js';
|
|
22
23
|
import { buildWorkPlans, plannedSlots } from './units.js';
|
|
23
24
|
import { SessionStore } from './sessions.js';
|
|
24
25
|
import { outputLayout } from './output.js';
|
|
25
|
-
import {
|
|
26
|
+
import { claimRerunDir, pruneArtifacts } from './artifacts.js';
|
|
27
|
+
import { carryForward, lastFailedIds, readLastRun, reportArtifactPaths } from './last-run.js';
|
|
26
28
|
import { childProcessSpawner } from './worker/handle.js';
|
|
27
29
|
import { setSecretRegistry } from '../secrets.js';
|
|
28
30
|
import { withAbort } from '../internal/time.js';
|
|
@@ -187,25 +189,38 @@ export async function run(options = {}) {
|
|
|
187
189
|
*/
|
|
188
190
|
const currentExitCode = () => combineExitCodes([
|
|
189
191
|
...resultExitCodes(results),
|
|
192
|
+
...(loaded.config?.failOnSkippedFailure === true && someSkippedAfterFailure(results, serialGroups) ? [1] : []),
|
|
190
193
|
...runErrors.map((runError) => exitCodeForCategory(runError.error.category)),
|
|
191
194
|
...(interruptController.signal.aborted ? [130] : []),
|
|
192
195
|
].filter((code) => code !== 130 || !(runAborted || stoppedEarly)));
|
|
193
196
|
// Detected once the config names the project root; a report written before
|
|
194
197
|
// that (a config failure) has no checkout to describe.
|
|
195
198
|
let vcs;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
199
|
+
// The report `--last-failed` selected from, once collection has read it;
|
|
200
|
+
// reporters get it beside this run's report to fold the rerun into it, and
|
|
201
|
+
// this run's report carries what it owed that this run left out.
|
|
202
|
+
let lastRun;
|
|
203
|
+
// The ids of every test collection found, a setup no selected test needed
|
|
204
|
+
// among them, and the files it could not collect: what a rerun can still
|
|
205
|
+
// carry when the report lists no row.
|
|
206
|
+
let rerunCollection = { testIds: new Set(), uncollectedFiles: new Set() };
|
|
207
|
+
const buildRunReport = (exitCode) => {
|
|
208
|
+
const document = buildReport({
|
|
209
|
+
runId,
|
|
210
|
+
config: loaded.config,
|
|
211
|
+
vcs,
|
|
212
|
+
startedAt,
|
|
213
|
+
status: statusOf(exitCode),
|
|
214
|
+
exitCode,
|
|
215
|
+
results,
|
|
216
|
+
serialGroups,
|
|
217
|
+
runErrors,
|
|
218
|
+
targetProvenance,
|
|
219
|
+
explore: options.tests?.explore?.snapshot(),
|
|
220
|
+
});
|
|
221
|
+
const carried = lastRun === undefined ? undefined : carryForward(lastRun, document, rerunCollection);
|
|
222
|
+
return carried === undefined ? document : { ...document, run: { ...document.run, carried } };
|
|
223
|
+
};
|
|
209
224
|
/**
|
|
210
225
|
* Writes the canonical report and returns its path only once the file
|
|
211
226
|
* exists: a host must never be handed a path to a report that was not
|
|
@@ -249,9 +264,6 @@ export async function run(options = {}) {
|
|
|
249
264
|
return undefined;
|
|
250
265
|
}
|
|
251
266
|
};
|
|
252
|
-
// The report `--last-failed` selected from, once collection has read it;
|
|
253
|
-
// reporters get it beside this run's report to fold the rerun into it.
|
|
254
|
-
let lastRun;
|
|
255
267
|
/**
|
|
256
268
|
* Whether the run got as far as its tests. Only such a run writes into the
|
|
257
269
|
* output directory: one that stopped before leaves the previous run's
|
|
@@ -364,6 +376,10 @@ export async function run(options = {}) {
|
|
|
364
376
|
: collectInMemory(config.projectRoot, options.tests.file, options.tests.registration, consumesSession(options.tests.registration) ? await collectSetups(config) : undefined);
|
|
365
377
|
const inputs = await selectionInputs(options, config);
|
|
366
378
|
lastRun = inputs.lastRun;
|
|
379
|
+
rerunCollection = {
|
|
380
|
+
testIds: new Set(collection.tests.map((test) => test.id)),
|
|
381
|
+
uncollectedFiles: new Set(collection.uncollected.map((skipped) => skipped.file)),
|
|
382
|
+
};
|
|
367
383
|
const selection = repeatEach(select(collection, config, inputs.filters, options.passWithNoTests !== undefined ? { passWithNoTests: options.passWithNoTests } : {}), options.repeatEach ?? 1);
|
|
368
384
|
return { collection, selection };
|
|
369
385
|
}));
|
|
@@ -450,15 +466,26 @@ export async function run(options = {}) {
|
|
|
450
466
|
// Tests are about to start, and only now is the previous run's evidence
|
|
451
467
|
// given up: the artifact tree is emptied, so what is there once this run
|
|
452
468
|
// ends is its own and nothing a report no longer names, and this run's
|
|
453
|
-
// report replaces the last. A
|
|
454
|
-
//
|
|
455
|
-
//
|
|
469
|
+
// report replaces the last. A `--last-failed` rerun keeps the evidence
|
|
470
|
+
// the report it reruns names instead, since that report's results fold
|
|
471
|
+
// into the rerun's and its carried tests do not run again, and files its
|
|
472
|
+
// own attempts in a fresh `rerun-<n>` directory beside it, so no attempt
|
|
473
|
+
// overwrites an earlier one's files. A run that stopped before here (no
|
|
474
|
+
// test selected, a collection error, a target that cannot record what it
|
|
475
|
+
// asks, an app that failed to start, an interrupt) leaves both, and
|
|
456
476
|
// `--last-failed` still reads the run that executed. A wipe that fails
|
|
457
477
|
// part way has already given up the old evidence, so this run's report
|
|
458
478
|
// records the failure.
|
|
459
479
|
testsStarted = true;
|
|
480
|
+
let rerunDir;
|
|
460
481
|
try {
|
|
461
|
-
|
|
482
|
+
if (lastRun === undefined) {
|
|
483
|
+
await rm(layout.artifacts, { recursive: true, force: true });
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
await pruneArtifacts(layout.artifacts, reportArtifactPaths(lastRun));
|
|
487
|
+
rerunDir = await claimRerunDir(layout.artifacts);
|
|
488
|
+
}
|
|
462
489
|
}
|
|
463
490
|
catch (cause) {
|
|
464
491
|
recordFailure(cause, 'launch');
|
|
@@ -482,6 +509,7 @@ export async function run(options = {}) {
|
|
|
482
509
|
inMemory: options.tests,
|
|
483
510
|
runId,
|
|
484
511
|
artifactsRoot,
|
|
512
|
+
rerunDir,
|
|
485
513
|
sessionStore: store,
|
|
486
514
|
headed: options.headed ?? false,
|
|
487
515
|
debug,
|
|
@@ -495,6 +523,7 @@ export async function run(options = {}) {
|
|
|
495
523
|
ports: config.ports,
|
|
496
524
|
runId,
|
|
497
525
|
artifactsRoot,
|
|
526
|
+
rerunDir,
|
|
498
527
|
headed: options.headed ?? false,
|
|
499
528
|
sessionsRoot: layout.sessions,
|
|
500
529
|
sessionKeyBase64: store.exportKeyForWorker(),
|
|
@@ -747,13 +776,21 @@ function resultExitCodes(results) {
|
|
|
747
776
|
/** Why a reporter's `onRunFinished` was given up on: the message is the stderr line. */
|
|
748
777
|
class ReporterAbandoned extends Error {
|
|
749
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* The built-in reporters that write files beside `report.json`. A forced
|
|
781
|
+
* interrupt never abandons them: they are local writes, and giving one up
|
|
782
|
+
* would leave the previous run's `summary.md` or `junit.xml` beside this
|
|
783
|
+
* run's report, where CI reads it as this run's.
|
|
784
|
+
*/
|
|
785
|
+
const FILE_REPORTERS = new Set([STATELESS_REPORTERS.junit, STATELESS_REPORTERS.markdown]);
|
|
750
786
|
/**
|
|
751
787
|
* Awaits every reporter's `onRunFinished` at once, each within `timeoutMs`
|
|
752
|
-
* and until a forced interrupt, and
|
|
753
|
-
* with. Each reporter gets a signal
|
|
754
|
-
*
|
|
755
|
-
* reporter that throws, runs out of
|
|
756
|
-
* rows is one line on stderr; it can
|
|
788
|
+
* and, except the built-in file reporters, until a forced interrupt, and
|
|
789
|
+
* collects the summary rows they resolve with. Each reporter gets a signal
|
|
790
|
+
* that aborts on either, so a well-behaved one cancels its own work and
|
|
791
|
+
* leaves no handle holding the process. A reporter that throws, runs out of
|
|
792
|
+
* time, or returns something other than rows is one line on stderr; it can
|
|
793
|
+
* never change the run's outcome.
|
|
757
794
|
*/
|
|
758
795
|
async function runReporters(reporters, finished, timeoutMs, force) {
|
|
759
796
|
const warn = (reporter, detail) => {
|
|
@@ -767,10 +804,12 @@ async function runReporters(reporters, finished, timeoutMs, force) {
|
|
|
767
804
|
const abandon = new AbortController();
|
|
768
805
|
const timer = setTimeout(() => abandon.abort(new ReporterAbandoned(`did not finish within ${budget}`)), timeoutMs);
|
|
769
806
|
const onForce = () => abandon.abort(new ReporterAbandoned('abandoned: the run was forced to stop'));
|
|
770
|
-
if (
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
807
|
+
if (!FILE_REPORTERS.has(reporter)) {
|
|
808
|
+
if (force.aborted)
|
|
809
|
+
onForce();
|
|
810
|
+
else
|
|
811
|
+
force.addEventListener('abort', onForce, { once: true });
|
|
812
|
+
}
|
|
774
813
|
try {
|
|
775
814
|
const result = await withAbort(() => Promise.resolve(onRunFinished.call(reporter, finished, abandon.signal)), abandon.signal, () => abandon.signal.reason);
|
|
776
815
|
if (result === undefined)
|
package/dist/run/serial.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface SharedSerialSession {
|
|
|
37
37
|
export interface SerialHost {
|
|
38
38
|
readonly target: ResolvedTarget;
|
|
39
39
|
readonly artifactsRoot: string;
|
|
40
|
+
/** See `TargetExecutorOptions.rerunDir`. */
|
|
41
|
+
readonly rerunDir: string | undefined;
|
|
40
42
|
readonly runId: string;
|
|
41
43
|
/** The configured artifact store, so group-owned artifacts (the shared trace) upload like any other. */
|
|
42
44
|
readonly artifactStore: ArtifactStore | undefined;
|
package/dist/run/serial.js
CHANGED
|
@@ -3,7 +3,7 @@ import { classifyError, ConfigurationError, serializeError, } from '../internal/
|
|
|
3
3
|
import { canonicalDigest, timestamp, uuidv7 } from '../internal/ids.js';
|
|
4
4
|
import { groupTitles } from '../collect/registry.js';
|
|
5
5
|
import { pairRecordings } from '../collect/select.js';
|
|
6
|
-
import { createAttemptArtifacts, sanitizePathSegment } from './artifacts.js';
|
|
6
|
+
import { attemptSegments, createAttemptArtifacts, sanitizePathSegment } from './artifacts.js';
|
|
7
7
|
import { findRegistered, RealmManager } from './realm.js';
|
|
8
8
|
import { isFailedStatus } from './records.js';
|
|
9
9
|
import { runWithRetries } from './retry.js';
|
|
@@ -35,8 +35,11 @@ export async function runSerialUnit(host, members, file) {
|
|
|
35
35
|
const finalStatus = await runWithRetries(first.options.retries + 1, host.interruptSignal, async (attemptIndex) => {
|
|
36
36
|
const attempt = await runSerialAttempt(host, members, file, attemptIndex);
|
|
37
37
|
group.attempts.push(attempt);
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// A retry the run interrupted keeps the verdict before it, so each member keeps what that attempt said.
|
|
39
|
+
const cutRetry = attemptIndex > 0 && attempt.status === 'interrupted';
|
|
40
|
+
if (!cutRetry)
|
|
41
|
+
for (const member of attempt.members)
|
|
42
|
+
memberFinalStatus.set(member.testId, member);
|
|
40
43
|
// A beforeAll failure is not retry-eligible: the
|
|
41
44
|
// attempt stands as recorded and the retry loop stops here.
|
|
42
45
|
if (attempt.error?.code === 'HOOK_FAILED')
|
|
@@ -88,13 +91,13 @@ async function runSerialAttempt(host, members, file, attemptIndex) {
|
|
|
88
91
|
const memberRecords = [];
|
|
89
92
|
const first = members[0];
|
|
90
93
|
const recordings = pairRecordings(first, attemptIndex);
|
|
91
|
-
const artifactSegments = [
|
|
94
|
+
const artifactSegments = attemptSegments(host.rerunDir, [
|
|
92
95
|
host.target.name,
|
|
93
96
|
sanitizePathSegment(first.test.serialId ?? first.test.id),
|
|
94
97
|
first.agent,
|
|
95
98
|
...repeatSegment(first.repeat),
|
|
96
99
|
`attempt-${attemptIndex}`,
|
|
97
|
-
];
|
|
100
|
+
]);
|
|
98
101
|
const artifacts = createAttemptArtifacts({
|
|
99
102
|
artifactsRoot: host.artifactsRoot,
|
|
100
103
|
segments: artifactSegments,
|
package/dist/run/steps.d.ts
CHANGED
|
@@ -271,12 +271,6 @@ export interface StepRecorderOptions {
|
|
|
271
271
|
/** Replaces secret values in a step's label, and in its error's message and details, before the record keeps them. */
|
|
272
272
|
readonly redact?: (text: string) => string;
|
|
273
273
|
}
|
|
274
|
-
/**
|
|
275
|
-
* Whether an unhandled rejection is an abandoned step's: already recorded as
|
|
276
|
-
* `STEP_NOT_AWAITED`, reaching the process only because the promise the
|
|
277
|
-
* test did not await rejected with it. Such a rejection is not a fault.
|
|
278
|
-
*/
|
|
279
|
-
export declare function isAbandonedStepRejection(cause: unknown): boolean;
|
|
280
274
|
export declare class StepRecorder {
|
|
281
275
|
private readonly attemptId;
|
|
282
276
|
private readonly attempt;
|
package/dist/run/steps.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Attempt-scoped step timeline. */
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
3
|
import { isAgentError } from '../agent/error.js';
|
|
4
|
+
import { markAbandonedRejection, relocateStack } from '../internal/abandoned.js';
|
|
4
5
|
import { withAiTraceStep } from '../internal/ai-trace.js';
|
|
5
6
|
import { classifyError, serializeError, TestError, withHint } from '../internal/errors.js';
|
|
6
7
|
import { timestamp } from '../internal/ids.js';
|
|
@@ -9,16 +10,6 @@ import { sourceLocation } from '../internal/source.js';
|
|
|
9
10
|
export const STEP_KINDS = ['agent', 'locator', 'assertion', 'screen', 'app', 'session', 'resource'];
|
|
10
11
|
/** Frames kept when a step captures where it was called from; the user's line is a few frames up. */
|
|
11
12
|
const STEP_STACK_FRAMES = 20;
|
|
12
|
-
/** What abandoned steps rejected with. The promise the test holds rejects with the same value, and nobody awaits it. */
|
|
13
|
-
const ABANDONED_REJECTIONS = new WeakSet();
|
|
14
|
-
/**
|
|
15
|
-
* Whether an unhandled rejection is an abandoned step's: already recorded as
|
|
16
|
-
* `STEP_NOT_AWAITED`, reaching the process only because the promise the
|
|
17
|
-
* test did not await rejected with it. Such a rejection is not a fault.
|
|
18
|
-
*/
|
|
19
|
-
export function isAbandonedStepRejection(cause) {
|
|
20
|
-
return typeof cause === 'object' && cause !== null && ABANDONED_REJECTIONS.has(cause);
|
|
21
|
-
}
|
|
22
13
|
/**
|
|
23
14
|
* The stack at a step's start, or nothing without a project root to read it
|
|
24
15
|
* against. Every step pays the capture, since the failing one is not known
|
|
@@ -38,13 +29,6 @@ function stepStack(projectRoot) {
|
|
|
38
29
|
Error.stackTraceLimit = limit;
|
|
39
30
|
}
|
|
40
31
|
}
|
|
41
|
-
/** Points `error` at the frames of `stack`, keeping its own name and message as the first line. */
|
|
42
|
-
function relocate(error, stack) {
|
|
43
|
-
if (stack === undefined)
|
|
44
|
-
return;
|
|
45
|
-
const frames = stack.split('\n').slice(1).join('\n');
|
|
46
|
-
error.stack = `${error.name}: ${error.message}\n${frames}`;
|
|
47
|
-
}
|
|
48
32
|
/** Characters of a step label an error message quotes before clipping it. */
|
|
49
33
|
const MAX_QUOTED_LABEL_CHARS = 80;
|
|
50
34
|
function quoted(label) {
|
|
@@ -137,8 +121,7 @@ export class StepRecorder {
|
|
|
137
121
|
}
|
|
138
122
|
catch (cause) {
|
|
139
123
|
if (this.abandoned.has(record.id)) {
|
|
140
|
-
|
|
141
|
-
ABANDONED_REJECTIONS.add(cause);
|
|
124
|
+
markAbandonedRejection(cause);
|
|
142
125
|
throw cause;
|
|
143
126
|
}
|
|
144
127
|
record.durationMs = Date.now() - startedMs;
|
|
@@ -177,7 +160,7 @@ export class StepRecorder {
|
|
|
177
160
|
for (const step of running) {
|
|
178
161
|
const more = first === undefined && running.length > 1 ? ` and ${running.length - 1} more` : '';
|
|
179
162
|
const error = new TestError('STEP_NOT_AWAITED', withHint(`the test body returned before ${step.api} ${quoted(step.label)}${more} finished`, 'put `await` in front of every step call'));
|
|
180
|
-
|
|
163
|
+
relocateStack(error, this.stacks.get(step.id));
|
|
181
164
|
first ??= error;
|
|
182
165
|
this.abandoned.add(step.id);
|
|
183
166
|
step.status = 'failed';
|