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/README.md
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
<a href="https://tester.army/discord"><img alt="Join the community on Discord" src="https://img.shields.io/badge/Join%20the%20community-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white&labelColor=000000" /></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://www.star-history.com/tester-army/e2e"><picture><source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=tester-army/e2e&type=trending&theme=dark" /><img alt="GitHub Trending Repository of the Day" src="https://api.star-history.com/badge?repo=tester-army/e2e&type=trending" /></picture></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
10
14
|
# e2e
|
|
11
15
|
|
|
12
16
|
[e2e](https://tester.army/e2e?utm_source=e2e&utm_medium=github&utm_campaign=readme_intro) is an end-to-end testing framework for web and mobile apps. Describe a goal in natural language and an agent drives the app to reach it. Check the result with locators and assertions in the same test.
|
|
@@ -40,6 +44,11 @@ npx e2e init
|
|
|
40
44
|
config and an example test. The
|
|
41
45
|
[quickstart](https://e2e.tester.army/docs/quickstart) covers the rest.
|
|
42
46
|
|
|
47
|
+
To see a finished setup in your stack, open
|
|
48
|
+
[`examples/`](https://github.com/tester-army/e2e/tree/main/examples):
|
|
49
|
+
Vite, Next.js, Expo, and SwiftUI, each a standalone project with a passing
|
|
50
|
+
suite.
|
|
51
|
+
|
|
43
52
|
## Packages
|
|
44
53
|
|
|
45
54
|
| Package | What it does |
|
|
@@ -50,6 +59,7 @@ config and an example test. The
|
|
|
50
59
|
| [`@e2e-dev/github`](https://www.npmjs.com/package/@e2e-dev/github) | Reporter that posts results as a pull request comment. |
|
|
51
60
|
| [`@e2e-dev/kernel`](https://www.npmjs.com/package/@e2e-dev/kernel) | Kernel hosted browsers for the web engine. |
|
|
52
61
|
| [`@e2e-dev/eas`](https://www.npmjs.com/package/@e2e-dev/eas) | EAS Simulators hosted iOS simulators and Android emulators for the mobile engine. |
|
|
62
|
+
| [`@e2e-dev/decision`](https://e2e.tester.army/docs/decision-models) | Decision-model executors for bounded semantic actions and assertions. |
|
|
53
63
|
|
|
54
64
|
## Documentation
|
|
55
65
|
|
package/dist/.build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.
|
|
1
|
+
{"version":"0.18.0"}
|
package/dist/agent/ai-sdk.js
CHANGED
|
@@ -23,10 +23,11 @@ export function asSdkLanguageModel(instance) {
|
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* The cache lives on `globalThis`, not in module state: config and test
|
|
26
|
-
* modules
|
|
27
|
-
* code and the runner's own model plumbing would
|
|
28
|
-
* caches
|
|
29
|
-
* each other's instanceof checks. One slot, one SDK
|
|
26
|
+
* modules may import a copy of e2e other than the runner's, so an executor
|
|
27
|
+
* constructed in config code and the runner's own model plumbing would
|
|
28
|
+
* otherwise hold two separate caches, and two separate `ai` module instances
|
|
29
|
+
* whose error classes fail each other's instanceof checks. One slot, one SDK
|
|
30
|
+
* instance, every copy.
|
|
30
31
|
*/
|
|
31
32
|
const cacheSlot = realmSlot('e2e.ai-sdk.v1');
|
|
32
33
|
function cache() {
|
package/dist/agent/error.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Runner-owned agent error classification. */
|
|
2
|
-
import { classifyError, E2EError } from '../internal/errors.js';
|
|
2
|
+
import { classifyError, E2EError, isE2EError } from '../internal/errors.js';
|
|
3
3
|
/**
|
|
4
4
|
* The single source of truth for the closed agent code set: exit/result class
|
|
5
5
|
* for every code, the blocked category for codes a `blocked` verdict may
|
|
@@ -55,7 +55,7 @@ const MODEL_UNREACHABLE_CODES = new Set(Object.entries(AGENT_CODE_TABLE)
|
|
|
55
55
|
* model was shown may be what went wrong.
|
|
56
56
|
*/
|
|
57
57
|
export function isModelUnreachable(error) {
|
|
58
|
-
if (!(error
|
|
58
|
+
if (!isE2EError(error))
|
|
59
59
|
return false;
|
|
60
60
|
return MODEL_UNREACHABLE_CODES.has(error.code);
|
|
61
61
|
}
|
|
@@ -95,7 +95,7 @@ const AGENT_CODES = new Set(Object.keys(CATEGORY_BY_CODE));
|
|
|
95
95
|
export function toAgentError(cause) {
|
|
96
96
|
if (isAgentError(cause))
|
|
97
97
|
return cause;
|
|
98
|
-
const classified =
|
|
98
|
+
const classified = classifyError(cause);
|
|
99
99
|
if (AGENT_CODES.has(classified.code)) {
|
|
100
100
|
return new AgentError(classified.code, classified.message, { cause });
|
|
101
101
|
}
|
package/dist/agent/invocation.js
CHANGED
|
@@ -104,14 +104,13 @@ export class Invocation {
|
|
|
104
104
|
/** Captures one raw observation through the shared race-hardened path. */
|
|
105
105
|
captureObservation(pixels) {
|
|
106
106
|
return retryingObserve({
|
|
107
|
-
observe: (operation) => this.session.observe(operation, {
|
|
108
|
-
pixels,
|
|
109
|
-
pixelFallback: this.options.vision !== false && !this.runtime.exposure.withholdsPixels,
|
|
110
|
-
}),
|
|
107
|
+
observe: (operation, pixelFallback) => this.session.observe(operation, { pixels, pixelFallback }),
|
|
111
108
|
operation: () => this.operation(),
|
|
112
109
|
guard: (cause) => this.checkDeadline(cause),
|
|
113
110
|
signal: this.runtime.engine.signal,
|
|
114
111
|
api: this.options.api,
|
|
112
|
+
fallback: this.options.vision !== false,
|
|
113
|
+
tainted: () => this.runtime.exposure.withholdsPixels,
|
|
115
114
|
});
|
|
116
115
|
}
|
|
117
116
|
/**
|
|
@@ -33,6 +33,15 @@ export declare function isForcedToolChoiceDowngraded(warning: {
|
|
|
33
33
|
export declare function isForcedToolCallSkipped(error: unknown): error is {
|
|
34
34
|
readonly content?: readonly unknown[];
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Whether a turn forced to call `complete_step` called other tools only. The
|
|
38
|
+
* AI SDK throws `AI_ToolChoiceViolationError` for that too, with the reply's
|
|
39
|
+
* raw content; it is the model breaking the protocol, as when it names a tool
|
|
40
|
+
* the turn does not offer, not the provider failing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isForcedToolCallMismatched(error: unknown): error is {
|
|
43
|
+
readonly content: readonly unknown[];
|
|
44
|
+
};
|
|
36
45
|
/**
|
|
37
46
|
* Whether a provider failure says the model does not accept a forced tool
|
|
38
47
|
* choice. Reads the message and the raw response body of the error and of
|
|
@@ -42,6 +42,20 @@ export function isForcedToolCallSkipped(error) {
|
|
|
42
42
|
const record = error;
|
|
43
43
|
return record.name === 'AI_ToolChoiceViolationError' && record.finishReason === 'stop';
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a turn forced to call `complete_step` called other tools only. The
|
|
47
|
+
* AI SDK throws `AI_ToolChoiceViolationError` for that too, with the reply's
|
|
48
|
+
* raw content; it is the model breaking the protocol, as when it names a tool
|
|
49
|
+
* the turn does not offer, not the provider failing.
|
|
50
|
+
*/
|
|
51
|
+
export function isForcedToolCallMismatched(error) {
|
|
52
|
+
if (typeof error !== 'object' || error === null)
|
|
53
|
+
return false;
|
|
54
|
+
const record = error;
|
|
55
|
+
return (record.name === 'AI_ToolChoiceViolationError' &&
|
|
56
|
+
Array.isArray(record.content) &&
|
|
57
|
+
record.content.some((part) => typeof part === 'object' && part !== null && 'type' in part && part.type === 'tool-call'));
|
|
58
|
+
}
|
|
45
59
|
/** Longest cause chain walked; a wrapped error rarely nests deeper. */
|
|
46
60
|
const MAX_CAUSE_DEPTH = 8;
|
|
47
61
|
/**
|
|
@@ -306,14 +306,13 @@ export class ObservationFeed {
|
|
|
306
306
|
async capture(pixels) {
|
|
307
307
|
this.opening = undefined;
|
|
308
308
|
const raw = await retryingObserve({
|
|
309
|
-
observe: (operation) => this.runtime.engine.session.observe(operation, {
|
|
310
|
-
pixels,
|
|
311
|
-
pixelFallback: !this.runtime.exposure.withholdsPixels,
|
|
312
|
-
}),
|
|
309
|
+
observe: (operation, pixelFallback) => this.runtime.engine.session.observe(operation, { pixels, pixelFallback }),
|
|
313
310
|
operation: () => this.accounting.operation(),
|
|
314
311
|
guard: (cause) => this.accounting.checkpoint(cause),
|
|
315
312
|
signal: this.runtime.engine.signal,
|
|
316
313
|
api: this.accounting.api,
|
|
314
|
+
fallback: true,
|
|
315
|
+
tainted: () => this.runtime.exposure.withholdsPixels,
|
|
317
316
|
});
|
|
318
317
|
if (raw.kind === 'pixels') {
|
|
319
318
|
this.semanticHistory = false;
|
package/dist/agent/phases.d.ts
CHANGED
|
@@ -67,11 +67,17 @@ export declare function checkStepClock(options: {
|
|
|
67
67
|
* re-read rather than surfaced as a failed call. `guard` is the caller's
|
|
68
68
|
* clock check, so a capture that outlives the deadline reports the step's own
|
|
69
69
|
* timeout rather than whichever transport error the truncated budget produced.
|
|
70
|
+
* `fallback` asks the engine for fallback pixels, which a secret fill
|
|
71
|
+
* (`tainted`, read per capture) withholds; a capture that then times out
|
|
72
|
+
* before the step clock does is the policy denial it is, not an engine
|
|
73
|
+
* timeout, since only those pixels could have answered.
|
|
70
74
|
*/
|
|
71
75
|
export declare function retryingObserve(options: {
|
|
72
|
-
readonly observe: (operation: OperationContext) => Promise<Observation>;
|
|
76
|
+
readonly observe: (operation: OperationContext, pixelFallback: boolean) => Promise<Observation>;
|
|
73
77
|
readonly operation: () => OperationContext;
|
|
74
78
|
readonly guard: (cause?: unknown) => void;
|
|
75
79
|
readonly signal: AbortSignal;
|
|
76
80
|
readonly api: string;
|
|
81
|
+
readonly fallback: boolean;
|
|
82
|
+
readonly tainted: () => boolean;
|
|
77
83
|
}): Promise<Observation>;
|
package/dist/agent/phases.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* these, so event recording, `--debug` buckets, tracing, deadline semantics,
|
|
5
5
|
* and observation race-hardening cannot drift between the two paths.
|
|
6
6
|
*/
|
|
7
|
-
import { asEngineError,
|
|
7
|
+
import { asEngineError, isE2EError } from '../internal/errors.js';
|
|
8
8
|
import { timestamp } from '../internal/ids.js';
|
|
9
9
|
import { POLL_INTERVAL_MS, sleep } from '../internal/time.js';
|
|
10
10
|
import { AgentError, toAgentError } from './error.js';
|
|
@@ -112,14 +112,23 @@ function timedOutAtDeadline(cause, deadline) {
|
|
|
112
112
|
* re-read rather than surfaced as a failed call. `guard` is the caller's
|
|
113
113
|
* clock check, so a capture that outlives the deadline reports the step's own
|
|
114
114
|
* timeout rather than whichever transport error the truncated budget produced.
|
|
115
|
+
* `fallback` asks the engine for fallback pixels, which a secret fill
|
|
116
|
+
* (`tainted`, read per capture) withholds; a capture that then times out
|
|
117
|
+
* before the step clock does is the policy denial it is, not an engine
|
|
118
|
+
* timeout, since only those pixels could have answered.
|
|
115
119
|
*/
|
|
116
120
|
export async function retryingObserve(options) {
|
|
117
121
|
for (;;) {
|
|
122
|
+
const withheld = options.fallback && options.tainted();
|
|
118
123
|
try {
|
|
119
|
-
return await options.observe(options.operation());
|
|
124
|
+
return await options.observe(options.operation(), options.fallback && !withheld);
|
|
120
125
|
}
|
|
121
126
|
catch (cause) {
|
|
122
127
|
options.guard(cause);
|
|
128
|
+
if (withheld && asEngineError(cause)?.code === 'OPERATION_TIMEOUT') {
|
|
129
|
+
throw new AgentError('POLICY_DENIED', `${options.api} could not read the semantic tree, and its screenshot fallback is denied: ` +
|
|
130
|
+
'a secret was filled in this attempt, so no pixels leave the runner until it ends (PIXEL_TAINTED)', { cause });
|
|
131
|
+
}
|
|
123
132
|
// Structural, not instanceof: an engine a config file imported lives in
|
|
124
133
|
// another module registry, and its retryable race would otherwise fail
|
|
125
134
|
// the observation the moment a page navigates under it.
|
|
@@ -133,7 +142,7 @@ export async function retryingObserve(options) {
|
|
|
133
142
|
}
|
|
134
143
|
/** Event code for a failed phase: the runner or engine code, or the error's name. */
|
|
135
144
|
function phaseErrorCode(cause) {
|
|
136
|
-
if (cause
|
|
145
|
+
if (isE2EError(cause))
|
|
137
146
|
return cause.code;
|
|
138
147
|
const engineError = asEngineError(cause);
|
|
139
148
|
if (engineError !== undefined)
|
package/dist/agent/prompts.js
CHANGED
|
@@ -95,6 +95,9 @@ export const JUDGMENT_REQUEST = [
|
|
|
95
95
|
'Decide whether the instruction is true for the observation right now.',
|
|
96
96
|
'Answer "holds" only when the observation shows it is true, and "fails" only when the observation shows it is false.',
|
|
97
97
|
'Answer "inconclusive" when the observation does not contain enough evidence to decide either way: the relevant part is not on screen, is still loading, or cannot be read. Never guess.',
|
|
98
|
+
'When the same value or state is shown in more than one place, such as an order total in a summary and again as the amount on a pay button, the instruction holds only when every place agrees with it; one place that contradicts it is a "fails", even if another agrees.',
|
|
99
|
+
'This applies only when the instruction does not say where to look: an instruction that names the place, such as "the order summary total", is judged on that place alone.',
|
|
100
|
+
'It is about one value shown twice, not about different items: a claim about some item, such as "a todo is marked done", holds when one item matches.',
|
|
98
101
|
'Quotation marks in the instruction delimit the words to look for; the marks themselves, their style (straight or curly), and the punctuation around them are not part of those words, so text that reads the same holds.',
|
|
99
102
|
'Write the explanation first and let the verdict follow from it: the two must agree.',
|
|
100
103
|
'Respond with { "protocolVersion": "agent-judgment-2", "explanation": <short reason grounded in the observation>, "verdict": "holds" | "fails" | "inconclusive" }.',
|
package/dist/agent/replay.js
CHANGED
|
@@ -150,22 +150,25 @@ export async function replayTrace(host, trace, options = {}) {
|
|
|
150
150
|
await planned.invoke();
|
|
151
151
|
break;
|
|
152
152
|
case 'scroll': {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
// A scroll on a list is paced by the relocation before each repeat.
|
|
154
|
+
// A viewport scroll relocates nothing, so each later repeat takes a
|
|
155
|
+
// settled look of its own, as the live loop did between them. A
|
|
156
|
+
// list judged lost scrolls as the viewport from then on, instead of
|
|
157
|
+
// waiting out the relocation backoff again on every repeat.
|
|
158
|
+
let list = planned.list;
|
|
159
|
+
for (let index = 0; index < planned.times; index += 1) {
|
|
160
|
+
if (list === undefined) {
|
|
157
161
|
if (index > 0)
|
|
158
162
|
await host.observe('held-still');
|
|
159
163
|
await host.actions.scroll(planned.direction);
|
|
160
|
-
repeated += 1;
|
|
161
164
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
else {
|
|
166
|
+
const scrolled = await scrollOnce(host, planned.direction, list, index === 0 ? look : HELD_STILL);
|
|
167
|
+
if (scrolled.kind === 'failed')
|
|
168
|
+
return stop(scrolled.failure, partial());
|
|
169
|
+
if (scrolled.kind === 'viewport')
|
|
170
|
+
list = undefined;
|
|
171
|
+
}
|
|
169
172
|
repeated += 1;
|
|
170
173
|
}
|
|
171
174
|
break;
|
|
@@ -377,12 +380,12 @@ async function scrollOnce(host, direction, list, look) {
|
|
|
377
380
|
const relocated = await relocate(host, list.descriptor, look);
|
|
378
381
|
if (relocated.kind === 'found') {
|
|
379
382
|
await host.actions.scroll(direction, { id: relocated.id });
|
|
380
|
-
return
|
|
383
|
+
return { kind: 'list' };
|
|
381
384
|
}
|
|
382
385
|
if ((list.spans ?? 0) < MAIN_LIST_SHARE)
|
|
383
|
-
return relocated.failure;
|
|
386
|
+
return { kind: 'failed', failure: relocated.failure };
|
|
384
387
|
await host.actions.scroll(direction);
|
|
385
|
-
return
|
|
388
|
+
return { kind: 'viewport' };
|
|
386
389
|
}
|
|
387
390
|
/** A fresh look that proves the screen holds still: the first look of a replay without a start capture, and the look after an action that moved the screen. */
|
|
388
391
|
const HELD_STILL = { kind: 'capture', settle: 'held-still' };
|
|
@@ -117,7 +117,8 @@ export declare class StepTraceSession {
|
|
|
117
117
|
* self-finalized verdict on a full replay whose postcondition holds;
|
|
118
118
|
* undefined dispatches the executor — after a miss from the top, after a
|
|
119
119
|
* divergence mid-step with `replayedPrefix` set. Every failure to replay is
|
|
120
|
-
* a miss, never an error; only runtime hard stops
|
|
120
|
+
* a miss, never an error; only runtime hard stops and a policy denial on
|
|
121
|
+
* the opening look propagate.
|
|
121
122
|
*/
|
|
122
123
|
begin(): Promise<StepVerdict | undefined>;
|
|
123
124
|
/**
|
package/dist/agent/step-cache.js
CHANGED
|
@@ -136,7 +136,8 @@ export class StepTraceSession {
|
|
|
136
136
|
* self-finalized verdict on a full replay whose postcondition holds;
|
|
137
137
|
* undefined dispatches the executor — after a miss from the top, after a
|
|
138
138
|
* divergence mid-step with `replayedPrefix` set. Every failure to replay is
|
|
139
|
-
* a miss, never an error; only runtime hard stops
|
|
139
|
+
* a miss, never an error; only runtime hard stops and a policy denial on
|
|
140
|
+
* the opening look propagate.
|
|
140
141
|
*/
|
|
141
142
|
async begin() {
|
|
142
143
|
this.startedMs = Date.now();
|
|
@@ -297,7 +298,7 @@ export class StepTraceSession {
|
|
|
297
298
|
* when this step may write. Settled as far as the capture's purpose asks.
|
|
298
299
|
*/
|
|
299
300
|
async captureStart(purpose) {
|
|
300
|
-
const observation = await probeScreen(this.host, purpose === 'path-only' ? 'raw' : 'held-still');
|
|
301
|
+
const observation = await probeScreen(this.host, purpose === 'path-only' ? 'raw' : 'held-still', true);
|
|
301
302
|
this.startPath = observation?.path;
|
|
302
303
|
if (this.recorder !== undefined && observation?.kind === 'semantic')
|
|
303
304
|
this.startNodes = observation.nodes;
|
|
@@ -565,14 +566,17 @@ function baselineScreen(screens, endPath) {
|
|
|
565
566
|
* One cache capture, settled as far as `mode` asks, or undefined when the
|
|
566
567
|
* surface cannot be observed right now, which is the executor's business,
|
|
567
568
|
* not the cache's. Runtime hard stops (timeout, cancellation) are the step's
|
|
568
|
-
* truth even when they land during cache bookkeeping, and propagate.
|
|
569
|
+
* truth even when they land during cache bookkeeping, and propagate. So does
|
|
570
|
+
* a policy denial on the step's opening look (`opening`): the executor's
|
|
571
|
+
* first look reads the same screen under the same policy, and would spend
|
|
572
|
+
* another operation budget to be denied again.
|
|
569
573
|
*/
|
|
570
|
-
async function probeScreen(host, mode) {
|
|
574
|
+
async function probeScreen(host, mode, opening = false) {
|
|
571
575
|
try {
|
|
572
576
|
return await host.observe(mode);
|
|
573
577
|
}
|
|
574
578
|
catch (cause) {
|
|
575
|
-
if (isRuntimeHardStop(cause))
|
|
579
|
+
if (isRuntimeHardStop(cause) || (opening && isAgentError(cause) && cause.code === 'POLICY_DENIED'))
|
|
576
580
|
throw cause;
|
|
577
581
|
return undefined;
|
|
578
582
|
}
|
package/dist/agent/tool-loop.js
CHANGED
|
@@ -15,7 +15,7 @@ import { MODEL_REQUEST_HEADERS } from '../internal/client-identity.js';
|
|
|
15
15
|
import { ConfigurationError, withHint } from '../internal/errors.js';
|
|
16
16
|
import { failureHint, isAbort, TRANSPORT_RETRIES } from './model/sdk.js';
|
|
17
17
|
import { isContextOverflow } from './model/overflow.js';
|
|
18
|
-
import { isForcedToolCallSkipped, isForcedToolChoiceDowngraded, isForcedToolChoiceRejected } from './model/tool-choice.js';
|
|
18
|
+
import { isForcedToolCallMismatched, isForcedToolCallSkipped, isForcedToolChoiceDowngraded, isForcedToolChoiceRejected, } from './model/tool-choice.js';
|
|
19
19
|
import { withStallGuard } from './model/stall.js';
|
|
20
20
|
import { providerHints } from './model/provider-hints.js';
|
|
21
21
|
import { isScreenOutput, toolResultTexts } from './screen-update.js';
|
|
@@ -193,6 +193,13 @@ class LoopRun {
|
|
|
193
193
|
prompt = shrunk;
|
|
194
194
|
continue;
|
|
195
195
|
}
|
|
196
|
+
const corrected = this.correctForcedTurn(cause, tracker);
|
|
197
|
+
if (corrected === 'out-of-turns')
|
|
198
|
+
break;
|
|
199
|
+
if (corrected !== undefined) {
|
|
200
|
+
prompt = corrected;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
196
203
|
const freed = this.freeToolChoiceRetry(cause, tracker);
|
|
197
204
|
if (freed !== undefined) {
|
|
198
205
|
prompt = freed;
|
|
@@ -275,6 +282,49 @@ class LoopRun {
|
|
|
275
282
|
{ role: 'user', content: TEXT_REPLY_NOTICE },
|
|
276
283
|
];
|
|
277
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* A turn forced to call `complete_step` that called other tools instead:
|
|
287
|
+
* the SDK throws `ToolChoiceViolationError` before running any of them. It
|
|
288
|
+
* is handled like a call to a tool the turn does not offer: the turn counts,
|
|
289
|
+
* each call goes back to the model as an error result, and the model is
|
|
290
|
+
* asked again on the same turn budget. Out of turns, the step ends as
|
|
291
|
+
* `STEP_NO_CONCLUSION`, like any step that never concluded.
|
|
292
|
+
*/
|
|
293
|
+
correctForcedTurn(cause, tracker) {
|
|
294
|
+
if (this.lastRequest === undefined || this.context.signal.aborted || this.hardStop !== undefined)
|
|
295
|
+
return undefined;
|
|
296
|
+
if (!isForcedToolCallMismatched(cause))
|
|
297
|
+
return undefined;
|
|
298
|
+
const reply = thrownReply(cause.content);
|
|
299
|
+
const results = reply.calls.map((call) => ({
|
|
300
|
+
type: 'tool-result',
|
|
301
|
+
toolCallId: call.toolCallId,
|
|
302
|
+
toolName: call.toolName,
|
|
303
|
+
output: {
|
|
304
|
+
type: 'error-text',
|
|
305
|
+
value: `${call.toolName} did not run: only complete_step is available now. Call complete_step with your verdict.`,
|
|
306
|
+
},
|
|
307
|
+
}));
|
|
308
|
+
this.recordThrownTurn(tracker, reply.calls.map((call) => `${call.toolName}(${truncate(safeJson(call.input), MAX_TURN_CALL_CHARS)})`), [
|
|
309
|
+
skippedReplyText(cause.content),
|
|
310
|
+
...results.map((result) => `[${result.toolName}] error: ${result.output.value}`),
|
|
311
|
+
].filter((line) => line !== '').join('\n'));
|
|
312
|
+
if (this.turnsUsed >= this.maxTurns)
|
|
313
|
+
return 'out-of-turns';
|
|
314
|
+
this.note(`turn ${String(this.turnsUsed)} called other tools in place of complete_step: asking again`);
|
|
315
|
+
this.turnOffset = this.turnsUsed;
|
|
316
|
+
return [...this.lastRequest, reply.message, { role: 'tool', content: results }];
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Counts a turn the provider answered, and billed, before the SDK threw on
|
|
320
|
+
* it. The SDK reports no usage for such a turn.
|
|
321
|
+
*/
|
|
322
|
+
recordThrownTurn(tracker, calls, outcome) {
|
|
323
|
+
tracker.onStepEnd({ usage: UNREPORTED_USAGE, providerMetadata: undefined, reasoningText: undefined });
|
|
324
|
+
this.turnsUsed += 1;
|
|
325
|
+
this.turns.push({ index: this.turnsUsed, calls, outcome, notes: [] });
|
|
326
|
+
this.attachTranscript();
|
|
327
|
+
}
|
|
278
328
|
/**
|
|
279
329
|
* A model that refuses a forced tool choice (HTTP 400 naming
|
|
280
330
|
* `tool_choice`) is asked again with `auto` and a tool-calls-only rule in
|
|
@@ -290,10 +340,7 @@ class LoopRun {
|
|
|
290
340
|
if (this.context.signal.aborted || this.hardStop !== undefined)
|
|
291
341
|
return undefined;
|
|
292
342
|
if (isForcedToolCallSkipped(cause)) {
|
|
293
|
-
|
|
294
|
-
tracker.onStepEnd({ usage: UNREPORTED_USAGE, providerMetadata: undefined, reasoningText: undefined });
|
|
295
|
-
this.turnsUsed += 1;
|
|
296
|
-
this.turns.push({ index: this.turnsUsed, calls: [], outcome: skippedReplyText(cause.content), notes: [] });
|
|
343
|
+
this.recordThrownTurn(tracker, [], skippedReplyText(cause.content));
|
|
297
344
|
this.note(`turn ${String(this.turnsUsed)} answered without the forced tool call: retrying with auto`);
|
|
298
345
|
}
|
|
299
346
|
else if (isForcedToolChoiceRejected(cause)) {
|
|
@@ -619,6 +666,46 @@ function skippedReplyText(content) {
|
|
|
619
666
|
.trim();
|
|
620
667
|
return text === '' ? '' : `assistant: ${truncate(text, MAX_TURN_TEXT_CHARS)}`;
|
|
621
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* A reply the SDK threw on, as the assistant message the SDK's own history
|
|
671
|
+
* would carry: its text, reasoning, and tool calls, each with the provider's
|
|
672
|
+
* metadata (reasoning signatures and Gemini's thought signatures ride there),
|
|
673
|
+
* and the tool calls apart, to answer each with a result.
|
|
674
|
+
*/
|
|
675
|
+
function thrownReply(content) {
|
|
676
|
+
const parts = [];
|
|
677
|
+
const calls = [];
|
|
678
|
+
for (const part of content) {
|
|
679
|
+
const metadata = part.providerMetadata === undefined ? {} : { providerOptions: part.providerMetadata };
|
|
680
|
+
if (part.type === 'text' && typeof part.text === 'string' && part.text !== '') {
|
|
681
|
+
parts.push({ type: 'text', text: part.text, ...metadata });
|
|
682
|
+
}
|
|
683
|
+
else if (part.type === 'reasoning' && typeof part.text === 'string') {
|
|
684
|
+
parts.push({ type: 'reasoning', text: part.text, ...metadata });
|
|
685
|
+
}
|
|
686
|
+
else if (part.type === 'tool-call' && typeof part.toolCallId === 'string' && typeof part.toolName === 'string') {
|
|
687
|
+
const call = {
|
|
688
|
+
type: 'tool-call',
|
|
689
|
+
toolCallId: part.toolCallId,
|
|
690
|
+
toolName: part.toolName,
|
|
691
|
+
input: typeof part.input === 'string' ? parseToolInput(part.input) : part.input,
|
|
692
|
+
...metadata,
|
|
693
|
+
};
|
|
694
|
+
parts.push(call);
|
|
695
|
+
calls.push(call);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return { message: { role: 'assistant', content: parts }, calls };
|
|
699
|
+
}
|
|
700
|
+
/** A raw tool call's input for the history: the parsed JSON, or the text itself when it does not parse. */
|
|
701
|
+
function parseToolInput(input) {
|
|
702
|
+
try {
|
|
703
|
+
return JSON.parse(input);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
return input;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
622
709
|
function describeOutput(output) {
|
|
623
710
|
if (isScreenOutput(output))
|
|
624
711
|
return `${output.text}\n[screenshot, ${String(output.pixels.data.byteLength)} bytes]`;
|
package/dist/cache/trace.js
CHANGED
|
@@ -203,7 +203,7 @@ function readRecordedAction(document) {
|
|
|
203
203
|
switch (name) {
|
|
204
204
|
case 'type': {
|
|
205
205
|
const target = readDescriptor(raw['target']);
|
|
206
|
-
const value =
|
|
206
|
+
const value = readTypedValue(raw['value']);
|
|
207
207
|
if (target === undefined || value === undefined)
|
|
208
208
|
return undefined;
|
|
209
209
|
return { name: 'type', summary, target, value };
|
|
@@ -310,7 +310,7 @@ function readRecordedAction(document) {
|
|
|
310
310
|
case 'back':
|
|
311
311
|
return { name: 'back', summary };
|
|
312
312
|
case 'typeText': {
|
|
313
|
-
const value =
|
|
313
|
+
const value = readTypedValue(raw['value']);
|
|
314
314
|
if (value === undefined || typeof raw['replace'] !== 'boolean')
|
|
315
315
|
return undefined;
|
|
316
316
|
return { name: 'typeText', summary, value, replace: raw['replace'] };
|
|
@@ -612,3 +612,7 @@ function readInputText(value) {
|
|
|
612
612
|
}
|
|
613
613
|
return value;
|
|
614
614
|
}
|
|
615
|
+
/** A typed value: `readInputText`, except that `''` is valid, since typing it clears a field. */
|
|
616
|
+
function readTypedValue(value) {
|
|
617
|
+
return value === '' ? value : readInputText(value);
|
|
618
|
+
}
|
package/dist/cli/bin.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { unsupportedRuntimeMessage } from '../internal/node-version.js';
|
|
3
3
|
// Checked before the CLI module loads: that module and its dependencies are
|
|
4
4
|
// what an old runtime would fail on, with a stack trace instead of a sentence.
|
|
5
|
-
const unsupported =
|
|
5
|
+
const unsupported = unsupportedRuntimeMessage();
|
|
6
6
|
if (unsupported !== undefined) {
|
|
7
7
|
process.stderr.write(`${unsupported}\n`);
|
|
8
8
|
process.exit(2);
|
package/dist/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { detectPackageManager, execCommand, runScriptCommand } from '../internal
|
|
|
7
7
|
import { packageVersion } from '../internal/package-version.js';
|
|
8
8
|
import { classifyError, exitCodeForCategory } from '../internal/errors.js';
|
|
9
9
|
import { list, run } from '../run/runner.js';
|
|
10
|
+
import { claimRunnerOutput } from './run-output.js';
|
|
10
11
|
import { explore, STEP_BOUNDS, TIMEOUT_BOUNDS } from '../explore/index.js';
|
|
11
12
|
import { BUILTIN_REPORTERS, isBuiltinReporter } from '../report/builtin.js';
|
|
12
13
|
import { bounded } from '../report/format.js';
|
|
@@ -401,11 +402,14 @@ function createProgram(version, telemetry) {
|
|
|
401
402
|
.description('Serve an MCP server over stdio for a coding agent such as Claude Code or Cursor: open_session opens a live session on one target of any config the agent names, call runs the session\'s tools (observe, tap, type, press, select, scroll, navigate, type_secret, locate, screenshot, and the project\'s own), tools describes them, close_session ends it. The agent explores the real app before writing a test. Register it with the client, e.g. claude mcp add e2e -- npx e2e mcp.')
|
|
402
403
|
.option('--config <path>', 'config file (default: the nearest e2e.config.ts)')
|
|
403
404
|
.option('--target <name>', 'target every session opens on (default: the only target, or the one open_session names)')
|
|
404
|
-
.option('--
|
|
405
|
+
.option('--headed', 'show the UI in sessions whose open_session does not set headed, when the engine supports it')
|
|
406
|
+
// Sessions were headed by default before 0.18 and took --headless; a client config that still passes it
|
|
407
|
+
// asks for the default now, and a refused flag would surface as nothing but a failed connection.
|
|
408
|
+
.addOption(new Option('--headless').hideHelp())
|
|
405
409
|
.option('--max-sessions <n>', `sessions open at once, each with its own browser or device, ${SESSION_BOUNDS.min} through ${SESSION_BOUNDS.max} (default: ${SESSION_BOUNDS.default})`, parseBoundedInt(SESSION_BOUNDS))
|
|
406
410
|
.addHelpText('after', [
|
|
407
411
|
'',
|
|
408
|
-
examples(['e2e mcp', 'e2e mcp --target web --
|
|
412
|
+
examples(['e2e mcp', 'e2e mcp --target web --headed', 'e2e mcp --max-sessions 8', 'claude mcp add e2e -- npx e2e mcp']),
|
|
409
413
|
'',
|
|
410
414
|
docsLine('/reference/mcp'),
|
|
411
415
|
].join('\n'))
|
|
@@ -466,28 +470,35 @@ function createProgram(version, telemetry) {
|
|
|
466
470
|
// Each value is what its parser returned, so a bad one never reaches here.
|
|
467
471
|
options, command) => {
|
|
468
472
|
rejectForwardedFlags(command, files);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
473
|
+
const output = claimRunnerOutput();
|
|
474
|
+
try {
|
|
475
|
+
await runToOutcome(telemetry, command, (signals) => run({
|
|
476
|
+
files,
|
|
477
|
+
configPath: options.config,
|
|
478
|
+
targetIds: options.target,
|
|
479
|
+
...selectionRunOptions(options),
|
|
480
|
+
headed: options.headed,
|
|
481
|
+
agent: options.agent,
|
|
482
|
+
retries: options.retries,
|
|
483
|
+
workers: options.workers,
|
|
484
|
+
maxFailures: options.maxFailures,
|
|
485
|
+
repeatEach: options.repeatEach,
|
|
486
|
+
reporters: options.reporter,
|
|
487
|
+
output: options.output,
|
|
488
|
+
noCache: options.cache === false,
|
|
489
|
+
strictCache: options.strictCache,
|
|
490
|
+
debug: options.debug,
|
|
491
|
+
aiTrace: options.aiTrace,
|
|
492
|
+
trace: recordingOption(options.trace),
|
|
493
|
+
video: recordingOption(options.video),
|
|
494
|
+
interruptSignal: signals.interruptSignal,
|
|
495
|
+
forceSignal: signals.forceSignal,
|
|
496
|
+
processOutput: output,
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
output.end();
|
|
501
|
+
}
|
|
491
502
|
});
|
|
492
503
|
program
|
|
493
504
|
.command('explore')
|
|
@@ -25,6 +25,7 @@ export declare function findRegisteredMcpFiles(cwd: string): string[];
|
|
|
25
25
|
/**
|
|
26
26
|
* Plans the writes for the given files. A file without the entry gets it
|
|
27
27
|
* merged in; one whose entry already matches needs nothing and is left out.
|
|
28
|
-
*
|
|
28
|
+
* Extra settings on the e2e entry, other servers, other keys, and the file's
|
|
29
|
+
* indentation are preserved.
|
|
29
30
|
*/
|
|
30
31
|
export declare function planMcpRegistration(cwd: string, files: readonly string[]): McpRegistration[];
|
|
@@ -45,7 +45,8 @@ export function findRegisteredMcpFiles(cwd) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Plans the writes for the given files. A file without the entry gets it
|
|
47
47
|
* merged in; one whose entry already matches needs nothing and is left out.
|
|
48
|
-
*
|
|
48
|
+
* Extra settings on the e2e entry, other servers, other keys, and the file's
|
|
49
|
+
* indentation are preserved.
|
|
49
50
|
*/
|
|
50
51
|
export function planMcpRegistration(cwd, files) {
|
|
51
52
|
const registrations = [];
|
|
@@ -55,9 +56,13 @@ export function planMcpRegistration(cwd, files) {
|
|
|
55
56
|
const servers = typeof document.mcpServers === 'object' && document.mcpServers !== null && !Array.isArray(document.mcpServers)
|
|
56
57
|
? document.mcpServers
|
|
57
58
|
: {};
|
|
58
|
-
|
|
59
|
+
const existing = servers[MCP_SERVER_NAME];
|
|
60
|
+
const entry = typeof existing === 'object' && existing !== null && !Array.isArray(existing)
|
|
61
|
+
? { ...existing, ...SERVER_ENTRY }
|
|
62
|
+
: SERVER_ENTRY;
|
|
63
|
+
if (JSON.stringify(existing) === JSON.stringify(entry))
|
|
59
64
|
continue;
|
|
60
|
-
const merged = { ...document, mcpServers: { ...servers, [MCP_SERVER_NAME]:
|
|
65
|
+
const merged = { ...document, mcpServers: { ...servers, [MCP_SERVER_NAME]: entry } };
|
|
61
66
|
const original = existsSync(absolute) ? readFileSync(absolute, 'utf8') : undefined;
|
|
62
67
|
const indent = original?.match(/\n([\t ]+)"/)?.[1] ?? ' ';
|
|
63
68
|
registrations.push({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"@e2e-dev/web":"0.
|
|
1
|
+
{"@e2e-dev/web":"0.13.0","@e2e-dev/mobile":"0.10.0"}
|