e2e 0.17.0 → 0.18.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/README.md +10 -0
- package/dist/.build.json +1 -1
- package/dist/agent/ai-sdk.js +5 -4
- package/dist/agent/error.js +3 -3
- package/dist/agent/invocation.js +3 -4
- package/dist/agent/model/tool-choice.d.ts +9 -0
- package/dist/agent/model/tool-choice.js +14 -0
- package/dist/agent/observation-feed.js +3 -4
- package/dist/agent/phases.d.ts +7 -1
- package/dist/agent/phases.js +12 -3
- package/dist/agent/prompts.js +3 -0
- package/dist/agent/replay.js +18 -15
- package/dist/agent/step-cache.d.ts +2 -1
- package/dist/agent/step-cache.js +9 -5
- package/dist/agent/tool-loop.js +92 -5
- package/dist/cache/trace.js +6 -2
- package/dist/cli/bin.js +2 -2
- package/dist/cli/index.js +35 -24
- package/dist/cli/init/mcp-config.d.ts +2 -1
- package/dist/cli/init/mcp-config.js +8 -3
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/cli/mcp.d.ts +3 -2
- package/dist/cli/mcp.js +52 -16
- package/dist/cli/run-output.d.ts +17 -0
- package/dist/cli/run-output.js +45 -0
- package/dist/collect/collect.js +8 -4
- package/dist/collect/registry.js +27 -16
- package/dist/config/compiled-files.d.ts +36 -0
- package/dist/config/compiled-files.js +50 -0
- package/dist/config/diagnose.js +21 -19
- package/dist/config/esm-hooks.d.ts +39 -44
- package/dist/config/esm-hooks.js +238 -63
- package/dist/config/load.d.ts +1 -7
- package/dist/config/load.js +23 -66
- package/dist/config/register.d.ts +8 -0
- package/dist/config/register.js +9 -0
- package/dist/config/specifiers.d.ts +19 -0
- package/dist/config/specifiers.js +33 -0
- package/dist/config/tsconfig.d.ts +56 -0
- package/dist/config/tsconfig.js +115 -0
- package/dist/config/typescript.d.ts +19 -0
- package/dist/config/typescript.js +253 -0
- package/dist/engine/contract.d.ts +1 -0
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/session.js +2 -2
- package/dist/expect/async.js +2 -1
- package/dist/expect/poll.js +2 -2
- package/dist/internal/errors.d.ts +10 -3
- package/dist/internal/errors.js +23 -8
- package/dist/internal/node-version.d.ts +16 -7
- package/dist/internal/node-version.js +50 -15
- package/dist/internal/package-manager.js +1 -1
- package/dist/internal/realm-slot.d.ts +5 -4
- package/dist/internal/redact.d.ts +2 -0
- package/dist/internal/redact.js +4 -0
- package/dist/internal/time.d.ts +21 -5
- package/dist/internal/time.js +35 -3
- package/dist/locator/engine.js +47 -11
- package/dist/locator/screen.js +31 -5
- package/dist/mcp/output.d.ts +59 -0
- package/dist/mcp/output.js +129 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +9 -12
- package/dist/mcp/session.d.ts +8 -1
- package/dist/mcp/session.js +55 -14
- package/dist/mcp/tools.js +7 -5
- package/dist/report/list.d.ts +11 -0
- package/dist/report/list.js +23 -6
- package/dist/run/artifacts.d.ts +4 -4
- package/dist/run/artifacts.js +3 -2
- package/dist/run/execute.d.ts +25 -5
- package/dist/run/execute.js +44 -20
- package/dist/run/failure-evidence.js +6 -2
- package/dist/run/in-process.js +51 -10
- package/dist/run/process-output.d.ts +73 -0
- package/dist/run/process-output.js +130 -0
- package/dist/run/provision.d.ts +2 -0
- package/dist/run/provision.js +1 -0
- package/dist/run/records.d.ts +4 -4
- package/dist/run/retry.d.ts +7 -0
- package/dist/run/retry.js +27 -11
- package/dist/run/runner.d.ts +8 -0
- package/dist/run/runner.js +32 -9
- package/dist/run/scheduler.js +255 -19
- package/dist/run/secrecy.d.ts +22 -19
- package/dist/run/secrecy.js +22 -18
- package/dist/run/serial.d.ts +73 -2
- package/dist/run/serial.js +239 -58
- package/dist/run/standalone.js +1 -1
- package/dist/run/trace-redaction.d.ts +5 -6
- package/dist/run/trace-redaction.js +5 -6
- package/dist/run/worker/entry.js +35 -14
- package/dist/run/worker/handle.js +10 -5
- package/dist/run/worker/protocol.d.ts +83 -3
- package/dist/run/worker/session.js +28 -0
- package/dist/telemetry/environment.d.ts +2 -1
- package/dist/telemetry/environment.js +12 -2
- package/dist/telemetry/project.d.ts +5 -6
- package/dist/telemetry/project.js +6 -11
- package/dist/telemetry/store.d.ts +5 -5
- package/dist/telemetry/store.js +5 -7
- package/dist/telemetry/telemetry.d.ts +2 -3
- package/dist/telemetry/telemetry.js +2 -4
- package/dist/types.d.ts +6 -3
- package/docs/agent-steps.mdx +1 -0
- package/docs/agents.mdx +1 -0
- package/docs/api-testing.mdx +2 -1
- package/docs/authentication.mdx +3 -2
- package/docs/browser.mdx +7 -6
- package/docs/bug-bash.mdx +2 -1
- package/docs/cache.mdx +2 -1
- package/docs/ci.mdx +3 -2
- package/docs/coding-agents.mdx +2 -1
- package/docs/core-concepts.mdx +208 -0
- package/docs/debugging.mdx +19 -9
- package/docs/decision-models.mdx +142 -0
- package/docs/example-projects.mdx +32 -0
- package/docs/executors.mdx +3 -0
- package/docs/explore.mdx +1 -0
- package/docs/github.mdx +2 -1
- package/docs/index.mdx +9 -5
- package/docs/integrations/eas.mdx +1 -0
- package/docs/integrations/index.mdx +1 -0
- package/docs/integrations/kernel.mdx +2 -1
- package/docs/migrate/cypress.mdx +26 -3
- package/docs/migrate/detox.mdx +24 -1
- package/docs/migrate/maestro.mdx +28 -4
- package/docs/migrate/playwright.mdx +27 -4
- package/docs/migrate/selenium.mdx +27 -3
- package/docs/mobile-ci.mdx +6 -5
- package/docs/mobile.mdx +79 -8
- package/docs/models.mdx +7 -2
- package/docs/quickstart.mdx +26 -15
- package/docs/reference/agent.mdx +13 -3
- package/docs/reference/app.mdx +1 -0
- package/docs/reference/cli.mdx +11 -5
- package/docs/reference/config.mdx +42 -6
- package/docs/reference/engine.mdx +22 -10
- package/docs/reference/environment.mdx +2 -1
- package/docs/reference/errors.mdx +11 -8
- package/docs/reference/expect.mdx +2 -1
- package/docs/reference/mcp.mdx +21 -8
- package/docs/reference/mobile.mdx +1 -0
- package/docs/reference/reporters.mdx +1 -0
- package/docs/reference/screen.mdx +19 -9
- package/docs/reference/test.mdx +21 -1
- package/docs/reference/web.mdx +8 -2
- package/docs/security.mdx +33 -27
- package/docs/subscriptions.mdx +1 -0
- package/docs/telemetry.mdx +8 -5
- package/docs/tools.mdx +1 -0
- package/docs/{starting-your-app.mdx → web.mdx} +157 -11
- package/docs/writing-an-engine.mdx +2 -1
- package/package.json +11 -7
- package/skills/e2e/SKILL.md +3 -1
- package/skills/e2e/references/agent.md +11 -0
- package/skills/e2e/references/debugging.md +1 -1
- package/skills/e2e/references/mcp.md +8 -3
- package/skills/e2e/references/running.md +2 -1
- package/skills/e2e/references/setup.md +9 -1
- package/skills/e2e/references/writing-tests.md +1 -1
- package/docs/assertions.mdx +0 -94
- package/docs/goals.mdx +0 -176
- package/docs/locators.mdx +0 -64
- package/docs/physical-devices.mdx +0 -76
- package/docs/viewports.mdx +0 -146
package/dist/run/in-process.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* `TargetWorker` core; only the transport and pair resolution differ, and the
|
|
6
6
|
* scheduler is capped at one worker so nothing overlaps in this process.
|
|
7
7
|
*/
|
|
8
|
+
import { isAbandonedRejection } from '../internal/abandoned.js';
|
|
8
9
|
import { classifyError, ConfigurationError, serializeError } from '../internal/errors.js';
|
|
9
10
|
import { pairKey } from './units.js';
|
|
10
11
|
import { TargetWorker } from './worker/session.js';
|
|
@@ -18,6 +19,22 @@ class InProcessRunner {
|
|
|
18
19
|
finish;
|
|
19
20
|
exited = false;
|
|
20
21
|
closing = false;
|
|
22
|
+
/**
|
|
23
|
+
* Set by a fatal error: the worker is dead to the scheduler from then on,
|
|
24
|
+
* as a crashed process would be, so nothing it reports about its unit
|
|
25
|
+
* while it winds down counts, and the unit's results are synthesized when
|
|
26
|
+
* it exits. Unlike a dead process it still disposes its engine, so the
|
|
27
|
+
* failures of that disposal (`shutdown-done`) are still reported.
|
|
28
|
+
*/
|
|
29
|
+
crashed = false;
|
|
30
|
+
/** Takes what this process failed to catch while the runner lives; see `catchStrays`. */
|
|
31
|
+
onRejection = (cause) => {
|
|
32
|
+
if (isAbandonedRejection(cause))
|
|
33
|
+
return;
|
|
34
|
+
if (!this.worker.strayRejection(cause))
|
|
35
|
+
this.fail(cause);
|
|
36
|
+
};
|
|
37
|
+
onException = (cause) => this.fail(cause);
|
|
21
38
|
constructor(targetName, workerSlot, events, options) {
|
|
22
39
|
this.targetName = targetName;
|
|
23
40
|
this.workerSlot = workerSlot;
|
|
@@ -30,20 +47,14 @@ class InProcessRunner {
|
|
|
30
47
|
this.finish = finish;
|
|
31
48
|
this.worker = new TargetWorker({
|
|
32
49
|
emit: (message) => {
|
|
33
|
-
if (
|
|
34
|
-
this.events.onMessage(message);
|
|
35
|
-
},
|
|
36
|
-
fatal: (cause) => {
|
|
37
|
-
if (this.exited)
|
|
50
|
+
if (this.exited || (this.crashed && message.type !== 'shutdown-done'))
|
|
38
51
|
return;
|
|
39
|
-
this.events.onMessage(
|
|
40
|
-
type: 'fatal',
|
|
41
|
-
error: serializeError(classifyError(cause)),
|
|
42
|
-
});
|
|
43
|
-
this.close();
|
|
52
|
+
this.events.onMessage(message);
|
|
44
53
|
},
|
|
54
|
+
fatal: (cause) => this.fail(cause),
|
|
45
55
|
finished: () => this.end('shut down'),
|
|
46
56
|
}, () => this.bootstrap());
|
|
57
|
+
this.catchStrays();
|
|
47
58
|
this.worker.start();
|
|
48
59
|
}
|
|
49
60
|
get alive() {
|
|
@@ -75,6 +86,34 @@ class InProcessRunner {
|
|
|
75
86
|
kill() {
|
|
76
87
|
this.close();
|
|
77
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Catches what a test leaves uncaught the way a worker process does
|
|
91
|
+
* (`worker/entry.ts`): a rejection nobody handled is charged to the attempt
|
|
92
|
+
* in flight, or recorded against the last test that finished, and is
|
|
93
|
+
* otherwise fatal to this worker, as an uncaught exception is (see
|
|
94
|
+
* `fail`). The
|
|
95
|
+
* listeners live exactly as long as the runner, so a host process keeps its
|
|
96
|
+
* own handling before and after the run. The scheduler keeps at most one
|
|
97
|
+
* in-process runner alive at a time, so nothing is charged twice.
|
|
98
|
+
*/
|
|
99
|
+
catchStrays() {
|
|
100
|
+
process.on('unhandledRejection', this.onRejection);
|
|
101
|
+
process.on('uncaughtException', this.onException);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Reports an unrecoverable failure and ends the worker the way a process
|
|
105
|
+
* dies: its unit is interrupted and nothing more it reports is passed on,
|
|
106
|
+
* so on exit the scheduler fails the test in flight with `WORKER_CRASH`
|
|
107
|
+
* and skips the unit's others, as it does for a worker process.
|
|
108
|
+
*/
|
|
109
|
+
fail(cause) {
|
|
110
|
+
if (this.exited || this.crashed)
|
|
111
|
+
return;
|
|
112
|
+
this.events.onMessage({ type: 'fatal', error: serializeError(classifyError(cause)) });
|
|
113
|
+
this.crashed = true;
|
|
114
|
+
this.worker.handle({ type: 'interrupt' });
|
|
115
|
+
this.close();
|
|
116
|
+
}
|
|
78
117
|
/** Stops accepting work and asks the queue to wind down. */
|
|
79
118
|
close() {
|
|
80
119
|
if (this.exited || this.closing)
|
|
@@ -86,6 +125,8 @@ class InProcessRunner {
|
|
|
86
125
|
if (this.exited)
|
|
87
126
|
return;
|
|
88
127
|
this.exited = true;
|
|
128
|
+
process.off('unhandledRejection', this.onRejection);
|
|
129
|
+
process.off('uncaughtException', this.onException);
|
|
89
130
|
this.events.onExit(detail);
|
|
90
131
|
this.finish();
|
|
91
132
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What user code in an `e2e run` process prints outside a test body: the
|
|
3
|
+
* config's top-level code, a test file's top level while it is collected,
|
|
4
|
+
* and reporters in the runner; the config's top-level code again in each
|
|
5
|
+
* worker. It is redacted with every secret value the process knows, as a
|
|
6
|
+
* test's output is. Until a config has loaded its secrets are unknown, so
|
|
7
|
+
* what is printed meanwhile is held, and withheld if the load fails.
|
|
8
|
+
*/
|
|
9
|
+
import { type SecretLedger } from '../internal/redact.ts';
|
|
10
|
+
import type { ListReporterOutput } from '../report/list.ts';
|
|
11
|
+
export type OutputStream = 'stdout' | 'stderr';
|
|
12
|
+
/** Writes text to one stream of the terminal; the callback fires once the stream took it. Answers false while the stream is backed up, like `Writable.write`. */
|
|
13
|
+
export type TerminalWrite = (text: string, callback?: () => void) => boolean;
|
|
14
|
+
/** Where runner output goes instead of the terminal while the list reporter shows the run. */
|
|
15
|
+
export interface OutputView {
|
|
16
|
+
/** Prints redacted text from `stream`. */
|
|
17
|
+
show(stream: OutputStream, text: string): void;
|
|
18
|
+
/** Prints what `show` left unfinished; nothing more follows. */
|
|
19
|
+
end(): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Holds what is written while a config loads. Released once the load
|
|
23
|
+
* registered the config's secrets; dropped for a warning when it failed.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ConfigLoadHold {
|
|
26
|
+
private held;
|
|
27
|
+
/** Holds `chunk` if a load is in flight; answers whether it did. */
|
|
28
|
+
hold(stream: OutputStream, chunk: string | Uint8Array): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Runs `load`, which loads a config and registers its secrets, holding
|
|
31
|
+
* what is written meanwhile. Then hands each held write to `release` in
|
|
32
|
+
* order, or, if `load` threw, `warn`s with how much was withheld.
|
|
33
|
+
*/
|
|
34
|
+
during<T>(load: () => Promise<T>, release: (stream: OutputStream, chunk: string | Uint8Array) => void, warn: (line: string) => void): Promise<T>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The runner process's user output, on its way to the terminal or, while
|
|
38
|
+
* the list reporter shows it, through the reporter, which prints it above
|
|
39
|
+
* its live window instead of letting it land inside.
|
|
40
|
+
*/
|
|
41
|
+
export declare class RunnerOutput {
|
|
42
|
+
private readonly terminal;
|
|
43
|
+
/** Each stream redacted on its own, across writes: a value split over two writes is still caught. */
|
|
44
|
+
private readonly redactors;
|
|
45
|
+
private readonly loadHold;
|
|
46
|
+
private view;
|
|
47
|
+
/** The terminal's stdout for the list reporter: its own lines and live window go straight there. */
|
|
48
|
+
readonly listOutput: ListReporterOutput;
|
|
49
|
+
constructor(terminal: Readonly<Record<OutputStream, TerminalWrite>>, ledger: SecretLedger);
|
|
50
|
+
/**
|
|
51
|
+
* One write of user code to `stream`, redacted: the tail a later write
|
|
52
|
+
* could complete into a value waits for it, or for `end`. Held while a
|
|
53
|
+
* config loads. Answers what the terminal did.
|
|
54
|
+
*/
|
|
55
|
+
write(stream: OutputStream, chunk: string | Uint8Array, callback?: () => void): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Runs `load`, a config load that registers the config's secrets, holding
|
|
58
|
+
* what is printed until it ends: the config's top-level code may print a
|
|
59
|
+
* value before the process knows it is one. Released redacted once it
|
|
60
|
+
* loaded; withheld with a warning when it failed.
|
|
61
|
+
*/
|
|
62
|
+
withholdDuring<T>(load: () => Promise<T>): Promise<T>;
|
|
63
|
+
/**
|
|
64
|
+
* Passes what is printed to `view` from now on, or to the terminal again
|
|
65
|
+
* when undefined. What the previous view was given is released to it
|
|
66
|
+
* first, unfinished lines included, and that view ended.
|
|
67
|
+
*/
|
|
68
|
+
showThrough(view: OutputView | undefined): void;
|
|
69
|
+
/** Releases what is still held, redacted, to the terminal. Later writes stay redacted; only a tail a later write could complete into a value is held. */
|
|
70
|
+
end(): void;
|
|
71
|
+
private release;
|
|
72
|
+
private emit;
|
|
73
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What user code in an `e2e run` process prints outside a test body: the
|
|
3
|
+
* config's top-level code, a test file's top level while it is collected,
|
|
4
|
+
* and reporters in the runner; the config's top-level code again in each
|
|
5
|
+
* worker. It is redacted with every secret value the process knows, as a
|
|
6
|
+
* test's output is. Until a config has loaded its secrets are unknown, so
|
|
7
|
+
* what is printed meanwhile is held, and withheld if the load fails.
|
|
8
|
+
*/
|
|
9
|
+
import { StreamRedactor } from '../internal/redact.js';
|
|
10
|
+
/** The warning that stands in for what a config printed before it failed to load. */
|
|
11
|
+
function withheldWarning(bytes) {
|
|
12
|
+
return `withheld ${bytes} bytes of output printed while the config failed to load: its secrets are unknown, so the output cannot be redacted`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Holds what is written while a config loads. Released once the load
|
|
16
|
+
* registered the config's secrets; dropped for a warning when it failed.
|
|
17
|
+
*/
|
|
18
|
+
export class ConfigLoadHold {
|
|
19
|
+
held;
|
|
20
|
+
/** Holds `chunk` if a load is in flight; answers whether it did. */
|
|
21
|
+
hold(stream, chunk) {
|
|
22
|
+
if (this.held === undefined)
|
|
23
|
+
return false;
|
|
24
|
+
// A copy: the writer may reuse its buffer once the write returns.
|
|
25
|
+
this.held.push({ stream, chunk: typeof chunk === 'string' ? chunk : Buffer.from(chunk) });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Runs `load`, which loads a config and registers its secrets, holding
|
|
30
|
+
* what is written meanwhile. Then hands each held write to `release` in
|
|
31
|
+
* order, or, if `load` threw, `warn`s with how much was withheld.
|
|
32
|
+
*/
|
|
33
|
+
async during(load, release, warn) {
|
|
34
|
+
this.held = [];
|
|
35
|
+
let loaded = false;
|
|
36
|
+
try {
|
|
37
|
+
const value = await load();
|
|
38
|
+
loaded = true;
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
const held = this.held;
|
|
43
|
+
this.held = undefined;
|
|
44
|
+
if (loaded) {
|
|
45
|
+
for (const { stream, chunk } of held)
|
|
46
|
+
release(stream, chunk);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const bytes = held.reduce((total, { chunk }) => total + Buffer.byteLength(chunk), 0);
|
|
50
|
+
if (bytes > 0)
|
|
51
|
+
warn(withheldWarning(bytes));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The runner process's user output, on its way to the terminal or, while
|
|
58
|
+
* the list reporter shows it, through the reporter, which prints it above
|
|
59
|
+
* its live window instead of letting it land inside.
|
|
60
|
+
*/
|
|
61
|
+
export class RunnerOutput {
|
|
62
|
+
terminal;
|
|
63
|
+
/** Each stream redacted on its own, across writes: a value split over two writes is still caught. */
|
|
64
|
+
redactors;
|
|
65
|
+
loadHold = new ConfigLoadHold();
|
|
66
|
+
view;
|
|
67
|
+
/** The terminal's stdout for the list reporter: its own lines and live window go straight there. */
|
|
68
|
+
listOutput;
|
|
69
|
+
constructor(terminal, ledger) {
|
|
70
|
+
this.terminal = terminal;
|
|
71
|
+
this.redactors = { stdout: new StreamRedactor(ledger), stderr: new StreamRedactor(ledger) };
|
|
72
|
+
this.listOutput = {
|
|
73
|
+
write: (line) => void terminal.stdout(`${line}\n`),
|
|
74
|
+
raw: (text) => void terminal.stdout(text),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* One write of user code to `stream`, redacted: the tail a later write
|
|
79
|
+
* could complete into a value waits for it, or for `end`. Held while a
|
|
80
|
+
* config loads. Answers what the terminal did.
|
|
81
|
+
*/
|
|
82
|
+
write(stream, chunk, callback) {
|
|
83
|
+
if (this.loadHold.hold(stream, chunk)) {
|
|
84
|
+
if (callback !== undefined)
|
|
85
|
+
process.nextTick(callback);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return this.emit(stream, this.redactors[stream].push(chunk), callback);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Runs `load`, a config load that registers the config's secrets, holding
|
|
92
|
+
* what is printed until it ends: the config's top-level code may print a
|
|
93
|
+
* value before the process knows it is one. Released redacted once it
|
|
94
|
+
* loaded; withheld with a warning when it failed.
|
|
95
|
+
*/
|
|
96
|
+
withholdDuring(load) {
|
|
97
|
+
return this.loadHold.during(load, (stream, chunk) => this.write(stream, chunk), (line) => this.emit('stderr', `e2e: [warning] ${line}\n`));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Passes what is printed to `view` from now on, or to the terminal again
|
|
101
|
+
* when undefined. What the previous view was given is released to it
|
|
102
|
+
* first, unfinished lines included, and that view ended.
|
|
103
|
+
*/
|
|
104
|
+
showThrough(view) {
|
|
105
|
+
if (this.view !== undefined) {
|
|
106
|
+
this.release();
|
|
107
|
+
this.view.end();
|
|
108
|
+
}
|
|
109
|
+
this.view = view;
|
|
110
|
+
}
|
|
111
|
+
/** Releases what is still held, redacted, to the terminal. Later writes stay redacted; only a tail a later write could complete into a value is held. */
|
|
112
|
+
end() {
|
|
113
|
+
this.showThrough(undefined);
|
|
114
|
+
this.release();
|
|
115
|
+
}
|
|
116
|
+
release() {
|
|
117
|
+
this.emit('stdout', this.redactors.stdout.flush());
|
|
118
|
+
this.emit('stderr', this.redactors.stderr.flush());
|
|
119
|
+
}
|
|
120
|
+
emit(stream, text, callback) {
|
|
121
|
+
if (text === '' || this.view !== undefined) {
|
|
122
|
+
if (text !== '')
|
|
123
|
+
this.view?.show(stream, text);
|
|
124
|
+
if (callback !== undefined)
|
|
125
|
+
process.nextTick(callback);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return this.terminal[stream](text, callback);
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/run/provision.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface PrepareScope {
|
|
|
55
55
|
/** The same `env` the workers are started with: what prepare provisions must be where a launch will look for it. */
|
|
56
56
|
readonly env: NodeJS.ProcessEnv;
|
|
57
57
|
readonly signal: AbortSignal;
|
|
58
|
+
/** Whether the run asked for a visible surface, so a hook provisions for the browser it will launch. */
|
|
59
|
+
readonly headed: boolean;
|
|
58
60
|
/** Where a hook's progress lines go, under the target they concern. */
|
|
59
61
|
readonly notice: (targetName: string, line: string) => void;
|
|
60
62
|
}
|
package/dist/run/provision.js
CHANGED
package/dist/run/records.d.ts
CHANGED
|
@@ -30,13 +30,13 @@ export interface ArtifactRecord {
|
|
|
30
30
|
/**
|
|
31
31
|
* Mirrors report-1: how much of the file the runner masked. A screenshot is
|
|
32
32
|
* `complete`; a trace is `complete` once every registered secret value was
|
|
33
|
-
* rewritten out of its text, and `not-required` when
|
|
34
|
-
*
|
|
33
|
+
* rewritten out of its text, and `not-required` when the run has no
|
|
34
|
+
* secret value to rewrite; a video is `incomplete`, since a recording masks nothing
|
|
35
35
|
* (a secure field renders its own dots, but anything else the screen
|
|
36
36
|
* showed is in the frames), and is kept as it is; a download is
|
|
37
37
|
* `incomplete` too, bytes the app served and the runner did not rewrite,
|
|
38
|
-
* unless
|
|
39
|
-
*
|
|
38
|
+
* unless the file is text the session's ledger was run over, which makes
|
|
39
|
+
* it `complete`. report-1 also admits an
|
|
40
40
|
* `incomplete` artifact without a `path`: a video a hosted service keeps,
|
|
41
41
|
* recorded by `url`, or one its producer withheld, which this runner never
|
|
42
42
|
* writes.
|
package/dist/run/retry.d.ts
CHANGED
|
@@ -17,3 +17,10 @@ export declare function isRetryEligible(attempt: RetryAttempt): boolean;
|
|
|
17
17
|
* did fail, and only its second chance was cut short.
|
|
18
18
|
*/
|
|
19
19
|
export declare function runWithRetries(maxAttempts: number, interruptSignal: AbortSignal, runOnce: (attemptIndex: number) => Promise<RetryAttempt | undefined>): Promise<ResultStatus>;
|
|
20
|
+
/**
|
|
21
|
+
* The verdict a sequence of finished attempts reaches: a pass is `flaky`
|
|
22
|
+
* after a failure, an interrupt during a retry keeps the verdict before it,
|
|
23
|
+
* and a self-skip decides. Also the verdict of attempts a worker finished
|
|
24
|
+
* before the run's interrupt stopped it, so both read alike.
|
|
25
|
+
*/
|
|
26
|
+
export declare function retryVerdict(attempts: readonly RetryAttempt[]): ResultStatus;
|
package/dist/run/retry.js
CHANGED
|
@@ -13,25 +13,41 @@ export function isRetryEligible(attempt) {
|
|
|
13
13
|
* did fail, and only its second chance was cut short.
|
|
14
14
|
*/
|
|
15
15
|
export async function runWithRetries(maxAttempts, interruptSignal, runOnce) {
|
|
16
|
-
|
|
17
|
-
let attemptCount = 0;
|
|
16
|
+
const attempts = [];
|
|
18
17
|
for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex += 1) {
|
|
19
18
|
if (interruptSignal.aborted)
|
|
20
19
|
break;
|
|
21
20
|
const attempt = await runOnce(attemptIndex);
|
|
22
21
|
if (attempt === undefined)
|
|
23
22
|
break;
|
|
24
|
-
|
|
25
|
-
if (attempt.status === 'passed')
|
|
26
|
-
return attemptCount > 1 ? 'flaky' : 'passed';
|
|
27
|
-
if (attempt.status === 'interrupted')
|
|
28
|
-
return attemptCount > 1 ? finalStatus : 'interrupted';
|
|
23
|
+
attempts.push(attempt);
|
|
29
24
|
// A body that skipped itself has decided; a retry would only ask again.
|
|
30
|
-
if (attempt.status === 'skipped')
|
|
31
|
-
|
|
32
|
-
finalStatus = attempt.status;
|
|
25
|
+
if (attempt.status === 'passed' || attempt.status === 'interrupted' || attempt.status === 'skipped')
|
|
26
|
+
break;
|
|
33
27
|
if (!isRetryEligible(attempt))
|
|
34
28
|
break;
|
|
35
29
|
}
|
|
36
|
-
return
|
|
30
|
+
return retryVerdict(attempts);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The verdict a sequence of finished attempts reaches: a pass is `flaky`
|
|
34
|
+
* after a failure, an interrupt during a retry keeps the verdict before it,
|
|
35
|
+
* and a self-skip decides. Also the verdict of attempts a worker finished
|
|
36
|
+
* before the run's interrupt stopped it, so both read alike.
|
|
37
|
+
*/
|
|
38
|
+
export function retryVerdict(attempts) {
|
|
39
|
+
let verdict = 'failed';
|
|
40
|
+
for (const [index, attempt] of attempts.entries()) {
|
|
41
|
+
switch (attempt.status) {
|
|
42
|
+
case 'passed':
|
|
43
|
+
return index > 0 ? 'flaky' : 'passed';
|
|
44
|
+
case 'interrupted':
|
|
45
|
+
return index > 0 ? verdict : 'interrupted';
|
|
46
|
+
case 'skipped':
|
|
47
|
+
return 'skipped';
|
|
48
|
+
default:
|
|
49
|
+
verdict = attempt.status;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return verdict;
|
|
37
53
|
}
|
package/dist/run/runner.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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
8
|
import { type ResultRecord } from './records.ts';
|
|
9
|
+
import type { RunnerOutput } from './process-output.ts';
|
|
9
10
|
import type { BuiltinReporter, E2EConfig, RecordingMode } from '../types.ts';
|
|
10
11
|
export interface RunOptions {
|
|
11
12
|
cwd?: string | undefined;
|
|
@@ -109,6 +110,13 @@ export interface RunOptions {
|
|
|
109
110
|
onEvent?: RunEventSink | undefined;
|
|
110
111
|
/** Budget for each reporter's `onRunFinished`, in ms. Only the test harness sets it; there is no flag. */
|
|
111
112
|
reporterTimeout?: number | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* The process's stdout and stderr, when the caller claimed them (the CLI):
|
|
115
|
+
* what user code in this process prints is held while the config loads
|
|
116
|
+
* and shown through the list reporter while the run reports, and the list
|
|
117
|
+
* reporter writes to the terminal past it.
|
|
118
|
+
*/
|
|
119
|
+
processOutput?: RunnerOutput | undefined;
|
|
112
120
|
}
|
|
113
121
|
/**
|
|
114
122
|
* A registration supplied in memory, under the virtual file name the report
|
package/dist/run/runner.js
CHANGED
|
@@ -23,6 +23,7 @@ import { runUnits } from './scheduler.js';
|
|
|
23
23
|
import { buildWorkPlans, plannedSlots } from './units.js';
|
|
24
24
|
import { SessionStore } from './sessions.js';
|
|
25
25
|
import { outputLayout } from './output.js';
|
|
26
|
+
import { registerStaticSecrets } from './secrecy.js';
|
|
26
27
|
import { claimRerunDir, pruneArtifacts } from './artifacts.js';
|
|
27
28
|
import { carryForward, lastFailedIds, readLastRun, reportArtifactPaths } from './last-run.js';
|
|
28
29
|
import { childProcessSpawner } from './worker/handle.js';
|
|
@@ -131,14 +132,27 @@ export async function run(options = {}) {
|
|
|
131
132
|
//
|
|
132
133
|
// Free ports for URLs declared with port 0 are chosen here, once: workers
|
|
133
134
|
// re-resolve the config and get the assignments in their bootstrap.
|
|
135
|
+
//
|
|
136
|
+
// The config's secrets are known to the process before what its top-level
|
|
137
|
+
// code printed is released, so that output is redacted like any other.
|
|
138
|
+
const loadConfig = async () => {
|
|
139
|
+
const config = await loadRunConfig(options, cwd, env, cli);
|
|
140
|
+
registerStaticSecrets(config.allSecrets);
|
|
141
|
+
return allocateAppPorts(config);
|
|
142
|
+
};
|
|
143
|
+
const processOutput = options.processOutput;
|
|
134
144
|
const loaded = await debug
|
|
135
|
-
.time('config.load', () =>
|
|
145
|
+
.time('config.load', () => (processOutput === undefined ? loadConfig() : processOutput.withholdDuring(loadConfig)))
|
|
136
146
|
.then((config) => ({ config, error: undefined }), (cause) => ({ config: undefined, error: classifyError(cause) }));
|
|
137
147
|
// Every reporter, built-in or configured, is one `Reporter` on one spine:
|
|
138
148
|
// the ids name the shipped ones, and a config failure still leaves the ids
|
|
139
149
|
// the CLI asked for, so the failure renders through them.
|
|
140
150
|
const reporterIds = loaded.config?.reporters ?? options.reporters ?? ['list'];
|
|
141
|
-
const listReporter = options.quiet === true || !reporterIds.includes('list') ? undefined : new ListReporter();
|
|
151
|
+
const listReporter = options.quiet === true || !reporterIds.includes('list') ? undefined : new ListReporter(processOutput?.listOutput);
|
|
152
|
+
// Until the run is over, what user code prints lands above the live window.
|
|
153
|
+
processOutput?.showThrough(listReporter === undefined
|
|
154
|
+
? undefined
|
|
155
|
+
: { show: (stream, text) => listReporter.processOutput(stream, text), end: () => listReporter.endProcessOutput() });
|
|
142
156
|
const activeReporters = [
|
|
143
157
|
...(listReporter === undefined ? [] : [listReporter]),
|
|
144
158
|
...reporterIds.filter((id) => id !== 'list').map((id) => STATELESS_REPORTERS[id]),
|
|
@@ -182,13 +196,13 @@ export async function run(options = {}) {
|
|
|
182
196
|
emit({ type: 'run-stopped', failures, limit });
|
|
183
197
|
};
|
|
184
198
|
/**
|
|
185
|
-
* The exit code is a fold over run state — every result, every run error,
|
|
199
|
+
* The exit code is a fold over run state — every result and serial group, every run error,
|
|
186
200
|
* the interrupt — never threaded through by hand. A run error recorded
|
|
187
201
|
* anywhere, including during teardown or the report write, reaches the exit
|
|
188
202
|
* code the same way.
|
|
189
203
|
*/
|
|
190
204
|
const currentExitCode = () => combineExitCodes([
|
|
191
|
-
...
|
|
205
|
+
...verdictExitCodes(results, serialGroups),
|
|
192
206
|
...(loaded.config?.failOnSkippedFailure === true && someSkippedAfterFailure(results, serialGroups) ? [1] : []),
|
|
193
207
|
...runErrors.map((runError) => exitCodeForCategory(runError.error.category)),
|
|
194
208
|
...(interruptController.signal.aborted ? [130] : []),
|
|
@@ -293,6 +307,9 @@ export async function run(options = {}) {
|
|
|
293
307
|
...(reportPath === undefined ? {} : { reportPath }),
|
|
294
308
|
...(aiTracePath === undefined ? {} : { aiTracePath }),
|
|
295
309
|
});
|
|
310
|
+
// The list reporter has printed its summary and stopped its window; a
|
|
311
|
+
// line another reporter left unfinished on `run-finished` prints too.
|
|
312
|
+
processOutput?.showThrough(undefined);
|
|
296
313
|
if (debug.enabled) {
|
|
297
314
|
process.stderr.write(debug.summary());
|
|
298
315
|
process.stderr.write(agentStepTable(results, serialGroups));
|
|
@@ -425,7 +442,7 @@ export async function run(options = {}) {
|
|
|
425
442
|
// stream as `notice` events under it, so the reporter prints them instead
|
|
426
443
|
// of a worker's stderr fighting the live status block.
|
|
427
444
|
try {
|
|
428
|
-
plans = await debug.time('engine.prepare', () => prepareEngines(plans, runWorkers, engines, { runId, projectRoot: config.projectRoot, env, signal: interrupted, notice }, emit));
|
|
445
|
+
plans = await debug.time('engine.prepare', () => prepareEngines(plans, runWorkers, engines, { runId, projectRoot: config.projectRoot, env, signal: interrupted, headed: options.headed ?? false, notice }, emit));
|
|
429
446
|
}
|
|
430
447
|
catch (cause) {
|
|
431
448
|
if (!interrupted.aborted)
|
|
@@ -752,14 +769,20 @@ function plannedFiles(selection) {
|
|
|
752
769
|
function statusOf(exitCode) {
|
|
753
770
|
return exitCode === 0 ? 'passed' : exitCode === 1 ? 'failed' : exitCode === 130 ? 'interrupted' : 'error';
|
|
754
771
|
}
|
|
755
|
-
|
|
772
|
+
/**
|
|
773
|
+
* The exit codes the verdicts imply: a failed test or serial group exits 1,
|
|
774
|
+
* or its attempts' error category when that is worse. A serial group answers
|
|
775
|
+
* for its members, whose results carry no attempts, and for a group attempt
|
|
776
|
+
* that failed before any member ran (a launch), whose members are skipped.
|
|
777
|
+
*/
|
|
778
|
+
function verdictExitCodes(results, serialGroups) {
|
|
756
779
|
const codes = [0];
|
|
757
|
-
for (const
|
|
758
|
-
switch (
|
|
780
|
+
for (const verdict of [...results, ...serialGroups]) {
|
|
781
|
+
switch (verdict.status) {
|
|
759
782
|
case 'failed':
|
|
760
783
|
case 'timed-out':
|
|
761
784
|
codes.push(1);
|
|
762
|
-
for (const attempt of
|
|
785
|
+
for (const attempt of verdict.attempts) {
|
|
763
786
|
if (attempt.error !== undefined)
|
|
764
787
|
codes.push(exitCodeForCategory(attempt.error.category));
|
|
765
788
|
}
|