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
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/** Child-process `UnitRunner`: spawn, messaging, exit tracking. */
|
|
2
2
|
import { fork } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { createRequire } from 'node:module';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { WARNED_TSCONFIGS_ENV, warnedTsconfigs } from '../../config/tsconfig.js';
|
|
6
6
|
import { InfrastructureError } from '../../internal/errors.js';
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the worker entry for both src (vitest, .ts) and dist (.js)
|
|
9
|
+
* layouts. From source, e2e's own loader is registered before the entry
|
|
10
|
+
* loads (`config/register.ts`): Node.js strips types but does not compile
|
|
11
|
+
* the rest of TypeScript.
|
|
12
|
+
*/
|
|
8
13
|
function resolveEntry() {
|
|
9
14
|
const js = fileURLToPath(new URL('./entry.js', import.meta.url));
|
|
10
15
|
if (existsSync(js))
|
|
11
16
|
return { path: js, execArgv: [] };
|
|
12
17
|
const ts = fileURLToPath(new URL('./entry.ts', import.meta.url));
|
|
13
18
|
if (existsSync(ts)) {
|
|
14
|
-
|
|
15
|
-
return { path: ts, execArgv: ['--import', require.resolve('tsx')] };
|
|
19
|
+
return { path: ts, execArgv: ['--import', new URL('../../config/register.ts', import.meta.url).href] };
|
|
16
20
|
}
|
|
17
21
|
throw new InfrastructureError('WORKER_ENTRY_MISSING', 'e2e worker entry module not found');
|
|
18
22
|
}
|
|
@@ -32,7 +36,8 @@ class ChildProcessRunner {
|
|
|
32
36
|
cwd: spawn.projectRoot,
|
|
33
37
|
execArgv: entry.execArgv,
|
|
34
38
|
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
35
|
-
|
|
39
|
+
// Collecting read the project's tsconfig.json files first; the worker warns only about ones it reaches on its own.
|
|
40
|
+
env: { ...spawn.env, [WARNED_TSCONFIGS_ENV]: warnedTsconfigs() },
|
|
36
41
|
});
|
|
37
42
|
this.child.on('message', (message) => events.onMessage(message));
|
|
38
43
|
this.exit = new Promise((resolve) => {
|
|
@@ -12,7 +12,8 @@ import type { CliOverrides, PortAssignments, ResolvedTarget } from '../../config
|
|
|
12
12
|
import type { AiTraceSnapshot } from '../../internal/ai-trace.ts';
|
|
13
13
|
import type { DebugSnapshot } from '../../internal/debug.ts';
|
|
14
14
|
import type { SerializedError } from '../../internal/errors.ts';
|
|
15
|
-
import type { ResultRecord, RunError, SerialGroupRecord, WireResultRecord } from '../records.ts';
|
|
15
|
+
import type { AttemptRecord, ResultRecord, RunError, SerialGroupRecord, SerialMemberRecord, WireResultRecord } from '../records.ts';
|
|
16
|
+
import type { SerialAttemptRun, SerialAttemptStart } from '../serial.ts';
|
|
16
17
|
import type { StepProgress } from '../steps.ts';
|
|
17
18
|
/** One runnable pair on the wire; the worker resolves the test function. */
|
|
18
19
|
export interface WirePair {
|
|
@@ -56,6 +57,8 @@ export interface WorkerBootstrap {
|
|
|
56
57
|
readonly workerSlot: number;
|
|
57
58
|
readonly runId: string;
|
|
58
59
|
readonly artifactsRoot: string;
|
|
60
|
+
/** See `TargetExecutorOptions.rerunDir`. */
|
|
61
|
+
readonly rerunDir: string | undefined;
|
|
59
62
|
readonly headed: boolean;
|
|
60
63
|
readonly sessionsRoot: string;
|
|
61
64
|
/** Per-run AES key; transferred only over this channel, never disk or env. */
|
|
@@ -105,7 +108,15 @@ export interface ShutdownMessage {
|
|
|
105
108
|
export interface TerminateMessage {
|
|
106
109
|
readonly type: 'terminate';
|
|
107
110
|
}
|
|
108
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Asks the worker to answer with `pong` from its event loop. The scheduler
|
|
113
|
+
* sends it only once an attempt is past its test timeout, to tell a worker
|
|
114
|
+
* still tearing the attempt down from one whose event loop is blocked.
|
|
115
|
+
*/
|
|
116
|
+
export interface PingMessage {
|
|
117
|
+
readonly type: 'ping';
|
|
118
|
+
}
|
|
119
|
+
export type MainToWorker = RunUnitMessage | InterruptMessage | ShutdownMessage | TerminateMessage | PingMessage;
|
|
109
120
|
export interface ReadyMessage {
|
|
110
121
|
readonly type: 'ready';
|
|
111
122
|
}
|
|
@@ -164,6 +175,35 @@ export interface NoticeMessage {
|
|
|
164
175
|
readonly type: 'notice';
|
|
165
176
|
readonly message: string;
|
|
166
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* An attempt's test timeout started: its `beforeEach` hooks and body run
|
|
180
|
+
* now. The worker enforces the timeout with its own timers, which a body
|
|
181
|
+
* that blocks the event loop never lets fire, so the scheduler keeps the
|
|
182
|
+
* deadline too; see `SchedulerWorker.watch`.
|
|
183
|
+
*/
|
|
184
|
+
export interface AttemptDeadlineMessage {
|
|
185
|
+
readonly type: 'attempt-deadline';
|
|
186
|
+
readonly testId: string;
|
|
187
|
+
readonly agent: string;
|
|
188
|
+
readonly repeat: number;
|
|
189
|
+
/** The attempt's index among the pair's attempts. */
|
|
190
|
+
readonly attempt: number;
|
|
191
|
+
/** The attempt's record id and start, which a watchdog kill reports it with. */
|
|
192
|
+
readonly attemptId: string;
|
|
193
|
+
readonly startedAt: string;
|
|
194
|
+
/** The test's resolved `timeout`. */
|
|
195
|
+
readonly timeoutMs: number;
|
|
196
|
+
/** How long past the timeout the worker may go without answering a `ping`: the cleanup budget. */
|
|
197
|
+
readonly graceMs: number;
|
|
198
|
+
}
|
|
199
|
+
/** The attempt the last `attempt-deadline` announced has ended, verdict and cleanup included. */
|
|
200
|
+
export interface AttemptEndMessage {
|
|
201
|
+
readonly type: 'attempt-end';
|
|
202
|
+
}
|
|
203
|
+
/** The answer to `ping`. */
|
|
204
|
+
export interface PongMessage {
|
|
205
|
+
readonly type: 'pong';
|
|
206
|
+
}
|
|
167
207
|
export interface ResultMessage {
|
|
168
208
|
readonly type: 'result';
|
|
169
209
|
readonly result: WireResultRecord;
|
|
@@ -172,6 +212,48 @@ export interface SerialGroupMessage {
|
|
|
172
212
|
readonly type: 'serial-group';
|
|
173
213
|
readonly group: SerialGroupRecord;
|
|
174
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* An attempt of an ordinary or setup pair begins, its realm and `beforeAll`
|
|
217
|
+
* hooks included. With `attempt`, it tells a crash during an attempt from
|
|
218
|
+
* one between attempts (an `afterAll` after the last one): only the first
|
|
219
|
+
* is charged a new attempt, with this index.
|
|
220
|
+
*/
|
|
221
|
+
export interface AttemptStartMessage {
|
|
222
|
+
readonly type: 'attempt-start';
|
|
223
|
+
readonly testId: string;
|
|
224
|
+
readonly agent: string;
|
|
225
|
+
readonly repeat: number;
|
|
226
|
+
readonly index: number;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* One finished attempt of an ordinary or setup pair. A pair's result waits
|
|
230
|
+
* for its last attempt, so each attempt also goes out as it ends: a worker
|
|
231
|
+
* that dies during a retry must not take the attempts before it along. The
|
|
232
|
+
* result still carries every attempt; the scheduler keeps these only until
|
|
233
|
+
* it arrives.
|
|
234
|
+
*/
|
|
235
|
+
export interface AttemptMessage {
|
|
236
|
+
readonly type: 'attempt';
|
|
237
|
+
readonly testId: string;
|
|
238
|
+
readonly agent: string;
|
|
239
|
+
readonly repeat: number;
|
|
240
|
+
readonly attempt: AttemptRecord;
|
|
241
|
+
}
|
|
242
|
+
/** One member that finished in the serial group attempt running now; see `AttemptMessage`. */
|
|
243
|
+
export interface SerialMemberMessage {
|
|
244
|
+
readonly type: 'serial-member';
|
|
245
|
+
/** The group's report id (`SerialGroupRecord.id`). */
|
|
246
|
+
readonly groupId: string;
|
|
247
|
+
readonly attempt: SerialAttemptStart;
|
|
248
|
+
readonly member: SerialMemberRecord;
|
|
249
|
+
}
|
|
250
|
+
/** One finished serial group attempt; see `AttemptMessage`. */
|
|
251
|
+
export interface SerialAttemptMessage {
|
|
252
|
+
readonly type: 'serial-attempt';
|
|
253
|
+
/** The group's report id (`SerialGroupRecord.id`). */
|
|
254
|
+
readonly groupId: string;
|
|
255
|
+
readonly run: SerialAttemptRun;
|
|
256
|
+
}
|
|
175
257
|
export interface UnitDoneMessage {
|
|
176
258
|
readonly type: 'unit-done';
|
|
177
259
|
readonly unitId: string;
|
|
@@ -206,6 +288,6 @@ export interface RunAbortMessage {
|
|
|
206
288
|
readonly type: 'run-abort';
|
|
207
289
|
readonly error: SerializedError;
|
|
208
290
|
}
|
|
209
|
-
export type WorkerToMain = ReadyMessage | PairStartMessage | ProgressMessage | OutputMessage | NoticeMessage | ResultMessage | SerialGroupMessage | UnitDoneMessage | ShutdownDoneMessage | FatalMessage | RunAbortMessage;
|
|
291
|
+
export type WorkerToMain = ReadyMessage | PairStartMessage | ProgressMessage | AttemptDeadlineMessage | AttemptEndMessage | PongMessage | OutputMessage | NoticeMessage | ResultMessage | SerialGroupMessage | AttemptStartMessage | AttemptMessage | SerialMemberMessage | SerialAttemptMessage | UnitDoneMessage | ShutdownDoneMessage | FatalMessage | RunAbortMessage;
|
|
210
292
|
/** Reattaches the scheduler's resolved target to a wire result. */
|
|
211
293
|
export declare function decodeResult(wire: WireResultRecord, target: ResolvedTarget): ResultRecord;
|
|
@@ -35,6 +35,8 @@ export interface TargetWorkerDeps {
|
|
|
35
35
|
readonly sessionStore: SessionStore;
|
|
36
36
|
readonly runId: string;
|
|
37
37
|
readonly artifactsRoot: string;
|
|
38
|
+
/** See `TargetExecutorOptions.rerunDir`. */
|
|
39
|
+
readonly rerunDir: string | undefined;
|
|
38
40
|
readonly headed: boolean;
|
|
39
41
|
/** See `TargetExecutorOptions.workerSlot`. */
|
|
40
42
|
readonly workerSlot: number;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* in-process) drive this class, so unit execution, interrupt handling, and
|
|
5
5
|
* run-error forwarding exist exactly once.
|
|
6
6
|
*/
|
|
7
|
+
import inspector from 'node:inspector';
|
|
7
8
|
import { DebugTrace } from '../../internal/debug.js';
|
|
8
9
|
import { TargetExecutor } from '../execute.js';
|
|
9
10
|
import { encodeResult } from '../records.js';
|
|
@@ -46,6 +47,7 @@ export class TargetWorker {
|
|
|
46
47
|
target: deps.target,
|
|
47
48
|
runId: deps.runId,
|
|
48
49
|
artifactsRoot: deps.artifactsRoot,
|
|
50
|
+
rerunDir: deps.rerunDir,
|
|
49
51
|
sessionStore: deps.sessionStore,
|
|
50
52
|
headed: deps.headed,
|
|
51
53
|
workerSlot: deps.workerSlot,
|
|
@@ -65,6 +67,10 @@ export class TargetWorker {
|
|
|
65
67
|
this.countFailure();
|
|
66
68
|
},
|
|
67
69
|
onSerialGroup: (group) => this.host.emit({ type: 'serial-group', group }),
|
|
70
|
+
onAttemptStart: (pair, index) => this.host.emit({ type: 'attempt-start', testId: pair.test.id, agent: pair.agent, repeat: pair.repeat, index }),
|
|
71
|
+
onAttempt: (pair, attempt) => this.host.emit({ type: 'attempt', testId: pair.test.id, agent: pair.agent, repeat: pair.repeat, attempt }),
|
|
72
|
+
onSerialMember: (groupId, attempt, member) => this.host.emit({ type: 'serial-member', groupId, attempt, member }),
|
|
73
|
+
onSerialAttempt: (groupId, run) => this.host.emit({ type: 'serial-attempt', groupId, run }),
|
|
68
74
|
onPairStart: (pair) => {
|
|
69
75
|
this.inFlight = { testId: pair.test.id, agent: pair.agent, repeat: pair.repeat };
|
|
70
76
|
this.host.emit({
|
|
@@ -77,6 +83,23 @@ export class TargetWorker {
|
|
|
77
83
|
serialId: pair.test.serialId,
|
|
78
84
|
});
|
|
79
85
|
},
|
|
86
|
+
onAttemptDeadline: (pair, attempt) => {
|
|
87
|
+
// A worker paused at a breakpoint looks blocked; with a debugger attached nothing watches its attempts.
|
|
88
|
+
if (inspector.url() !== undefined)
|
|
89
|
+
return;
|
|
90
|
+
this.host.emit({
|
|
91
|
+
type: 'attempt-deadline',
|
|
92
|
+
testId: pair.test.id,
|
|
93
|
+
agent: pair.agent,
|
|
94
|
+
repeat: pair.repeat,
|
|
95
|
+
attempt: attempt.index,
|
|
96
|
+
attemptId: attempt.id,
|
|
97
|
+
startedAt: attempt.startedAt,
|
|
98
|
+
timeoutMs: pair.options.timeout,
|
|
99
|
+
graceMs: deps.config.cleanupTimeout,
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
onAttemptEnd: () => this.host.emit({ type: 'attempt-end' }),
|
|
80
103
|
onProgress: (pair, progress) => this.host.emit({ type: 'progress', testId: pair.test.id, agent: pair.agent, repeat: pair.repeat, progress }),
|
|
81
104
|
onNotice: (message) => this.host.emit({ type: 'notice', message }),
|
|
82
105
|
onRunAbort: (runError) => {
|
|
@@ -112,6 +135,12 @@ export class TargetWorker {
|
|
|
112
135
|
case 'terminate':
|
|
113
136
|
this.terminate();
|
|
114
137
|
return;
|
|
138
|
+
case 'ping':
|
|
139
|
+
// Answered now, not queued behind the running unit: the answer is the proof the event loop turns.
|
|
140
|
+
this.host.emit({ type: 'pong' });
|
|
141
|
+
return;
|
|
142
|
+
default:
|
|
143
|
+
message;
|
|
115
144
|
}
|
|
116
145
|
}
|
|
117
146
|
/**
|
|
@@ -42,7 +42,8 @@ export declare function fleetName(env: NodeJS.ProcessEnv): string | null;
|
|
|
42
42
|
* The id the environment states for itself, standing in for a machine that
|
|
43
43
|
* has no preferences file: a fleet by its name, else the CI vendor when `CI`
|
|
44
44
|
* is set. Undefined on a person's machine, where the random per-machine id
|
|
45
|
-
* is the unit
|
|
45
|
+
* is the unit: a vendor marker without `CI` is a shell on a runner, and `CI`
|
|
46
|
+
* with a coding agent but no vendor is the agent's shell on a laptop.
|
|
46
47
|
*/
|
|
47
48
|
export declare function statedIdentity(env: NodeJS.ProcessEnv): string | undefined;
|
|
48
49
|
export declare function collectEnvironment(options: {
|
|
@@ -30,6 +30,10 @@ const CI_VENDORS = [
|
|
|
30
30
|
['SEMAPHORE', 'semaphore'],
|
|
31
31
|
['APPVEYOR', 'appveyor'],
|
|
32
32
|
['WOODPECKER', 'woodpecker'],
|
|
33
|
+
['EAS_BUILD', 'eas'],
|
|
34
|
+
['BITRISE_IO', 'bitrise'],
|
|
35
|
+
['CM_BUILD_ID', 'codemagic'],
|
|
36
|
+
['CI_XCODE_CLOUD', 'xcode-cloud'],
|
|
33
37
|
];
|
|
34
38
|
/**
|
|
35
39
|
* Environment variables coding agents set in the shells they spawn. The
|
|
@@ -99,13 +103,19 @@ export function fleetName(env) {
|
|
|
99
103
|
* The id the environment states for itself, standing in for a machine that
|
|
100
104
|
* has no preferences file: a fleet by its name, else the CI vendor when `CI`
|
|
101
105
|
* is set. Undefined on a person's machine, where the random per-machine id
|
|
102
|
-
* is the unit
|
|
106
|
+
* is the unit: a vendor marker without `CI` is a shell on a runner, and `CI`
|
|
107
|
+
* with a coding agent but no vendor is the agent's shell on a laptop.
|
|
103
108
|
*/
|
|
104
109
|
export function statedIdentity(env) {
|
|
105
110
|
const fleet = fleetName(env);
|
|
106
111
|
if (fleet !== null)
|
|
107
112
|
return `fleet:${fleet}`;
|
|
108
|
-
|
|
113
|
+
if (!isCiMode(env))
|
|
114
|
+
return undefined;
|
|
115
|
+
const vendor = firstMarker(env, CI_VENDORS);
|
|
116
|
+
if (vendor === undefined && firstMarker(env, CODING_AGENTS) !== undefined)
|
|
117
|
+
return undefined;
|
|
118
|
+
return `ci:${vendor ?? 'unknown'}`;
|
|
109
119
|
}
|
|
110
120
|
/** The runtime executing this process: Bun and Deno each announce themselves in `process.versions`. */
|
|
111
121
|
function runtime(versions) {
|
package/dist/telemetry/events.js
CHANGED
|
@@ -205,6 +205,7 @@ export function runCompletedEvent(report, context) {
|
|
|
205
205
|
tests_executed: run.summary.executed,
|
|
206
206
|
tests_passed: run.summary.passed,
|
|
207
207
|
tests_failed: run.summary.failed,
|
|
208
|
+
tests_interrupted: run.summary.interrupted,
|
|
208
209
|
tests_flaky: run.summary.flaky,
|
|
209
210
|
tests_skipped: run.summary.skipped,
|
|
210
211
|
attempts_total: attempts.reduce((total, count) => total + count, 0),
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* commit, which every clone shares and no one can produce without the
|
|
9
9
|
* repository itself; a commit hash carries no name, path, or remote. A
|
|
10
10
|
* shallow clone has no root to offer: its boundary commits pose as roots and
|
|
11
|
-
* move with every fetch, so it
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* In CI there is no salt, so a runner without a full history has no id: its
|
|
15
|
-
* working directory is not a project.
|
|
11
|
+
* move with every fetch, so it has no id, like a directory outside git: a
|
|
12
|
+
* path is the same project on one machine only, so it cannot be told apart
|
|
13
|
+
* from a scratch directory nobody returns to.
|
|
16
14
|
*/
|
|
17
|
-
|
|
15
|
+
/** The hashed root commit of the repository at `projectRoot`; undefined outside git and in a shallow clone. */
|
|
16
|
+
export declare function anonymousProjectId(projectRoot: string): Promise<string | undefined>;
|
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
* commit, which every clone shares and no one can produce without the
|
|
9
9
|
* repository itself; a commit hash carries no name, path, or remote. A
|
|
10
10
|
* shallow clone has no root to offer: its boundary commits pose as roots and
|
|
11
|
-
* move with every fetch, so it
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* In CI there is no salt, so a runner without a full history has no id: its
|
|
15
|
-
* working directory is not a project.
|
|
11
|
+
* move with every fetch, so it has no id, like a directory outside git: a
|
|
12
|
+
* path is the same project on one machine only, so it cannot be told apart
|
|
13
|
+
* from a scratch directory nobody returns to.
|
|
16
14
|
*/
|
|
17
15
|
import { COMMIT_HASH, git } from '../internal/git.js';
|
|
18
16
|
import { sha256Hex } from '../internal/ids.js';
|
|
@@ -31,11 +29,8 @@ async function rootCommit(cwd) {
|
|
|
31
29
|
.filter((line) => COMMIT_HASH.test(line))
|
|
32
30
|
.toSorted()[0];
|
|
33
31
|
}
|
|
34
|
-
|
|
32
|
+
/** The hashed root commit of the repository at `projectRoot`; undefined outside git and in a shallow clone. */
|
|
33
|
+
export async function anonymousProjectId(projectRoot) {
|
|
35
34
|
const commit = await rootCommit(projectRoot);
|
|
36
|
-
|
|
37
|
-
return sha256Hex(`git\n${commit}`);
|
|
38
|
-
if (salt === undefined)
|
|
39
|
-
return undefined;
|
|
40
|
-
return sha256Hex(`path\n${salt}\n${projectRoot}`);
|
|
35
|
+
return commit === undefined ? undefined : sha256Hex(`git\n${commit}`);
|
|
41
36
|
}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* The user-level telemetry preferences: `~/.config/e2e/telemetry.json`
|
|
3
3
|
* (`$XDG_CONFIG_HOME/e2e` when set, `%APPDATA%\e2e` on Windows). The file
|
|
4
4
|
* holds the opt-out, the random id every event from this machine is
|
|
5
|
-
* attributed to,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* attributed to, and which notice was shown when. Nothing in it names the
|
|
6
|
+
* person or the machine: the id is random bytes, generated here and never
|
|
7
|
+
* derived. A complete file also holds a random salt that nothing is hashed
|
|
8
|
+
* with anymore: older versions hashed paths with it and replace the id of a
|
|
9
|
+
* file without one, and a machine often has several versions installed.
|
|
8
10
|
*
|
|
9
11
|
* The store fails closed. `open` completes the file in one write when the id
|
|
10
12
|
* or the salt is missing, and when that write cannot happen there is no
|
|
@@ -38,8 +40,6 @@ export declare class TelemetryStore {
|
|
|
38
40
|
get enabled(): boolean;
|
|
39
41
|
/** The random per-machine id every event from this machine is attributed to. */
|
|
40
42
|
get anonymousId(): string;
|
|
41
|
-
/** The local salt that keeps a hashed project path unrecoverable; never leaves the machine. */
|
|
42
|
-
get pathSalt(): string;
|
|
43
43
|
/** Whole days since the ids were generated; undefined for a file from before the time was recorded. */
|
|
44
44
|
ageDays(now?: number): number | undefined;
|
|
45
45
|
/** Saves the choice; false when the file could not be written and the choice stayed in memory. */
|
package/dist/telemetry/store.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* The user-level telemetry preferences: `~/.config/e2e/telemetry.json`
|
|
3
3
|
* (`$XDG_CONFIG_HOME/e2e` when set, `%APPDATA%\e2e` on Windows). The file
|
|
4
4
|
* holds the opt-out, the random id every event from this machine is
|
|
5
|
-
* attributed to,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* attributed to, and which notice was shown when. Nothing in it names the
|
|
6
|
+
* person or the machine: the id is random bytes, generated here and never
|
|
7
|
+
* derived. A complete file also holds a random salt that nothing is hashed
|
|
8
|
+
* with anymore: older versions hashed paths with it and replace the id of a
|
|
9
|
+
* file without one, and a machine often has several versions installed.
|
|
8
10
|
*
|
|
9
11
|
* The store fails closed. `open` completes the file in one write when the id
|
|
10
12
|
* or the salt is missing, and when that write cannot happen there is no
|
|
@@ -113,10 +115,6 @@ export class TelemetryStore {
|
|
|
113
115
|
get anonymousId() {
|
|
114
116
|
return this.preferences.anonymousId;
|
|
115
117
|
}
|
|
116
|
-
/** The local salt that keeps a hashed project path unrecoverable; never leaves the machine. */
|
|
117
|
-
get pathSalt() {
|
|
118
|
-
return this.preferences.salt;
|
|
119
|
-
}
|
|
120
118
|
/** Whole days since the ids were generated; undefined for a file from before the time was recorded. */
|
|
121
119
|
ageDays(now = Date.now()) {
|
|
122
120
|
const created = this.preferences.createdAt;
|
|
@@ -43,7 +43,7 @@ export interface TelemetryOptions {
|
|
|
43
43
|
readonly fetch?: typeof fetch;
|
|
44
44
|
/** Where the notice and the debug output go; stderr when absent. */
|
|
45
45
|
readonly write?: (text: string) => void;
|
|
46
|
-
/** Resolves the anonymous project id;
|
|
46
|
+
/** Resolves the anonymous project id; the hashed root commit when absent. */
|
|
47
47
|
readonly projectId?: typeof anonymousProjectId;
|
|
48
48
|
}
|
|
49
49
|
export declare class Telemetry {
|
|
@@ -114,8 +114,7 @@ export declare class Telemetry {
|
|
|
114
114
|
record(event: TelemetryEvent): void;
|
|
115
115
|
/**
|
|
116
116
|
* Git is asked for the project id now, while the command runs, so the
|
|
117
|
-
* flush at the end waits on the network alone.
|
|
118
|
-
* store, so a path outside git has nothing to salt it and yields no id.
|
|
117
|
+
* flush at the end waits on the network alone.
|
|
119
118
|
*/
|
|
120
119
|
private lookUpProject;
|
|
121
120
|
/** The session event, first in the batch; absent for an invocation that never reached a command. */
|
|
@@ -187,12 +187,10 @@ export class Telemetry {
|
|
|
187
187
|
}
|
|
188
188
|
/**
|
|
189
189
|
* Git is asked for the project id now, while the command runs, so the
|
|
190
|
-
* flush at the end waits on the network alone.
|
|
191
|
-
* store, so a path outside git has nothing to salt it and yields no id.
|
|
190
|
+
* flush at the end waits on the network alone.
|
|
192
191
|
*/
|
|
193
192
|
lookUpProject() {
|
|
194
|
-
|
|
195
|
-
this.project ??= this.projectId(this.cwd, store?.pathSalt);
|
|
193
|
+
this.project ??= this.projectId(this.cwd);
|
|
196
194
|
}
|
|
197
195
|
/** The session event, first in the batch; absent for an invocation that never reached a command. */
|
|
198
196
|
sessionEvent() {
|
package/dist/types.d.ts
CHANGED
|
@@ -198,7 +198,7 @@ export interface TextMatchOptions {
|
|
|
198
198
|
/**
|
|
199
199
|
* Role query options. A role query never matches a node hidden from the
|
|
200
200
|
* accessibility tree, on every engine; `visible` (inherited) is the one knob
|
|
201
|
-
* that narrows the other query kinds the
|
|
201
|
+
* that narrows the other query kinds, to the nodes `toBeVisible()` accepts.
|
|
202
202
|
*/
|
|
203
203
|
export interface RoleOptions extends TextMatchOptions {
|
|
204
204
|
/** Accessible name filter. */
|
|
@@ -265,6 +265,7 @@ export interface SwipeOptions {
|
|
|
265
265
|
momentum?: Momentum;
|
|
266
266
|
from?: never;
|
|
267
267
|
to?: never;
|
|
268
|
+
duration?: never;
|
|
268
269
|
}
|
|
269
270
|
/** A swipe along a path between two viewport points: a touch swipe on a device, a pointer drag on a document platform. */
|
|
270
271
|
export interface SwipePathOptions {
|
|
@@ -272,6 +273,8 @@ export interface SwipePathOptions {
|
|
|
272
273
|
from: Point;
|
|
273
274
|
/** Where it lifts. */
|
|
274
275
|
to: Point;
|
|
276
|
+
/** Gesture duration in milliseconds; omitted = the engine's default. */
|
|
277
|
+
duration?: number;
|
|
275
278
|
direction?: never;
|
|
276
279
|
momentum?: never;
|
|
277
280
|
}
|
|
@@ -377,9 +380,13 @@ export interface Locator extends Screen {
|
|
|
377
380
|
all(): Promise<Locator[]>;
|
|
378
381
|
/** Reads the normalized text of every current match, without auto-waiting; empty when nothing matches. */
|
|
379
382
|
allTextContents(): Promise<string[]>;
|
|
380
|
-
/**
|
|
383
|
+
/**
|
|
384
|
+
* Waits for the requested locator state, `visible` by default: `attached`
|
|
385
|
+
* for one match, visible or not; `detached` for none; `hidden` for none or
|
|
386
|
+
* a hidden one.
|
|
387
|
+
*/
|
|
381
388
|
waitFor(options?: {
|
|
382
|
-
state?: 'visible' | 'hidden';
|
|
389
|
+
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
|
383
390
|
timeout?: number;
|
|
384
391
|
}): Promise<void>;
|
|
385
392
|
/** Adds deterministic locator filters. */
|
|
@@ -1024,9 +1031,9 @@ export interface StoredArtifact {
|
|
|
1024
1031
|
readonly path: string;
|
|
1025
1032
|
/**
|
|
1026
1033
|
* How much of the file the runner masked, as the report records it. A
|
|
1027
|
-
* `download` is `incomplete` unless the runner
|
|
1028
|
-
* secret
|
|
1029
|
-
* this rather than the kind.
|
|
1034
|
+
* `download` is `incomplete` unless the runner ran it as text through the
|
|
1035
|
+
* session's secret values; a store that exports only what the runner
|
|
1036
|
+
* vouches for reads this rather than the kind.
|
|
1030
1037
|
*/
|
|
1031
1038
|
readonly redaction: 'complete' | 'not-required' | 'incomplete';
|
|
1032
1039
|
readonly runId: string;
|
|
@@ -1250,6 +1257,8 @@ export interface E2EConfig {
|
|
|
1250
1257
|
cleanupTimeout?: number;
|
|
1251
1258
|
/** Retries per test, 0 through 10; default 1 in CI, else 0. */
|
|
1252
1259
|
retries?: number;
|
|
1260
|
+
/** Fail the run when a test skips itself after a soft failure or an earlier failed attempt; default false. */
|
|
1261
|
+
failOnSkippedFailure?: boolean;
|
|
1253
1262
|
/** Parallel workers, 1 through 1024; default 1 in CI, else half the cores. An engine may cap it lower. */
|
|
1254
1263
|
workers?: number;
|
|
1255
1264
|
/** `{ store }` hands every artifact to a host store as it is produced. */
|
package/docs/agent-steps.mdx
CHANGED
package/docs/agents.mdx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Agents and personas
|
|
3
3
|
description: Configure several agents in one suite, pin tests to them, and run one flow as each persona.
|
|
4
|
+
icon: "users"
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
Define named agents in `agents` to give tests different models, instructions,
|
|
@@ -109,9 +110,12 @@ agent if the problem affects several tests:
|
|
|
109
110
|
5. Compare models or `providerOptions` by running the same tests with
|
|
110
111
|
`--agent <name>`.
|
|
111
112
|
|
|
112
|
-
Named agents can use the [replay cache](/cache).
|
|
113
|
-
|
|
114
|
-
and
|
|
113
|
+
Named agents can use the [replay cache](/cache). Each agent records its
|
|
114
|
+
own entries, keyed by its name and its context, so a test that runs as
|
|
115
|
+
`buyer` and as `admin` never replays one persona's steps for the other, and
|
|
116
|
+
changing `agentContext` records again. Custom tools and executors have their
|
|
117
|
+
own replay constraints, described under [project tools](/tools) and
|
|
118
|
+
[custom executors](/executors#replay-cache).
|
|
115
119
|
|
|
116
120
|
## Pick an agent per run
|
|
117
121
|
|
package/docs/api-testing.mdx
CHANGED
package/docs/authentication.mdx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Signing in
|
|
3
3
|
description: Sign in once, reuse the session across tests, and pass secrets without exposing them to the model.
|
|
4
|
+
icon: "key"
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
Sign in once in a setup test and save a named session. Other tests restore
|
|
@@ -164,7 +165,7 @@ runner keeps it away from test code and the model altogether.
|
|
|
164
165
|
|
|
165
166
|
A `Secret` has no `.value`. Three sinks accept one: the two below in a
|
|
166
167
|
test, and an engine option that declares it, as
|
|
167
|
-
[`basicAuth`](/
|
|
168
|
+
[`basicAuth`](/web#protected-previews) does:
|
|
168
169
|
|
|
169
170
|
<Tabs>
|
|
170
171
|
<Tab title="Deterministic">
|
|
@@ -211,7 +212,9 @@ export default {
|
|
|
211
212
|
```
|
|
212
213
|
|
|
213
214
|
`E2E_SECRET_<NAME>` overrides an entry per run, with the same uppercasing rule
|
|
214
|
-
as credentials, so a CI job can supply the value without touching the config
|
|
215
|
+
as credentials, so a CI job can supply the value without touching the config.
|
|
216
|
+
Two entries that map to one variable, like `stripe-key` and `stripe_key`, fail
|
|
217
|
+
the config load with `INVALID_CONFIG`, as two such credentials do:
|
|
215
218
|
|
|
216
219
|
<CodeGroup>
|
|
217
220
|
```bash npm
|
|
@@ -277,7 +280,7 @@ account, a username with the secrets that sign it in, while a secret is one
|
|
|
277
280
|
value, so `credentials.admin` and `secrets.admin` can both exist.
|
|
278
281
|
|
|
279
282
|
In `e2e.config.ts`, `secrets.get()` hands an engine option a secret it needs
|
|
280
|
-
before any test runs, such as a [basic-auth password](/
|
|
283
|
+
before any test runs, such as a [basic-auth password](/web#protected-previews).
|
|
281
284
|
The config load checks the name, and the value is redacted once the engine
|
|
282
285
|
resolves it, together with the base64 `Authorization` credential it becomes.
|
|
283
286
|
The protection is text only: screenshots and the model's pixels are kept, so
|