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/cache/trace.js
CHANGED
|
@@ -34,10 +34,11 @@ export function bound(text, maxChars) {
|
|
|
34
34
|
return text.length <= maxChars ? text : `${text.slice(0, maxChars - 1)}…`;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Cap on recorded
|
|
38
|
-
* appeared on screen between the first observation and the
|
|
39
|
-
* a same-screen mutation rarely has more
|
|
40
|
-
* the whole screen keeps the first few
|
|
37
|
+
* Cap on recorded anchors, per side of the delta. Anchors are the step's own
|
|
38
|
+
* delta, what appeared on screen between the first observation and the
|
|
39
|
+
* passing one and what vanished, so a same-screen mutation rarely has more
|
|
40
|
+
* than a handful; a step that changes the whole screen keeps the first few,
|
|
41
|
+
* announcements first (`cache/anchors.ts`).
|
|
41
42
|
*/
|
|
42
43
|
export const MAX_TRACE_ANCHORS = 8;
|
|
43
44
|
/** Longest a replay waits for the recorded end state to return. */
|
|
@@ -66,6 +67,9 @@ function each(items, read) {
|
|
|
66
67
|
const SHA256_HEX = /^[a-f0-9]{64}$/u;
|
|
67
68
|
/** Most twins a positional recording distinguishes between; beyond it a list is data, not a control set. */
|
|
68
69
|
const MAX_TRACE_POSITION_OF = 1000;
|
|
70
|
+
/** The node states an anchor records: the ones a step sets on a control it leaves on screen. */
|
|
71
|
+
export const TRACE_ANCHOR_STATES = ['checked', 'expanded', 'pressed', 'selected'];
|
|
72
|
+
const isTraceAnchorState = oneOf(TRACE_ANCHOR_STATES);
|
|
69
73
|
/** The verbs that act on one node and carry no input: tap and its variants, hover, scroll into view. */
|
|
70
74
|
const NODE_ACTION_NAMES = ['tap', 'doubleTap', 'longPress', 'secondaryTap', 'hover', 'scrollTo'];
|
|
71
75
|
const isNodeActionName = oneOf(NODE_ACTION_NAMES);
|
|
@@ -131,18 +135,13 @@ function readActionTrace(document) {
|
|
|
131
135
|
const summary = readBoundedText(raw['summary'], MAX_TRACE_SUMMARY_CHARS);
|
|
132
136
|
if (summary === undefined)
|
|
133
137
|
return undefined;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (endPath !== undefined) {
|
|
142
|
-
if (typeof endPath !== 'string' || endPath === '' || endPath.length > MAX_TRACE_DESCRIPTOR_CHARS) {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
138
|
+
// A location is a replay precondition, kept whole like a typed value.
|
|
139
|
+
const startPath = raw['startPath'] === undefined ? undefined : readInputText(raw['startPath']);
|
|
140
|
+
if (raw['startPath'] !== undefined && startPath === undefined)
|
|
141
|
+
return undefined;
|
|
142
|
+
const endPath = raw['endPath'] === undefined ? undefined : readInputText(raw['endPath']);
|
|
143
|
+
if (raw['endPath'] !== undefined && endPath === undefined)
|
|
144
|
+
return undefined;
|
|
146
145
|
const truncated = raw['truncated'];
|
|
147
146
|
if (truncated !== undefined && typeof truncated !== 'boolean')
|
|
148
147
|
return undefined;
|
|
@@ -151,15 +150,12 @@ function readActionTrace(document) {
|
|
|
151
150
|
(typeof endWaitMs !== 'number' || !Number.isInteger(endWaitMs) || endWaitMs < 0 || endWaitMs > MAX_TRACE_END_WAIT_MS)) {
|
|
152
151
|
return undefined;
|
|
153
152
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (endAnchors === undefined)
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
153
|
+
const endAnchors = readAnchors(raw['endAnchors']);
|
|
154
|
+
if (endAnchors === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
const goneAnchors = readAnchors(raw['goneAnchors']);
|
|
157
|
+
if (goneAnchors === undefined)
|
|
158
|
+
return undefined;
|
|
163
159
|
const actionsRaw = raw['actions'];
|
|
164
160
|
if (!Array.isArray(actionsRaw) || actionsRaw.length === 0 || actionsRaw.length > MAX_TRACE_ACTIONS) {
|
|
165
161
|
return undefined;
|
|
@@ -177,11 +173,20 @@ function readActionTrace(document) {
|
|
|
177
173
|
summary,
|
|
178
174
|
...(startPath === undefined ? {} : { startPath }),
|
|
179
175
|
...(endPath === undefined ? {} : { endPath }),
|
|
180
|
-
...(endAnchors
|
|
176
|
+
...(endAnchors.length === 0 ? {} : { endAnchors }),
|
|
177
|
+
...(goneAnchors.length === 0 ? {} : { goneAnchors }),
|
|
181
178
|
...(endWaitMs === undefined ? {} : { endWaitMs }),
|
|
182
179
|
...(truncated === undefined ? {} : { truncated }),
|
|
183
180
|
};
|
|
184
181
|
}
|
|
182
|
+
/** One side of the recorded delta: absent reads as empty, anything malformed or over the cap as undefined. */
|
|
183
|
+
function readAnchors(document) {
|
|
184
|
+
if (document === undefined)
|
|
185
|
+
return [];
|
|
186
|
+
if (!Array.isArray(document) || document.length > MAX_TRACE_ANCHORS)
|
|
187
|
+
return undefined;
|
|
188
|
+
return each(document, readDescriptor);
|
|
189
|
+
}
|
|
185
190
|
function readRecordedAction(document) {
|
|
186
191
|
if (typeof document !== 'object' || document === null || Array.isArray(document)) {
|
|
187
192
|
return undefined;
|
|
@@ -198,7 +203,7 @@ function readRecordedAction(document) {
|
|
|
198
203
|
switch (name) {
|
|
199
204
|
case 'type': {
|
|
200
205
|
const target = readDescriptor(raw['target']);
|
|
201
|
-
const value =
|
|
206
|
+
const value = readTypedValue(raw['value']);
|
|
202
207
|
if (target === undefined || value === undefined)
|
|
203
208
|
return undefined;
|
|
204
209
|
return { name: 'type', summary, target, value };
|
|
@@ -305,7 +310,7 @@ function readRecordedAction(document) {
|
|
|
305
310
|
case 'back':
|
|
306
311
|
return { name: 'back', summary };
|
|
307
312
|
case 'typeText': {
|
|
308
|
-
const value =
|
|
313
|
+
const value = readTypedValue(raw['value']);
|
|
309
314
|
if (value === undefined || typeof raw['replace'] !== 'boolean')
|
|
310
315
|
return undefined;
|
|
311
316
|
return { name: 'typeText', summary, value, replace: raw['replace'] };
|
|
@@ -370,13 +375,14 @@ export const DESCRIPTOR_FIELDS = [
|
|
|
370
375
|
'selector',
|
|
371
376
|
'inputPurpose',
|
|
372
377
|
'within',
|
|
378
|
+
'value',
|
|
373
379
|
];
|
|
374
380
|
/**
|
|
375
381
|
* The descriptor fields that carry text read off the screen, where a value
|
|
376
382
|
* a test supplied can reappear. `role`, `selector`, and `inputPurpose` are
|
|
377
383
|
* vocabulary, never screen text.
|
|
378
384
|
*/
|
|
379
|
-
const DESCRIPTOR_TEXT_FIELDS = ['name', 'text', 'testId', 'placeholder', 'within'];
|
|
385
|
+
const DESCRIPTOR_TEXT_FIELDS = ['name', 'text', 'testId', 'placeholder', 'within', 'value'];
|
|
380
386
|
function mapDescriptorText(descriptor, map) {
|
|
381
387
|
const changes = {};
|
|
382
388
|
for (const field of DESCRIPTOR_TEXT_FIELDS) {
|
|
@@ -486,6 +492,9 @@ export function mapTraceText(trace, map) {
|
|
|
486
492
|
const endAnchors = trace.endAnchors === undefined ? undefined : mapDescriptors(trace.endAnchors, map);
|
|
487
493
|
if (trace.endAnchors !== undefined && endAnchors === undefined)
|
|
488
494
|
return undefined;
|
|
495
|
+
const goneAnchors = trace.goneAnchors === undefined ? undefined : mapDescriptors(trace.goneAnchors, map);
|
|
496
|
+
if (trace.goneAnchors !== undefined && goneAnchors === undefined)
|
|
497
|
+
return undefined;
|
|
489
498
|
return {
|
|
490
499
|
...trace,
|
|
491
500
|
actions,
|
|
@@ -493,6 +502,7 @@ export function mapTraceText(trace, map) {
|
|
|
493
502
|
...(startPath === undefined ? {} : { startPath }),
|
|
494
503
|
...(endPath === undefined ? {} : { endPath }),
|
|
495
504
|
...(endAnchors === undefined ? {} : { endAnchors }),
|
|
505
|
+
...(goneAnchors === undefined ? {} : { goneAnchors }),
|
|
496
506
|
};
|
|
497
507
|
}
|
|
498
508
|
function mapDescriptors(descriptors, map) {
|
|
@@ -521,8 +531,23 @@ function readDescriptor(document) {
|
|
|
521
531
|
return undefined;
|
|
522
532
|
descriptor['position'] = position;
|
|
523
533
|
}
|
|
534
|
+
if (raw['states'] !== undefined) {
|
|
535
|
+
const states = readStates(raw['states']);
|
|
536
|
+
if (states === undefined)
|
|
537
|
+
return undefined;
|
|
538
|
+
descriptor['states'] = states;
|
|
539
|
+
}
|
|
524
540
|
return descriptor;
|
|
525
541
|
}
|
|
542
|
+
/** Known anchor states, each once, in sorted order, as `anchors.ts` writes them; an empty list is never written. */
|
|
543
|
+
function readStates(document) {
|
|
544
|
+
if (!Array.isArray(document) || document.length === 0)
|
|
545
|
+
return undefined;
|
|
546
|
+
const states = each(document, (state) => (isTraceAnchorState(state) ? state : undefined));
|
|
547
|
+
if (states === undefined)
|
|
548
|
+
return undefined;
|
|
549
|
+
return states.every((state, index) => index === 0 || states[index - 1] < state) ? states : undefined;
|
|
550
|
+
}
|
|
526
551
|
/** A well-formed position: two safe integers with the index inside the count. */
|
|
527
552
|
function readPosition(document) {
|
|
528
553
|
if (typeof document !== 'object' || document === null || Array.isArray(document))
|
|
@@ -549,7 +574,24 @@ function readProvenance(document) {
|
|
|
549
574
|
return undefined;
|
|
550
575
|
if (typeof instructionDigest !== 'string' || !SHA256_HEX.test(instructionDigest))
|
|
551
576
|
return undefined;
|
|
552
|
-
|
|
577
|
+
const paramsDigest = raw['paramsDigest'];
|
|
578
|
+
if (paramsDigest !== undefined && (typeof paramsDigest !== 'string' || !SHA256_HEX.test(paramsDigest)))
|
|
579
|
+
return undefined;
|
|
580
|
+
const callIndex = raw['callIndex'];
|
|
581
|
+
if (callIndex !== undefined && (typeof callIndex !== 'number' || !Number.isSafeInteger(callIndex) || callIndex < 0)) {
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
const agent = raw['agent'] === undefined ? undefined : readBoundedText(raw['agent'], MAX_TRACE_DESCRIPTOR_CHARS);
|
|
585
|
+
if (raw['agent'] !== undefined && agent === undefined)
|
|
586
|
+
return undefined;
|
|
587
|
+
return {
|
|
588
|
+
testId,
|
|
589
|
+
targetId,
|
|
590
|
+
instructionDigest,
|
|
591
|
+
...(paramsDigest === undefined ? {} : { paramsDigest }),
|
|
592
|
+
...(callIndex === undefined ? {} : { callIndex }),
|
|
593
|
+
...(agent === undefined ? {} : { agent }),
|
|
594
|
+
};
|
|
553
595
|
}
|
|
554
596
|
function readBoundedText(value, maxChars) {
|
|
555
597
|
if (typeof value !== 'string' || value.trim() === '' || value.length > maxChars) {
|
|
@@ -570,3 +612,7 @@ function readInputText(value) {
|
|
|
570
612
|
}
|
|
571
613
|
return value;
|
|
572
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';
|
|
@@ -357,22 +358,22 @@ function createProgram(version, telemetry) {
|
|
|
357
358
|
});
|
|
358
359
|
program
|
|
359
360
|
.command('login')
|
|
360
|
-
.summary('sign in to a ChatGPT, GitHub Copilot, or SuperGrok subscription for agent steps')
|
|
361
|
-
.description('Sign in once to a personal subscription and store the login for the e2e/oauth models: openai (ChatGPT Plus/Pro, the Codex sign-in), github-copilot (GitHub Copilot; reuses the GitHub CLI login or runs a device flow for your OAuth App), spacexai (SuperGrok or X Premium+, device code). The config then constructs the model with chatgpt(), copilot(), or grok() from e2e/oauth/<provider>.')
|
|
362
|
-
.addArgument(new Argument('[provider]', 'openai, github-copilot, or spacexai; omitted, a picker').choices(PROVIDER_IDS))
|
|
361
|
+
.summary('sign in to a ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription for agent steps')
|
|
362
|
+
.description('Sign in once to a personal subscription and store the login for the e2e/oauth models: openai (ChatGPT Plus/Pro, the Codex sign-in), github-copilot (GitHub Copilot; reuses the GitHub CLI login or runs a device flow for your OAuth App), opencode-console (OpenCode Console, device code), spacexai (SuperGrok or X Premium+, device code). The config then constructs the model with chatgpt(), copilot(), opencodeConsole(), or grok() from e2e/oauth/<provider>.')
|
|
363
|
+
.addArgument(new Argument('[provider]', 'openai, github-copilot, opencode-console, or spacexai; omitted, a picker').choices(PROVIDER_IDS))
|
|
363
364
|
.option('--device', 'ChatGPT: show a code to enter on another device instead of opening a browser')
|
|
364
365
|
.option('--client-id <id>', 'GitHub Copilot: the client id of your GitHub OAuth App with the device flow enabled')
|
|
365
366
|
.option('--from-gh', 'GitHub Copilot: reuse the token of the signed-in GitHub CLI')
|
|
366
367
|
.option('--enterprise-url <host>', 'GitHub Copilot: the GitHub Enterprise host')
|
|
367
|
-
.addHelpText('after', ['', examples(['e2e login openai', 'e2e login github-copilot --from-gh', 'e2e login spacexai', 'e2e login']), '', docsLine('/subscriptions')].join('\n'))
|
|
368
|
+
.addHelpText('after', ['', examples(['e2e login openai', 'e2e login github-copilot --from-gh', 'e2e login opencode-console', 'e2e login spacexai', 'e2e login']), '', docsLine('/subscriptions')].join('\n'))
|
|
368
369
|
.action(async (provider, options) => {
|
|
369
370
|
process.exitCode = await runLogin(provider, options);
|
|
370
371
|
});
|
|
371
372
|
program
|
|
372
373
|
.command('logout')
|
|
373
374
|
.summary('forget a stored subscription login')
|
|
374
|
-
.description('Remove the stored login of one provider (openai, github-copilot, spacexai). Without a provider, a picker over the stored logins.')
|
|
375
|
-
.addArgument(new Argument('[provider]', 'openai, github-copilot, or spacexai; omitted, a picker').choices(PROVIDER_IDS))
|
|
375
|
+
.description('Remove the stored login of one provider (openai, github-copilot, opencode-console, spacexai). Without a provider, a picker over the stored logins.')
|
|
376
|
+
.addArgument(new Argument('[provider]', 'openai, github-copilot, opencode-console, or spacexai; omitted, a picker').choices(PROVIDER_IDS))
|
|
376
377
|
.addHelpText('after', ['', examples(['e2e logout', 'e2e logout openai']), '', docsLine('/subscriptions')].join('\n'))
|
|
377
378
|
.action(async (provider) => {
|
|
378
379
|
process.exitCode = await runLogout(provider);
|
|
@@ -380,8 +381,8 @@ function createProgram(version, telemetry) {
|
|
|
380
381
|
program
|
|
381
382
|
.command('models')
|
|
382
383
|
.summary('list the models a stored subscription login serves')
|
|
383
|
-
.description('Ask the vendor which models the stored login serves and print their ids, the ones chatgpt(), copilot(), and grok() take. Without a provider, every stored login in turn.')
|
|
384
|
-
.addArgument(new Argument('[provider]', 'openai, github-copilot, or spacexai; omitted, every stored login').choices(PROVIDER_IDS))
|
|
384
|
+
.description('Ask the vendor which models the stored login serves and print their ids, the ones chatgpt(), copilot(), opencodeConsole(), and grok() take. Without a provider, every stored login in turn.')
|
|
385
|
+
.addArgument(new Argument('[provider]', 'openai, github-copilot, opencode-console, or spacexai; omitted, every stored login').choices(PROVIDER_IDS))
|
|
385
386
|
.addHelpText('after', ['', examples(['e2e models', 'e2e models openai']), '', docsLine('/subscriptions#pick-a-model')].join('\n'))
|
|
386
387
|
.action(async (provider) => {
|
|
387
388
|
process.exitCode = await runModels(provider);
|
|
@@ -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')
|
|
@@ -16,9 +16,7 @@ export declare function getEnginePresets(): readonly [{
|
|
|
16
16
|
readonly id: 'web';
|
|
17
17
|
readonly label: 'Web';
|
|
18
18
|
readonly hint: 'Playwright';
|
|
19
|
-
readonly dependencies:
|
|
20
|
-
readonly playwright: string;
|
|
21
|
-
};
|
|
19
|
+
readonly dependencies: Readonly<Record<string, string>>;
|
|
22
20
|
readonly imports: readonly ["import { web } from '@e2e-dev/web';"];
|
|
23
21
|
readonly config: ` targets: [{
|
|
24
22
|
engine: web(),
|
package/dist/cli/init/engines.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** CLI scaffold presets. Engine packages are referenced as generated source, never imported. */
|
|
2
2
|
import os from 'node:os';
|
|
3
|
-
import {
|
|
3
|
+
import { siblingDependency } from './versions.js';
|
|
4
4
|
/** Builds prompt choices and scaffolds with defaults for the machine running init. */
|
|
5
5
|
export function getEnginePresets() {
|
|
6
6
|
const ios = os.platform() === 'darwin';
|
|
@@ -9,7 +9,7 @@ export function getEnginePresets() {
|
|
|
9
9
|
id: 'web',
|
|
10
10
|
label: 'Web',
|
|
11
11
|
hint: 'Playwright',
|
|
12
|
-
dependencies:
|
|
12
|
+
dependencies: siblingDependency('@e2e-dev/web'),
|
|
13
13
|
imports: ["import { web } from '@e2e-dev/web';"],
|
|
14
14
|
config: ` targets: [{
|
|
15
15
|
engine: web(),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* of them and takes the constructed instance. Adding a gateway is adding an
|
|
5
5
|
* entry here.
|
|
6
6
|
*/
|
|
7
|
-
export type GatewayId = 'vercel' | 'openrouter' | 'openai-compatible' | 'chatgpt' | 'copilot' | 'grok';
|
|
7
|
+
export type GatewayId = 'vercel' | 'openrouter' | 'openai-compatible' | 'chatgpt' | 'copilot' | 'opencode-console' | 'grok';
|
|
8
8
|
export interface GatewayPreset {
|
|
9
9
|
readonly id: GatewayId;
|
|
10
10
|
readonly label: string;
|
|
@@ -50,12 +50,22 @@ export const GATEWAYS = [
|
|
|
50
50
|
id: 'copilot',
|
|
51
51
|
label: 'GitHub Copilot subscription',
|
|
52
52
|
hint: 'your Copilot plan: OpenAI, Anthropic, Google, and SpaceXAI models',
|
|
53
|
-
dependencies: { '@ai-sdk/openai-compatible': '^3.0.0' },
|
|
53
|
+
dependencies: { '@ai-sdk/openai-compatible': '^3.0.0', '@ai-sdk/openai': '^4.0.0' },
|
|
54
54
|
import: "import { copilot } from 'e2e/oauth/copilot';",
|
|
55
55
|
comment: 'Your GitHub Copilot subscription serves the model; sign in once with `e2e login github-copilot`, `e2e models github-copilot` lists the ids.',
|
|
56
56
|
login: 'github-copilot',
|
|
57
57
|
model: () => "copilot('claude-sonnet-5')",
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
id: 'opencode-console',
|
|
61
|
+
label: 'OpenCode Console',
|
|
62
|
+
hint: 'the models your OpenCode Console workspace serves',
|
|
63
|
+
dependencies: { '@ai-sdk/openai-compatible': '^3.0.0', '@ai-sdk/openai': '^4.0.0', '@ai-sdk/anthropic': '^4.0.0', '@ai-sdk/google': '^4.0.0' },
|
|
64
|
+
import: "import { opencodeConsole } from 'e2e/oauth/opencode-console';",
|
|
65
|
+
comment: 'Your OpenCode Console workspace serves the model; sign in once with `e2e login opencode-console`, `e2e models opencode-console` lists the ids.',
|
|
66
|
+
login: 'opencode-console',
|
|
67
|
+
model: () => "opencodeConsole('deepseek-v4.1-flash')",
|
|
68
|
+
},
|
|
59
69
|
{
|
|
60
70
|
id: 'grok',
|
|
61
71
|
label: 'SuperGrok subscription',
|
|
@@ -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"}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* build in `sibling-versions.json` next to this module from their manifests.
|
|
4
4
|
* Absent when running from source. Both preset tables read from here.
|
|
5
5
|
*/
|
|
6
|
-
export declare const SIBLING_VERSIONS: Readonly<Record<string, string>> | undefined;
|
|
7
6
|
/**
|
|
8
7
|
* The range init writes for the runner and for an engine. Engines version
|
|
9
8
|
* independently of the runner and pin it through their own peer range, so init
|
|
@@ -20,10 +19,3 @@ export declare const SIBLING_VERSIONS: Readonly<Record<string, string>> | undefi
|
|
|
20
19
|
export declare function dependencyRange(version: string | undefined): string;
|
|
21
20
|
/** The range init writes for a sibling `@e2e-dev/*` package released alongside this runner. */
|
|
22
21
|
export declare function siblingDependency(name: string): Readonly<Record<string, string>>;
|
|
23
|
-
/**
|
|
24
|
-
* The range init writes for `playwright`, which `@e2e-dev/web` peers on
|
|
25
|
-
* rather than installs, so an app that already ships Playwright keeps one copy
|
|
26
|
-
* and one browser cache. The build records the version the engine was built
|
|
27
|
-
* and tested against; from source, where nothing is recorded, any 1.x will do.
|
|
28
|
-
*/
|
|
29
|
-
export declare function playwrightRange(version: string | undefined): string;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Absent when running from source. Both preset tables read from here.
|
|
5
5
|
*/
|
|
6
6
|
import { readJson } from '../../internal/package-version.js';
|
|
7
|
-
|
|
7
|
+
const SIBLING_VERSIONS = readJson(import.meta.url, './sibling-versions.json');
|
|
8
8
|
/**
|
|
9
9
|
* The range init writes for the runner and for an engine. Engines version
|
|
10
10
|
* independently of the runner and pin it through their own peer range, so init
|
|
@@ -27,12 +27,3 @@ export function dependencyRange(version) {
|
|
|
27
27
|
export function siblingDependency(name) {
|
|
28
28
|
return { [name]: dependencyRange(SIBLING_VERSIONS?.[name]) };
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
* The range init writes for `playwright`, which `@e2e-dev/web` peers on
|
|
32
|
-
* rather than installs, so an app that already ships Playwright keeps one copy
|
|
33
|
-
* and one browser cache. The build records the version the engine was built
|
|
34
|
-
* and tested against; from source, where nothing is recorded, any 1.x will do.
|
|
35
|
-
*/
|
|
36
|
-
export function playwrightRange(version) {
|
|
37
|
-
return version === undefined ? '^1' : `^${version}`;
|
|
38
|
-
}
|
package/dist/cli/mcp.d.ts
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* stdio, stdout is the JSON-RPC stream, so anything else that writes there
|
|
4
4
|
* (a stray console.log, a library banner) would corrupt the handshake. The
|
|
5
5
|
* transport keeps the process's own stdout writer, and everything else that
|
|
6
|
-
* reaches process.stdout
|
|
6
|
+
* reaches process.stdout or process.stderr goes to stderr, redacted, for the
|
|
7
|
+
* whole process: user code runs here and may print a secret.
|
|
7
8
|
*/
|
|
8
9
|
import { type ServeOptions } from '../mcp/server.ts';
|
|
9
10
|
import type { Telemetry } from '../telemetry/telemetry.ts';
|
|
10
11
|
export interface McpCommandOptions {
|
|
11
12
|
config?: string | undefined;
|
|
12
13
|
target?: string | undefined;
|
|
13
|
-
|
|
14
|
+
headed?: boolean | undefined;
|
|
14
15
|
maxSessions?: number | undefined;
|
|
15
16
|
}
|
|
16
17
|
/**
|
package/dist/cli/mcp.js
CHANGED
|
@@ -3,30 +3,48 @@
|
|
|
3
3
|
* stdio, stdout is the JSON-RPC stream, so anything else that writes there
|
|
4
4
|
* (a stray console.log, a library banner) would corrupt the handshake. The
|
|
5
5
|
* transport keeps the process's own stdout writer, and everything else that
|
|
6
|
-
* reaches process.stdout
|
|
6
|
+
* reaches process.stdout or process.stderr goes to stderr, redacted, for the
|
|
7
|
+
* whole process: user code runs here and may print a secret.
|
|
7
8
|
*/
|
|
8
9
|
import { Writable } from 'node:stream';
|
|
9
|
-
import { isCiMode } from '../config/resolve.js';
|
|
10
10
|
import { errorMessage, exitCodeForCategory, classifyError } from '../internal/errors.js';
|
|
11
|
+
import { McpOutput } from '../mcp/output.js';
|
|
11
12
|
import { serveMcp } from '../mcp/server.js';
|
|
13
|
+
import { processSecrets } from '../run/secrecy.js';
|
|
12
14
|
import { mcpSessionEvent } from '../telemetry/events.js';
|
|
13
15
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* Takes over the process's stdout and stderr: the returned protocol stream
|
|
17
|
+
* still reaches the real stdout, and every other write to either stream goes
|
|
18
|
+
* through `output` to stderr. The protocol stream wraps the original writer
|
|
19
|
+
* rather than opening file descriptor 1 again: once Node has set up
|
|
17
20
|
* `process.stdout` on a pipe, the descriptor is non-blocking, and a plain
|
|
18
21
|
* `fs` write to it fails with EAGAIN as soon as the client reads slower than
|
|
19
22
|
* the server writes (a screenshot result), killing the server. The socket
|
|
20
23
|
* writer queues and retries instead.
|
|
21
24
|
*/
|
|
22
|
-
function
|
|
25
|
+
function claimStdio() {
|
|
23
26
|
const stdout = process.stdout;
|
|
24
27
|
const stderr = process.stderr;
|
|
25
|
-
const
|
|
26
|
-
|
|
28
|
+
const writeStdout = stdout.write.bind(stdout);
|
|
29
|
+
const writeStderr = stderr.write.bind(stderr);
|
|
30
|
+
const output = new McpOutput(writeStderr, processSecrets);
|
|
31
|
+
for (const [stream, name] of [
|
|
32
|
+
[stdout, 'stdout'],
|
|
33
|
+
[stderr, 'stderr'],
|
|
34
|
+
]) {
|
|
35
|
+
stream.write = ((chunk, encoding, callback) => {
|
|
36
|
+
const done = typeof encoding === 'function' ? encoding : callback;
|
|
37
|
+
const text = typeof chunk === 'string' && typeof encoding === 'string' ? Buffer.from(chunk, encoding) : chunk;
|
|
38
|
+
const accepted = output.write(name, text, done);
|
|
39
|
+
// stderr drains on its own; a writer waiting on stdout is told when stderr has.
|
|
40
|
+
if (!accepted && stream === stdout)
|
|
41
|
+
stderr.once('drain', () => stdout.emit('drain'));
|
|
42
|
+
return accepted;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
27
45
|
const protocol = new Writable({
|
|
28
46
|
write(chunk, _encoding, callback) {
|
|
29
|
-
|
|
47
|
+
writeStdout(chunk, callback);
|
|
30
48
|
},
|
|
31
49
|
});
|
|
32
50
|
// A client that went away fails the pipe with EPIPE; the protocol stream
|
|
@@ -36,7 +54,7 @@ function claimStdout() {
|
|
|
36
54
|
// before its app processes stop.
|
|
37
55
|
stdout.on('error', (error) => protocol.destroy(error));
|
|
38
56
|
stderr.on('error', () => undefined);
|
|
39
|
-
return protocol;
|
|
57
|
+
return { protocol, output };
|
|
40
58
|
}
|
|
41
59
|
/**
|
|
42
60
|
* Records each MCP session as it ends and sends it at once: a server lives
|
|
@@ -51,34 +69,52 @@ export function sessionTelemetry(telemetry) {
|
|
|
51
69
|
}
|
|
52
70
|
/** Runs the server until the client disconnects or a signal arrives; returns the exit code. */
|
|
53
71
|
export async function mcp(version, options, telemetry) {
|
|
54
|
-
const
|
|
72
|
+
const { protocol, output } = claimStdio();
|
|
55
73
|
const stop = new AbortController();
|
|
56
74
|
const onSignal = () => stop.abort();
|
|
75
|
+
// Node would print an error nobody caught (a project tool's unawaited
|
|
76
|
+
// promise) straight to the stderr descriptor, past the redaction, and
|
|
77
|
+
// exit with every session's app still running. It is logged redacted, and
|
|
78
|
+
// held like user output while a config loads, and the server shuts down as
|
|
79
|
+
// on a signal.
|
|
80
|
+
let crashed = false;
|
|
81
|
+
const onUncaught = (cause) => {
|
|
82
|
+
crashed = true;
|
|
83
|
+
const text = cause instanceof Error ? (cause.stack ?? errorMessage(cause)) : errorMessage(cause);
|
|
84
|
+
output.report(`e2e mcp: [error] uncaught: ${text}`);
|
|
85
|
+
stop.abort();
|
|
86
|
+
};
|
|
57
87
|
process.once('SIGINT', onSignal);
|
|
58
88
|
process.once('SIGTERM', onSignal);
|
|
89
|
+
// A rejection nobody handled arrives here too: with no `unhandledRejection` listener, Node raises it as uncaught.
|
|
90
|
+
process.on('uncaughtException', onUncaught);
|
|
59
91
|
try {
|
|
60
|
-
|
|
92
|
+
const code = await serveMcp({
|
|
61
93
|
cwd: process.cwd(),
|
|
62
94
|
configPath: options.config,
|
|
63
95
|
target: options.target,
|
|
64
|
-
headed: options.
|
|
96
|
+
headed: options.headed === true,
|
|
65
97
|
maxSessions: options.maxSessions,
|
|
66
98
|
env: process.env,
|
|
67
99
|
version,
|
|
68
100
|
stdin: process.stdin,
|
|
69
|
-
stdout,
|
|
70
|
-
log: (line) =>
|
|
101
|
+
stdout: protocol,
|
|
102
|
+
log: (line) => output.log(`e2e mcp: ${line}`),
|
|
103
|
+
output,
|
|
71
104
|
signal: stop.signal,
|
|
72
105
|
onSessionEnd: sessionTelemetry(telemetry),
|
|
73
106
|
});
|
|
107
|
+
return crashed ? 1 : code;
|
|
74
108
|
}
|
|
75
109
|
catch (cause) {
|
|
76
110
|
const error = classifyError(cause);
|
|
77
|
-
|
|
111
|
+
output.log(`e2e mcp: ${error.code}: ${processSecrets.redact(errorMessage(cause))}`);
|
|
78
112
|
return exitCodeForCategory(error.category);
|
|
79
113
|
}
|
|
80
114
|
finally {
|
|
115
|
+
output.end();
|
|
81
116
|
process.off('SIGINT', onSignal);
|
|
82
117
|
process.off('SIGTERM', onSignal);
|
|
118
|
+
process.off('uncaughtException', onUncaught);
|
|
83
119
|
}
|
|
84
120
|
}
|