e2e 0.16.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/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/ai-sdk.js +5 -4
- package/dist/agent/error.js +3 -3
- package/dist/agent/invocation.js +3 -4
- package/dist/agent/model/sdk.js +2 -0
- package/dist/agent/model/tool-choice.d.ts +9 -0
- package/dist/agent/model/tool-choice.js +14 -0
- package/dist/agent/observation-feed.d.ts +5 -6
- package/dist/agent/observation-feed.js +8 -11
- package/dist/agent/observation.d.ts +40 -11
- package/dist/agent/observation.js +109 -59
- package/dist/agent/phases.d.ts +7 -1
- package/dist/agent/phases.js +12 -3
- package/dist/agent/point-tap.d.ts +8 -8
- package/dist/agent/point-tap.js +1 -1
- package/dist/agent/prompts.js +3 -0
- package/dist/agent/replay.d.ts +19 -8
- package/dist/agent/replay.js +29 -29
- package/dist/agent/scroll-target.d.ts +2 -3
- package/dist/agent/step-cache.d.ts +43 -15
- package/dist/agent/step-cache.js +156 -67
- package/dist/agent/tool-loop.js +94 -5
- 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 +76 -30
- package/dist/cli/bin.js +2 -2
- package/dist/cli/index.js +43 -32
- 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/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/init/versions.d.ts +0 -8
- package/dist/cli/init/versions.js +1 -10
- 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/resolve.d.ts +1 -0
- package/dist/config/resolve.js +33 -1
- 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 +4 -2
- package/dist/engine/session.js +2 -2
- package/dist/expect/async.js +4 -2
- package/dist/expect/poll-scope.d.ts +52 -0
- package/dist/expect/poll-scope.js +113 -0
- package/dist/expect/poll.js +33 -6
- 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/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 +9 -4
- package/dist/internal/redact.js +120 -23
- package/dist/internal/time.d.ts +26 -4
- package/dist/internal/time.js +58 -14
- 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 +55 -14
- package/dist/locator/screen.js +59 -9
- package/dist/mcp/catalog.d.ts +3 -2
- package/dist/mcp/catalog.js +6 -6
- 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 +56 -15
- package/dist/mcp/tools.js +7 -5
- 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 +12 -0
- package/dist/report/list.js +61 -24
- 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 +22 -4
- package/dist/run/artifacts.js +71 -3
- package/dist/run/execute.d.ts +34 -5
- package/dist/run/execute.js +93 -44
- package/dist/run/extended-fixtures.js +6 -2
- package/dist/run/failure-evidence.js +10 -4
- package/dist/run/fixtures.js +6 -8
- package/dist/run/in-process.d.ts +2 -0
- package/dist/run/in-process.js +52 -10
- package/dist/run/last-run.d.ts +42 -8
- package/dist/run/last-run.js +121 -14
- 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/realm.d.ts +4 -1
- package/dist/run/realm.js +20 -6
- package/dist/run/records.d.ts +20 -4
- package/dist/run/records.js +26 -0
- package/dist/run/retry.d.ts +10 -1
- package/dist/run/retry.js +30 -12
- package/dist/run/runner.d.ts +9 -1
- package/dist/run/runner.js +101 -39
- 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 +75 -2
- package/dist/run/serial.js +243 -59
- package/dist/run/standalone.js +1 -1
- package/dist/run/steps.d.ts +0 -6
- package/dist/run/steps.js +3 -20
- package/dist/run/trace-redaction.d.ts +5 -6
- package/dist/run/trace-redaction.js +5 -6
- package/dist/run/worker/entry.js +40 -18
- package/dist/run/worker/handle.js +10 -5
- package/dist/run/worker/protocol.d.ts +85 -3
- package/dist/run/worker/session.d.ts +2 -0
- package/dist/run/worker/session.js +29 -0
- package/dist/telemetry/environment.d.ts +2 -1
- package/dist/telemetry/environment.js +12 -2
- package/dist/telemetry/events.js +1 -0
- 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 +15 -6
- package/docs/agent-steps.mdx +1 -0
- package/docs/agents.mdx +7 -3
- package/docs/api-testing.mdx +2 -1
- package/docs/authentication.mdx +6 -3
- package/docs/browser.mdx +49 -12
- package/docs/bug-bash.mdx +2 -1
- package/docs/cache.mdx +66 -19
- package/docs/ci.mdx +14 -9
- package/docs/coding-agents.mdx +2 -1
- package/docs/core-concepts.mdx +208 -0
- package/docs/debugging.mdx +25 -12
- package/docs/decision-models.mdx +142 -0
- package/docs/example-projects.mdx +32 -0
- package/docs/executors.mdx +7 -2
- package/docs/explore.mdx +1 -0
- package/docs/github.mdx +14 -5
- 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 +3 -2
- 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 +44 -18
- package/docs/migrate/selenium.mdx +27 -3
- package/docs/mobile-ci.mdx +6 -5
- package/docs/mobile.mdx +96 -8
- package/docs/models.mdx +60 -9
- package/docs/quickstart.mdx +28 -16
- package/docs/reference/agent.mdx +13 -3
- package/docs/reference/app.mdx +2 -1
- package/docs/reference/cli.mdx +41 -18
- package/docs/reference/config.mdx +67 -13
- package/docs/reference/engine.mdx +25 -13
- package/docs/reference/environment.mdx +10 -2
- package/docs/reference/errors.mdx +13 -10
- package/docs/reference/expect.mdx +36 -13
- package/docs/reference/mcp.mdx +21 -8
- package/docs/reference/mobile.mdx +24 -13
- package/docs/reference/reporters.mdx +9 -2
- package/docs/reference/screen.mdx +49 -16
- package/docs/reference/test.mdx +32 -5
- package/docs/reference/web.mdx +133 -23
- package/docs/security.mdx +58 -42
- package/docs/subscriptions.mdx +92 -12
- package/docs/telemetry.mdx +9 -6
- 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 +25 -8
- package/schema/report-v1.schema.json +14 -1
- package/skills/e2e/SKILL.md +5 -3
- package/skills/e2e/references/agent.md +33 -11
- package/skills/e2e/references/debugging.md +4 -4
- package/skills/e2e/references/mcp.md +8 -3
- package/skills/e2e/references/running.md +15 -9
- package/skills/e2e/references/setup.md +45 -16
- package/skills/e2e/references/writing-tests.md +35 -18
- package/docs/viewports.mdx +0 -146
- package/docs/writing-tests.mdx +0 -256
package/dist/run/scheduler.js
CHANGED
|
@@ -7,14 +7,23 @@
|
|
|
7
7
|
* gating, and reporting logic covers both child-process and in-process
|
|
8
8
|
* execution.
|
|
9
9
|
*/
|
|
10
|
-
import { InfrastructureError, serializeError } from '../internal/errors.js';
|
|
10
|
+
import { InfrastructureError, TestTimeoutError, serializeError } from '../internal/errors.js';
|
|
11
11
|
import { timestamp, uuidv7 } from '../internal/ids.js';
|
|
12
|
+
import { retryVerdict } from './retry.js';
|
|
13
|
+
import { SerialGroupProgress, serialGroupId } from './serial.js';
|
|
12
14
|
import { INTERRUPTED_BEFORE_START, countsTowardFailureLimit, failureLimitSkip, nonRunResult, pairResult, setupFailedSkip, unstartedResult, pairKey, } from './units.js';
|
|
13
15
|
import { decodeResult, } from './worker/protocol.js';
|
|
14
16
|
/** Consecutive worker-boot failures per target before its units are failed. */
|
|
15
17
|
const MAX_INIT_FAILURES = 2;
|
|
16
18
|
/** How long a retiring or draining worker gets before it is force-killed. */
|
|
17
19
|
const SHUTDOWN_GRACE_MS = 10_000;
|
|
20
|
+
/** Why a pair of a target whose workers cannot boot never ran. */
|
|
21
|
+
const WORKER_START_FAILED = {
|
|
22
|
+
cause: 'infrastructure-unavailable',
|
|
23
|
+
reason: 'worker process failed to start',
|
|
24
|
+
};
|
|
25
|
+
/** The least time a worker past an attempt deadline gets to answer a ping, however small `cleanupTimeout` is. */
|
|
26
|
+
const MIN_PING_REPLY_MS = 5_000;
|
|
18
27
|
/** Whether a worker process died of a signal that interrupts the run, as a Ctrl-C to its process group does. */
|
|
19
28
|
function isInterruptSignal(signal) {
|
|
20
29
|
return signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGHUP';
|
|
@@ -23,6 +32,16 @@ function isInterruptSignal(signal) {
|
|
|
23
32
|
export async function runUnits(options) {
|
|
24
33
|
await new Scheduler(options).run();
|
|
25
34
|
}
|
|
35
|
+
/** The result of a pair from the attempts it finished, with the verdict they reach. */
|
|
36
|
+
function finishedResult(pair, attempts) {
|
|
37
|
+
const last = attempts.at(-1);
|
|
38
|
+
return pairResult(pair, {
|
|
39
|
+
status: retryVerdict(attempts),
|
|
40
|
+
selected: true,
|
|
41
|
+
attempts,
|
|
42
|
+
...(last?.status === 'skipped' ? { skip: last.skip } : {}),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
26
45
|
/**
|
|
27
46
|
* One worker and the unit it is executing. Owns its own lifecycle so the
|
|
28
47
|
* scheduler never has to keep a separate record in sync with the transport.
|
|
@@ -39,13 +58,22 @@ class SchedulerWorker {
|
|
|
39
58
|
reported = new Set();
|
|
40
59
|
/** The pair (`pairKey`) executing now, when one is. */
|
|
41
60
|
inFlightPair;
|
|
61
|
+
/** Attempts of `unit`'s pairs heard ahead of the pair's result, by `pairKey`: what a crash keeps. */
|
|
62
|
+
attempts = new Map();
|
|
63
|
+
/** What was heard of `unit`'s serial groups still running, by group report id: what a crash keeps. */
|
|
64
|
+
serialGroups = new Map();
|
|
42
65
|
sawFailure = false;
|
|
43
66
|
becameReady = false;
|
|
44
67
|
/** Told to tear down at once by a forced interrupt; its exit is then the one asked for. */
|
|
45
68
|
terminated = false;
|
|
46
69
|
/** Killed by the scheduler, once a grace budget ran out or over a fatal it reported; its exit is then the one asked for too. */
|
|
47
70
|
killed = false;
|
|
71
|
+
/** The attempt whose worker stopped answering past its deadline, once the watchdog killed the worker over it. */
|
|
72
|
+
hung;
|
|
48
73
|
killTimer;
|
|
74
|
+
watched;
|
|
75
|
+
watchTimer;
|
|
76
|
+
awaitingPong = false;
|
|
49
77
|
constructor(targetName, workerSlot, spawn, onMessage, onExit) {
|
|
50
78
|
this.targetName = targetName;
|
|
51
79
|
this.workerSlot = workerSlot;
|
|
@@ -53,6 +81,7 @@ class SchedulerWorker {
|
|
|
53
81
|
onMessage: (message) => onMessage(this, message),
|
|
54
82
|
onExit: (detail, signal) => {
|
|
55
83
|
this.clearKillTimer();
|
|
84
|
+
this.unwatch();
|
|
56
85
|
onExit(this, detail, signal ?? null);
|
|
57
86
|
},
|
|
58
87
|
});
|
|
@@ -62,6 +91,8 @@ class SchedulerWorker {
|
|
|
62
91
|
this.state = 'busy';
|
|
63
92
|
this.unit = unit;
|
|
64
93
|
this.reported.clear();
|
|
94
|
+
this.attempts.clear();
|
|
95
|
+
this.serialGroups.clear();
|
|
65
96
|
this.inFlightPair = undefined;
|
|
66
97
|
this.sawFailure = false;
|
|
67
98
|
const pairs = unit.pairs.map((pair) => ({
|
|
@@ -104,12 +135,79 @@ class SchedulerWorker {
|
|
|
104
135
|
}, graceMs);
|
|
105
136
|
this.killTimer.unref();
|
|
106
137
|
}
|
|
138
|
+
/** The attempts heard of one of `unit`'s pairs, started on first word of them. */
|
|
139
|
+
pairAttempts(key) {
|
|
140
|
+
let attempts = this.attempts.get(key);
|
|
141
|
+
if (attempts === undefined) {
|
|
142
|
+
attempts = { finished: [], running: undefined };
|
|
143
|
+
this.attempts.set(key, attempts);
|
|
144
|
+
}
|
|
145
|
+
return attempts;
|
|
146
|
+
}
|
|
147
|
+
/** The progress of one of `unit`'s serial groups, started on first word of it. */
|
|
148
|
+
serialGroup(groupId) {
|
|
149
|
+
let progress = this.serialGroups.get(groupId);
|
|
150
|
+
if (progress === undefined) {
|
|
151
|
+
progress = new SerialGroupProgress();
|
|
152
|
+
this.serialGroups.set(groupId, progress);
|
|
153
|
+
}
|
|
154
|
+
return progress;
|
|
155
|
+
}
|
|
107
156
|
clearKillTimer() {
|
|
108
157
|
if (this.killTimer === undefined)
|
|
109
158
|
return;
|
|
110
159
|
clearTimeout(this.killTimer);
|
|
111
160
|
this.killTimer = undefined;
|
|
112
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* The watchdog over one attempt. The worker enforces the test timeout with
|
|
164
|
+
* timers in its own process, which a body that blocks the event loop
|
|
165
|
+
* (`while (true) {}`) never lets fire. From the deadline on, the worker is
|
|
166
|
+
* pinged once per cleanup budget (at least `MIN_PING_REPLY_MS`) for as
|
|
167
|
+
* long as the attempt lasts, teardown included: one still tearing it down
|
|
168
|
+
* answers and goes on, one that has not answered by the next ping is
|
|
169
|
+
* killed, and the attempt is the one it hung in.
|
|
170
|
+
*/
|
|
171
|
+
watch(message) {
|
|
172
|
+
this.unwatch();
|
|
173
|
+
this.watched = {
|
|
174
|
+
pair: pairKey(message.testId, message.agent, message.repeat),
|
|
175
|
+
index: message.attempt,
|
|
176
|
+
id: message.attemptId,
|
|
177
|
+
startedAt: message.startedAt,
|
|
178
|
+
timeoutMs: message.timeoutMs,
|
|
179
|
+
};
|
|
180
|
+
const check = () => {
|
|
181
|
+
if (this.watched === undefined || !this.runner.alive)
|
|
182
|
+
return;
|
|
183
|
+
if (this.awaitingPong) {
|
|
184
|
+
this.hung = this.watched;
|
|
185
|
+
this.killed = true;
|
|
186
|
+
this.runner.kill();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.awaitingPong = true;
|
|
190
|
+
this.watchAfter(Math.max(message.graceMs, MIN_PING_REPLY_MS), check);
|
|
191
|
+
this.runner.send({ type: 'ping' });
|
|
192
|
+
};
|
|
193
|
+
this.watchAfter(message.timeoutMs, check);
|
|
194
|
+
}
|
|
195
|
+
/** The worker answered a ping: its event loop turns. */
|
|
196
|
+
pong() {
|
|
197
|
+
this.awaitingPong = false;
|
|
198
|
+
}
|
|
199
|
+
/** Stops watching the attempt: it ended, or the worker is gone. */
|
|
200
|
+
unwatch() {
|
|
201
|
+
if (this.watchTimer !== undefined)
|
|
202
|
+
clearTimeout(this.watchTimer);
|
|
203
|
+
this.watchTimer = undefined;
|
|
204
|
+
this.watched = undefined;
|
|
205
|
+
this.awaitingPong = false;
|
|
206
|
+
}
|
|
207
|
+
watchAfter(delayMs, check) {
|
|
208
|
+
this.watchTimer = setTimeout(check, delayMs);
|
|
209
|
+
this.watchTimer.unref();
|
|
210
|
+
}
|
|
113
211
|
}
|
|
114
212
|
class Scheduler {
|
|
115
213
|
options;
|
|
@@ -356,8 +454,17 @@ class Scheduler {
|
|
|
356
454
|
return runnable.length === unit.pairs.length ? unit : { ...unit, pairs: runnable };
|
|
357
455
|
}
|
|
358
456
|
}
|
|
359
|
-
/**
|
|
457
|
+
/**
|
|
458
|
+
* Puts an untaken unit back so capacity pressure never drops work. A
|
|
459
|
+
* failed target's queues were already drained and nothing takes from them
|
|
460
|
+
* again, so its unit skips instead.
|
|
461
|
+
*/
|
|
360
462
|
returnUnit(state, unit) {
|
|
463
|
+
if (state.failed) {
|
|
464
|
+
for (const pair of unit.pairs)
|
|
465
|
+
this.report(unstartedResult(pair, WORKER_START_FAILED));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
361
468
|
if (unit.kind === 'setup')
|
|
362
469
|
state.setupQueue.unshift(unit);
|
|
363
470
|
else
|
|
@@ -478,10 +585,27 @@ class Scheduler {
|
|
|
478
585
|
}
|
|
479
586
|
case 'pair-start': {
|
|
480
587
|
worker.inFlightPair = pairKey(message.testId, message.agent, message.repeat);
|
|
588
|
+
if (message.serialId !== undefined) {
|
|
589
|
+
worker
|
|
590
|
+
.serialGroup(serialGroupId(message.serialId, worker.targetName, message.agent, message.repeat))
|
|
591
|
+
.memberStarted(message.testId);
|
|
592
|
+
}
|
|
481
593
|
const { type: _type, ...start } = message;
|
|
482
594
|
this.options.events.onTestStart?.(start, worker.targetName);
|
|
483
595
|
break;
|
|
484
596
|
}
|
|
597
|
+
case 'attempt-deadline': {
|
|
598
|
+
worker.watch(message);
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
case 'attempt-end': {
|
|
602
|
+
worker.unwatch();
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case 'pong': {
|
|
606
|
+
worker.pong();
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
485
609
|
case 'progress': {
|
|
486
610
|
this.options.events.onProgress?.({ testId: message.testId, agent: message.agent, repeat: message.repeat }, worker.targetName, message.progress);
|
|
487
611
|
break;
|
|
@@ -498,7 +622,9 @@ class Scheduler {
|
|
|
498
622
|
case 'result': {
|
|
499
623
|
const state = this.targetState(worker);
|
|
500
624
|
const result = decodeResult(message.result, state.target);
|
|
501
|
-
|
|
625
|
+
const key = pairKey(result.test.id, result.agent, result.repeat);
|
|
626
|
+
worker.reported.add(key);
|
|
627
|
+
worker.attempts.delete(key);
|
|
502
628
|
if (result.status !== 'passed' && result.status !== 'flaky' && result.status !== 'skipped') {
|
|
503
629
|
worker.sawFailure = true;
|
|
504
630
|
}
|
|
@@ -508,9 +634,28 @@ class Scheduler {
|
|
|
508
634
|
break;
|
|
509
635
|
}
|
|
510
636
|
case 'serial-group': {
|
|
637
|
+
worker.serialGroups.delete(message.group.id);
|
|
511
638
|
this.options.events.onSerialGroup(message.group);
|
|
512
639
|
break;
|
|
513
640
|
}
|
|
641
|
+
case 'attempt-start': {
|
|
642
|
+
worker.pairAttempts(pairKey(message.testId, message.agent, message.repeat)).running = message.index;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
case 'attempt': {
|
|
646
|
+
const attempts = worker.pairAttempts(pairKey(message.testId, message.agent, message.repeat));
|
|
647
|
+
attempts.finished.push(message.attempt);
|
|
648
|
+
attempts.running = undefined;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case 'serial-member': {
|
|
652
|
+
worker.serialGroup(message.groupId).memberFinished(message.attempt, message.member);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
case 'serial-attempt': {
|
|
656
|
+
worker.serialGroup(message.groupId).attemptFinished(message.run);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
514
659
|
case 'unit-done': {
|
|
515
660
|
for (const runError of message.runErrors)
|
|
516
661
|
this.options.events.onRunError(runError);
|
|
@@ -610,50 +755,121 @@ class Scheduler {
|
|
|
610
755
|
this.signalledWave ??= new Set([...this.workers, worker]);
|
|
611
756
|
return this.signalledWave.has(worker);
|
|
612
757
|
}
|
|
613
|
-
/**
|
|
758
|
+
/**
|
|
759
|
+
* Emits records for a unit whose worker died before reporting it done.
|
|
760
|
+
* Attempts the worker finished stand. The test in flight gets a
|
|
761
|
+
* `WORKER_CRASH` attempt after them when the crash landed during an
|
|
762
|
+
* attempt; one between attempts (an `afterAll` after the last) is recorded
|
|
763
|
+
* on the last attempt instead, whose cleanup it forced. A serial group in
|
|
764
|
+
* flight gets a failed group attempt after its finished ones. A worker the
|
|
765
|
+
* run's interrupt stopped charges no crash: a test keeps the attempts it
|
|
766
|
+
* finished and the verdict they reach, as a retry the interrupt cut short
|
|
767
|
+
* does, and a serial group's attempt in flight is recorded interrupted.
|
|
768
|
+
*
|
|
769
|
+
* A worker the watchdog killed hung in one attempt: that attempt timed out
|
|
770
|
+
* (`TEST_TIMEOUT`, after the attempts before it, never retried), and the
|
|
771
|
+
* unit's tests it never reached run on a fresh worker, as they would have
|
|
772
|
+
* after a timeout the worker enforced itself. Only the rest of a serial
|
|
773
|
+
* group it hung in is lost with it.
|
|
774
|
+
*/
|
|
614
775
|
synthesizeCrashResults(state, worker, unit) {
|
|
615
|
-
const interrupted = this.interrupting;
|
|
776
|
+
const interrupted = this.interrupting && (worker.terminated || worker.killed);
|
|
777
|
+
const crash = serializeError(new InfrastructureError('WORKER_CRASH', 'worker process exited during this test'));
|
|
778
|
+
const hung = worker.hung;
|
|
779
|
+
const hungPair = hung === undefined ? undefined : unit.pairs.find((pair) => pairKey(pair.test.id, pair.agent, pair.repeat) === hung.pair);
|
|
780
|
+
const hungGroupId = hungPair?.test.serialId === undefined
|
|
781
|
+
? undefined
|
|
782
|
+
: serialGroupId(hungPair.test.serialId, worker.targetName, hungPair.agent, hungPair.repeat);
|
|
783
|
+
const settledGroups = new Set();
|
|
784
|
+
const rescheduled = [];
|
|
616
785
|
for (const pair of unit.pairs) {
|
|
617
786
|
const key = pairKey(pair.test.id, pair.agent, pair.repeat);
|
|
618
787
|
if (worker.reported.has(key))
|
|
619
788
|
continue;
|
|
789
|
+
const { serialId } = pair.test;
|
|
790
|
+
const groupId = serialId === undefined ? undefined : serialGroupId(serialId, worker.targetName, pair.agent, pair.repeat);
|
|
791
|
+
if (groupId !== undefined) {
|
|
792
|
+
if (settledGroups.has(groupId))
|
|
793
|
+
continue;
|
|
794
|
+
const members = unit.pairs.filter((member) => member.test.serialId === serialId && member.agent === pair.agent && member.repeat === pair.repeat);
|
|
795
|
+
const progress = worker.serialGroups.get(groupId);
|
|
796
|
+
let settled;
|
|
797
|
+
if (hung !== undefined && groupId === hungGroupId)
|
|
798
|
+
settled = progress?.crashed(members, state.target, hangError(hung), 'timed-out');
|
|
799
|
+
else if (interrupted)
|
|
800
|
+
settled = progress?.interrupted(members, state.target, this.stopSkip ?? INTERRUPTED_BEFORE_START);
|
|
801
|
+
else
|
|
802
|
+
settled = progress?.crashed(members, state.target, crash, 'failed');
|
|
803
|
+
if (settled !== undefined) {
|
|
804
|
+
settledGroups.add(groupId);
|
|
805
|
+
this.options.events.onSerialGroup(settled.group);
|
|
806
|
+
for (const result of settled.results)
|
|
807
|
+
this.report(result);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const { finished, running } = worker.attempts.get(key) ?? { finished: [], running: undefined };
|
|
812
|
+
if (hung !== undefined && key === hung.pair) {
|
|
813
|
+
this.markSessionsFailed(state, pair);
|
|
814
|
+
this.report(finishedResult(pair, [...finished, hungAttempt(hung)]));
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
620
817
|
if (interrupted) {
|
|
621
|
-
this.report(
|
|
818
|
+
this.report(finished.length === 0
|
|
819
|
+
? unstartedResult(pair, this.stopSkip ?? INTERRUPTED_BEFORE_START)
|
|
820
|
+
: finishedResult(pair, finished));
|
|
622
821
|
continue;
|
|
623
822
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
state.failedSessions.set(session, pair.test.id);
|
|
628
|
-
}
|
|
823
|
+
if (hung !== undefined && (hungGroupId === undefined || groupId !== hungGroupId)) {
|
|
824
|
+
rescheduled.push(pair);
|
|
825
|
+
continue;
|
|
629
826
|
}
|
|
630
|
-
|
|
631
|
-
if (
|
|
827
|
+
this.markSessionsFailed(state, pair);
|
|
828
|
+
if (key !== worker.inFlightPair) {
|
|
632
829
|
this.report(unstartedResult(pair, {
|
|
633
830
|
cause: 'infrastructure-unavailable',
|
|
634
831
|
reason: 'worker process exited before this test started',
|
|
635
832
|
}));
|
|
636
833
|
continue;
|
|
637
834
|
}
|
|
835
|
+
const last = finished.at(-1);
|
|
836
|
+
if (last !== undefined && running === undefined) {
|
|
837
|
+
this.report(finishedResult(pair, [
|
|
838
|
+
...finished.slice(0, -1),
|
|
839
|
+
{ ...last, secondaryErrors: [...last.secondaryErrors, crash], cleanup: 'forced' },
|
|
840
|
+
]));
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
638
843
|
this.report(pairResult(pair, {
|
|
639
844
|
status: 'failed',
|
|
640
845
|
selected: true,
|
|
641
846
|
attempts: [
|
|
847
|
+
...finished,
|
|
642
848
|
{
|
|
643
849
|
id: uuidv7(),
|
|
644
|
-
index:
|
|
850
|
+
index: running ?? finished.length,
|
|
645
851
|
status: 'failed',
|
|
646
852
|
startedAt: timestamp(),
|
|
647
853
|
durationMs: 0,
|
|
648
854
|
steps: [],
|
|
649
855
|
artifacts: [],
|
|
650
|
-
error:
|
|
856
|
+
error: crash,
|
|
651
857
|
secondaryErrors: [],
|
|
652
858
|
cleanup: 'forced',
|
|
653
859
|
},
|
|
654
860
|
],
|
|
655
861
|
}));
|
|
656
862
|
}
|
|
863
|
+
if (rescheduled.length > 0)
|
|
864
|
+
this.returnUnit(state, { ...unit, pairs: rescheduled });
|
|
865
|
+
}
|
|
866
|
+
/** A crashed setup never persisted its sessions; dependents must skip. */
|
|
867
|
+
markSessionsFailed(state, pair) {
|
|
868
|
+
if (pair.test.kind !== 'setup')
|
|
869
|
+
return;
|
|
870
|
+
for (const session of pair.test.sessions) {
|
|
871
|
+
state.failedSessions.set(session, pair.test.id);
|
|
872
|
+
}
|
|
657
873
|
}
|
|
658
874
|
/**
|
|
659
875
|
* Fails a target whose workers cannot boot; drains its queues with skips.
|
|
@@ -667,10 +883,7 @@ class Scheduler {
|
|
|
667
883
|
error: serializeError(new InfrastructureError('WORKER_INIT_FAILED', `workers for target "${state.target.name}" failed to start ${MAX_INIT_FAILURES} times; remaining units skipped`)),
|
|
668
884
|
});
|
|
669
885
|
}
|
|
670
|
-
this.skipQueues(state,
|
|
671
|
-
cause: 'infrastructure-unavailable',
|
|
672
|
-
reason: 'worker process failed to start',
|
|
673
|
-
});
|
|
886
|
+
this.skipQueues(state, WORKER_START_FAILED);
|
|
674
887
|
}
|
|
675
888
|
/** Shuts every remaining worker down, force-killing stragglers. */
|
|
676
889
|
async drain() {
|
|
@@ -682,3 +895,26 @@ class Scheduler {
|
|
|
682
895
|
await Promise.all(remaining.map((worker) => worker.runner.exit));
|
|
683
896
|
}
|
|
684
897
|
}
|
|
898
|
+
/** Why the watchdog killed a worker that hung in `hung`. */
|
|
899
|
+
function hangError(hung) {
|
|
900
|
+
return serializeError(new TestTimeoutError(`test ran past its ${hung.timeoutMs} ms timeout and its worker stopped responding, so the worker was killed`));
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* The attempt a worker the watchdog killed hung in. It ends the pair's
|
|
904
|
+
* attempts: a body that blocked the event loop once does so again, so it is
|
|
905
|
+
* not retried.
|
|
906
|
+
*/
|
|
907
|
+
function hungAttempt(hung) {
|
|
908
|
+
return {
|
|
909
|
+
id: hung.id,
|
|
910
|
+
index: hung.index,
|
|
911
|
+
status: 'timed-out',
|
|
912
|
+
startedAt: hung.startedAt,
|
|
913
|
+
durationMs: Date.now() - Date.parse(hung.startedAt),
|
|
914
|
+
steps: [],
|
|
915
|
+
artifacts: [],
|
|
916
|
+
error: hangError(hung),
|
|
917
|
+
secondaryErrors: [],
|
|
918
|
+
cleanup: 'forced',
|
|
919
|
+
};
|
|
920
|
+
}
|
package/dist/run/secrecy.d.ts
CHANGED
|
@@ -6,40 +6,43 @@ import type { Secret } from '../types.ts';
|
|
|
6
6
|
export interface SessionSecrecy {
|
|
7
7
|
/** Every secret value the session may have seen; live, so a provider's value joins the moment it exists. */
|
|
8
8
|
readonly ledger: SecretLedger;
|
|
9
|
-
/**
|
|
9
|
+
/** Whether a secret was filled into the session, and what that withholds. */
|
|
10
10
|
readonly exposure: SecretExposure;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
* `none`,
|
|
15
|
-
*
|
|
16
|
-
* and a page can render it (an echo of the `Authorization` header) though
|
|
17
|
-
* nothing was typed; `filled`, a secret was typed into the app.
|
|
13
|
+
* Whether a secret was typed into one session's app. It only rises, and is
|
|
14
|
+
* never cleared: `none`, nothing was filled; `filled`, a secret was typed
|
|
15
|
+
* into the app.
|
|
18
16
|
*/
|
|
19
|
-
export type SecretExposureLevel = 'none' | '
|
|
17
|
+
export type SecretExposureLevel = 'none' | 'filled';
|
|
20
18
|
/**
|
|
21
19
|
* One session's exposure level, and the one place that maps it to what each
|
|
22
|
-
* consumer
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* text only, so a page that renders it on screen is not masked. Only a fill
|
|
20
|
+
* consumer withholds. Only a fill withholds pixels (failure screenshots,
|
|
21
|
+
* `app.screenshot()`, assert evidence, model input, and a trace's
|
|
22
|
+
* screencast frames): a value that reached the app otherwise (an engine
|
|
23
|
+
* option, a URL or a `fill` the test spelled it into) is protected as text
|
|
24
|
+
* only, so a page that renders it on screen is not masked. Only a fill
|
|
28
25
|
* carries into a saved session too: the value then lives in the app's
|
|
29
|
-
* state
|
|
30
|
-
* the restoring session.
|
|
26
|
+
* state. Text is not decided here: see `redactsRecordings`.
|
|
31
27
|
*/
|
|
32
28
|
export declare class SecretExposure {
|
|
33
29
|
private current;
|
|
34
|
-
/** Records that a secret
|
|
30
|
+
/** Records that a secret was filled into the session. */
|
|
35
31
|
raise(level: Exclude<SecretExposureLevel, 'none'>): void;
|
|
36
32
|
/** Whether no pixels may leave the session: no screenshot or screencast frame is taken, kept, or handed to a model. */
|
|
37
33
|
get withholdsPixels(): boolean;
|
|
38
|
-
/** Whether a text recording of the session (a trace, a text-like download) is rewritten through the ledger before it is kept. */
|
|
39
|
-
get redactsRecordings(): boolean;
|
|
40
34
|
/** Whether a session saved from this one carries the taint to the sessions that restore it. */
|
|
41
35
|
get carriesTaint(): boolean;
|
|
42
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether a text recording of the session (a trace, a text-like download) is
|
|
39
|
+
* rewritten through its ledger before it is kept: whenever the ledger holds
|
|
40
|
+
* a value, filled or not. A value reaches the app in ways the runner never
|
|
41
|
+
* sees (a URL or a `fill` the test spelled it into, the app's own config),
|
|
42
|
+
* so recordings are redacted like every report and observation, not only
|
|
43
|
+
* after a fill.
|
|
44
|
+
*/
|
|
45
|
+
export declare function redactsRecordings(secrecy: SessionSecrecy): boolean;
|
|
43
46
|
/**
|
|
44
47
|
* Every secret value any session in this process has seen, for text the
|
|
45
48
|
* process itself emits rather than a session: a test's console output leaves
|
|
@@ -56,7 +59,7 @@ export declare const processSecrets: SecretLedger;
|
|
|
56
59
|
* handed out read-only: nothing registers into it.
|
|
57
60
|
*/
|
|
58
61
|
export declare function staticSecretLedger(secrets: ResolvedConfig['allSecrets']): Pick<SecretLedger, 'redact' | 'entries'>;
|
|
59
|
-
/** Seeds `processSecrets` with the static values of `secrets
|
|
62
|
+
/** Seeds `processSecrets` with the static values of `secrets` and makes it `serializeError`'s default redactor, so output before any session opens is covered too. */
|
|
60
63
|
export declare function registerStaticSecrets(secrets: ResolvedConfig['allSecrets']): void;
|
|
61
64
|
/**
|
|
62
65
|
* The plaintext of one configured secret, a provider's value computed fresh.
|
package/dist/run/secrecy.js
CHANGED
|
@@ -1,41 +1,44 @@
|
|
|
1
1
|
/** What one live session knows about secrets: the values to redact, and whether one reached it. */
|
|
2
2
|
import { MIN_SECRET_LENGTH, secretLength } from '../config/secrets.js';
|
|
3
|
-
import { ConfigurationError } from '../internal/errors.js';
|
|
3
|
+
import { ConfigurationError, setErrorRedactor } from '../internal/errors.js';
|
|
4
4
|
import { SecretLedger } from '../internal/redact.js';
|
|
5
5
|
import { unavailableCode } from '../secrets.js';
|
|
6
|
-
const EXPOSURE_RANK = { none: 0, engine: 1, filled: 2 };
|
|
7
6
|
/**
|
|
8
7
|
* One session's exposure level, and the one place that maps it to what each
|
|
9
|
-
* consumer
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* text only, so a page that renders it on screen is not masked. Only a fill
|
|
8
|
+
* consumer withholds. Only a fill withholds pixels (failure screenshots,
|
|
9
|
+
* `app.screenshot()`, assert evidence, model input, and a trace's
|
|
10
|
+
* screencast frames): a value that reached the app otherwise (an engine
|
|
11
|
+
* option, a URL or a `fill` the test spelled it into) is protected as text
|
|
12
|
+
* only, so a page that renders it on screen is not masked. Only a fill
|
|
15
13
|
* carries into a saved session too: the value then lives in the app's
|
|
16
|
-
* state
|
|
17
|
-
* the restoring session.
|
|
14
|
+
* state. Text is not decided here: see `redactsRecordings`.
|
|
18
15
|
*/
|
|
19
16
|
export class SecretExposure {
|
|
20
17
|
current = 'none';
|
|
21
|
-
/** Records that a secret
|
|
18
|
+
/** Records that a secret was filled into the session. */
|
|
22
19
|
raise(level) {
|
|
23
|
-
|
|
24
|
-
this.current = level;
|
|
20
|
+
this.current = level;
|
|
25
21
|
}
|
|
26
22
|
/** Whether no pixels may leave the session: no screenshot or screencast frame is taken, kept, or handed to a model. */
|
|
27
23
|
get withholdsPixels() {
|
|
28
24
|
return this.current === 'filled';
|
|
29
25
|
}
|
|
30
|
-
/** Whether a text recording of the session (a trace, a text-like download) is rewritten through the ledger before it is kept. */
|
|
31
|
-
get redactsRecordings() {
|
|
32
|
-
return this.current !== 'none';
|
|
33
|
-
}
|
|
34
26
|
/** Whether a session saved from this one carries the taint to the sessions that restore it. */
|
|
35
27
|
get carriesTaint() {
|
|
36
28
|
return this.current === 'filled';
|
|
37
29
|
}
|
|
38
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether a text recording of the session (a trace, a text-like download) is
|
|
33
|
+
* rewritten through its ledger before it is kept: whenever the ledger holds
|
|
34
|
+
* a value, filled or not. A value reaches the app in ways the runner never
|
|
35
|
+
* sees (a URL or a `fill` the test spelled it into, the app's own config),
|
|
36
|
+
* so recordings are redacted like every report and observation, not only
|
|
37
|
+
* after a fill.
|
|
38
|
+
*/
|
|
39
|
+
export function redactsRecordings(secrecy) {
|
|
40
|
+
return !secrecy.ledger.isEmpty;
|
|
41
|
+
}
|
|
39
42
|
/**
|
|
40
43
|
* Every secret value any session in this process has seen, for text the
|
|
41
44
|
* process itself emits rather than a session: a test's console output leaves
|
|
@@ -61,8 +64,9 @@ export function staticSecretLedger(secrets) {
|
|
|
61
64
|
}
|
|
62
65
|
return ledger;
|
|
63
66
|
}
|
|
64
|
-
/** Seeds `processSecrets` with the static values of `secrets
|
|
67
|
+
/** Seeds `processSecrets` with the static values of `secrets` and makes it `serializeError`'s default redactor, so output before any session opens is covered too. */
|
|
65
68
|
export function registerStaticSecrets(secrets) {
|
|
69
|
+
setErrorRedactor(processSecrets.redact);
|
|
66
70
|
for (const [name, { value }] of secrets) {
|
|
67
71
|
if (typeof value === 'string')
|
|
68
72
|
processSecrets.register(name, value);
|
package/dist/run/serial.d.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import type { TargetSession } from '../engine/surface.ts';
|
|
3
3
|
import { type SerializedError } from '../internal/errors.ts';
|
|
4
4
|
import { type RegisteredTest } from '../collect/registry.ts';
|
|
5
|
-
import { type TestTargetPair } from '../collect/select.ts';
|
|
5
|
+
import { type SkipInfo, type TestTargetPair } from '../collect/select.ts';
|
|
6
6
|
import type { ResolvedTarget } from '../config/resolve.ts';
|
|
7
7
|
import type { ArtifactStore } from '../types.ts';
|
|
8
8
|
import type { AttemptContext, ClosingRecord, SessionClose, SessionPlan } from './execute.ts';
|
|
9
9
|
import type { ArtifactSink } from './fixtures.ts';
|
|
10
10
|
import type { StepRecord } from './steps.ts';
|
|
11
11
|
import { type FileRef, type Realm, RealmManager } from './realm.ts';
|
|
12
|
-
import type { AttemptRecord, ResultRecord, SerialGroupRecord } from './records.ts';
|
|
12
|
+
import type { AttemptRecord, ResultRecord, SerialAttemptRecord, SerialGroupRecord, SerialMemberRecord } from './records.ts';
|
|
13
13
|
/**
|
|
14
14
|
* One shared session for a serial-group attempt: members preserve app state,
|
|
15
15
|
* so a screen an earlier member reached stays current for later members.
|
|
@@ -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;
|
|
@@ -54,6 +56,77 @@ export interface SerialHost {
|
|
|
54
56
|
* steps, duration, and error from the group it has already seen.
|
|
55
57
|
*/
|
|
56
58
|
emitSerialGroup(group: SerialGroupRecord): void;
|
|
59
|
+
/**
|
|
60
|
+
* Streams one member that finished in the group attempt running now, so a
|
|
61
|
+
* worker that dies before the group ends does not take it along.
|
|
62
|
+
*/
|
|
63
|
+
serialMemberFinished(groupId: string, attempt: SerialAttemptStart, member: SerialMemberRecord): void;
|
|
64
|
+
/** Streams one finished group attempt, for the same reason. */
|
|
65
|
+
serialAttemptFinished(groupId: string, run: SerialAttemptRun): void;
|
|
57
66
|
}
|
|
67
|
+
/** The identity of a group attempt, known from its start. */
|
|
68
|
+
export type SerialAttemptStart = Pick<SerialAttemptRecord, 'id' | 'index' | 'startedAt'>;
|
|
69
|
+
/** One group attempt as it ended, and whether it got as far as its members. */
|
|
70
|
+
export interface SerialAttemptRun {
|
|
71
|
+
readonly record: SerialAttemptRecord;
|
|
72
|
+
/**
|
|
73
|
+
* False when the attempt stopped before any member ran (its file would not
|
|
74
|
+
* load, its session would not launch). Such a retry keeps the members'
|
|
75
|
+
* verdicts from the attempt before it.
|
|
76
|
+
*/
|
|
77
|
+
readonly reachedMembers: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** The report id of one serial group variant: a group runs once per agent and per repeat. */
|
|
80
|
+
export declare function serialGroupId(serialId: string, targetName: string, agent: string, repeat: number): string;
|
|
58
81
|
/** Runs one serial unit to completion and emits every member result. */
|
|
59
82
|
export declare function runSerialUnit(host: SerialHost, members: readonly TestTargetPair[], file: FileRef): Promise<SerialGroupRecord>;
|
|
83
|
+
/** A serial group decided without its worker's word: the record and its members' results. */
|
|
84
|
+
interface SettledSerialGroup {
|
|
85
|
+
readonly group: SerialGroupRecord;
|
|
86
|
+
readonly results: readonly ResultRecord[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* What the runner heard of one serial group a worker is running: the
|
|
90
|
+
* attempts it finished, and the members finished so far in the attempt
|
|
91
|
+
* running now. When the worker dies before reporting the group, this is
|
|
92
|
+
* what the report keeps.
|
|
93
|
+
*/
|
|
94
|
+
export declare class SerialGroupProgress {
|
|
95
|
+
private readonly finished;
|
|
96
|
+
private current;
|
|
97
|
+
private currentMembers;
|
|
98
|
+
/** The member whose body is running now, by test id, until its record arrives. */
|
|
99
|
+
private running;
|
|
100
|
+
/** A member of the group announced its start. */
|
|
101
|
+
memberStarted(testId: string): void;
|
|
102
|
+
/** A member finished in the attempt running now. */
|
|
103
|
+
memberFinished(attempt: SerialAttemptStart, member: SerialMemberRecord): void;
|
|
104
|
+
/** A group attempt finished; the next one starts empty. */
|
|
105
|
+
attemptFinished(run: SerialAttemptRun): void;
|
|
106
|
+
/**
|
|
107
|
+
* The group after its worker died with `error`, or undefined when the
|
|
108
|
+
* runner heard nothing of it running. The finished attempts stand, and the
|
|
109
|
+
* attempt in flight fails: the member whose body was running ends `status`
|
|
110
|
+
* with `error` (`timed-out` when the watchdog killed a worker that hung in
|
|
111
|
+
* it) and the ones after it skip. A crash between members (a hook,
|
|
112
|
+
* the session closing) fails the attempt and leaves its finished members
|
|
113
|
+
* as they were; one in a retry before any member ran fails the first, as a
|
|
114
|
+
* launch failure does. A crash after the last attempt, when no retry
|
|
115
|
+
* follows it, is recorded on that attempt.
|
|
116
|
+
*/
|
|
117
|
+
crashed(members: readonly TestTargetPair[], target: ResolvedTarget, error: SerializedError, status: 'failed' | 'timed-out'): SettledSerialGroup | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* The group after the run's interrupt stopped its worker, or undefined
|
|
120
|
+
* when the runner heard nothing of it running. The finished attempts
|
|
121
|
+
* stand, and an attempt in flight is interrupted: the members it finished
|
|
122
|
+
* keep their records and the rest skip with `skip`. The verdict is the one
|
|
123
|
+
* those attempts reach, as for a retry the interrupt cut short.
|
|
124
|
+
*/
|
|
125
|
+
interrupted(members: readonly TestTargetPair[], target: ResolvedTarget, skip: SkipInfo): SettledSerialGroup | undefined;
|
|
126
|
+
/** Whether a member of the attempt in flight started or finished. */
|
|
127
|
+
private get inFlight();
|
|
128
|
+
/** The record of the attempt in flight as it stood, with the members it finished. */
|
|
129
|
+
private attemptInFlight;
|
|
130
|
+
private settled;
|
|
131
|
+
}
|
|
132
|
+
export {};
|