e2e 0.21.1 → 0.21.2

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.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act-validation.js +1 -0
  4. package/dist/agent/act.js +7 -3
  5. package/dist/agent/action-dispatcher.js +30 -8
  6. package/dist/agent/actions.d.ts +2 -0
  7. package/dist/agent/actions.js +5 -2
  8. package/dist/agent/error.js +1 -0
  9. package/dist/agent/executor.d.ts +14 -6
  10. package/dist/agent/index.js +5 -1
  11. package/dist/agent/invocation.d.ts +3 -0
  12. package/dist/agent/model/sdk.d.ts +5 -0
  13. package/dist/agent/model/sdk.js +18 -1
  14. package/dist/agent/observation.js +1 -0
  15. package/dist/agent/primitives.js +14 -3
  16. package/dist/agent/replay.js +9 -3
  17. package/dist/agent/step-cache.d.ts +3 -2
  18. package/dist/agent/step-cache.js +7 -4
  19. package/dist/agent/tool-loop.js +4 -1
  20. package/dist/cache/context.js +1 -1
  21. package/dist/cache/recorder.d.ts +2 -1
  22. package/dist/cache/recorder.js +11 -2
  23. package/dist/cache/trace-fields.d.ts +10 -0
  24. package/dist/cache/trace-fields.js +17 -0
  25. package/dist/cache/trace.d.ts +9 -1
  26. package/dist/cache/trace.js +26 -7
  27. package/dist/cli/feedback.js +2 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/collect/select.js +12 -2
  30. package/dist/config/agent.d.ts +1 -1
  31. package/dist/config/diagnose.js +9 -3
  32. package/dist/config/esm-hooks.js +3 -2
  33. package/dist/config/resolve.js +3 -2
  34. package/dist/config/typescript.js +52 -2
  35. package/dist/engine/contract.d.ts +3 -2
  36. package/dist/engine/manifest.js +2 -2
  37. package/dist/internal/errors.js +5 -2
  38. package/dist/internal/source.js +2 -0
  39. package/dist/internal/unserved-model.d.ts +5 -0
  40. package/dist/internal/unserved-model.js +22 -0
  41. package/dist/locator/screen.js +1 -2
  42. package/dist/oauth/copilot.js +23 -5
  43. package/dist/oauth/device-code.js +35 -19
  44. package/dist/oauth/errors.d.ts +3 -0
  45. package/dist/oauth/errors.js +26 -11
  46. package/dist/oauth/providers/github-copilot.d.ts +4 -0
  47. package/dist/oauth/providers/github-copilot.js +64 -16
  48. package/dist/oauth/providers/openai.d.ts +2 -2
  49. package/dist/oauth/providers/openai.js +47 -19
  50. package/dist/oauth/providers/opencode-console.js +36 -15
  51. package/dist/oauth/providers/xai.js +18 -9
  52. package/dist/oauth/token-endpoint.d.ts +15 -6
  53. package/dist/oauth/token-endpoint.js +18 -6
  54. package/dist/report/code-frame.js +9 -3
  55. package/dist/report/failure-text.d.ts +10 -2
  56. package/dist/report/failure-text.js +24 -8
  57. package/dist/report/list-model.d.ts +13 -0
  58. package/dist/report/list-steps.d.ts +6 -1
  59. package/dist/report/list-steps.js +29 -0
  60. package/dist/report/list.d.ts +1 -0
  61. package/dist/report/list.js +16 -7
  62. package/dist/report/live-window.js +2 -1
  63. package/dist/report/markdown.js +1 -1
  64. package/dist/run/fixtures.js +2 -0
  65. package/dist/run/host-trace.d.ts +3 -2
  66. package/dist/run/host-trace.js +2 -1
  67. package/dist/run/last-run.d.ts +9 -0
  68. package/dist/run/last-run.js +28 -0
  69. package/dist/run/open-session.d.ts +3 -1
  70. package/dist/run/open-session.js +1 -1
  71. package/dist/run/runner.js +6 -2
  72. package/dist/run/sessions.js +3 -3
  73. package/dist/run/standalone-attempt.d.ts +2 -1
  74. package/dist/run/standalone-attempt.js +3 -1
  75. package/dist/run/steps.d.ts +3 -1
  76. package/dist/run/units.d.ts +8 -0
  77. package/dist/run/units.js +14 -0
  78. package/dist/run/worker-models.d.ts +2 -0
  79. package/dist/run/worker-models.js +7 -0
  80. package/dist/types.d.ts +2 -2
  81. package/docs/authentication.mdx +26 -23
  82. package/docs/browser.mdx +7 -4
  83. package/docs/bug-bash.mdx +2 -2
  84. package/docs/cache.mdx +6 -1
  85. package/docs/ci/bitrise.mdx +1 -1
  86. package/docs/ci/codemagic.mdx +2 -2
  87. package/docs/ci/eas-workflows.mdx +1 -1
  88. package/docs/coding-agents.mdx +7 -35
  89. package/docs/debugging.mdx +24 -36
  90. package/docs/executors.mdx +1 -1
  91. package/docs/github.mdx +2 -1
  92. package/docs/integrations/eas.mdx +1 -1
  93. package/docs/integrations/index.mdx +6 -0
  94. package/docs/integrations/lightpanda.mdx +192 -0
  95. package/docs/integrations/testmu.mdx +152 -0
  96. package/docs/mcp.mdx +123 -0
  97. package/docs/migrate/detox.mdx +1 -1
  98. package/docs/migrate/maestro.mdx +101 -105
  99. package/docs/migrate/playwright.mdx +65 -70
  100. package/docs/mobile.mdx +18 -3
  101. package/docs/models.mdx +4 -2
  102. package/docs/quickstart.mdx +7 -0
  103. package/docs/reference/agent.mdx +369 -277
  104. package/docs/reference/app.mdx +141 -32
  105. package/docs/reference/cli.mdx +674 -558
  106. package/docs/reference/config.mdx +1406 -752
  107. package/docs/reference/engine.mdx +1062 -256
  108. package/docs/reference/environment.mdx +156 -84
  109. package/docs/reference/errors.mdx +106 -72
  110. package/docs/reference/expect.mdx +752 -372
  111. package/docs/reference/mcp.mdx +366 -141
  112. package/docs/reference/mobile.mdx +844 -403
  113. package/docs/reference/reporters.mdx +312 -106
  114. package/docs/reference/runner.mdx +373 -174
  115. package/docs/reference/screen.mdx +931 -398
  116. package/docs/reference/security.mdx +106 -40
  117. package/docs/reference/test.mdx +626 -273
  118. package/docs/reference/web.mdx +956 -533
  119. package/docs/security.mdx +6 -5
  120. package/docs/snippets/model-provider.jsx +763 -0
  121. package/docs/snippets/setup-prompt-card.jsx +22 -0
  122. package/docs/snippets/show-more.jsx +22 -0
  123. package/docs/snippets/video-card.jsx +34 -0
  124. package/docs/snippets/video.jsx +25 -0
  125. package/docs/subscriptions.mdx +10 -3
  126. package/docs/telemetry.mdx +39 -39
  127. package/package.json +3 -2
  128. package/schema/report-v1.schema.json +6 -0
  129. package/skills/e2e/references/agent.md +8 -3
  130. package/skills/e2e/references/debugging.md +1 -1
  131. package/skills/e2e/references/mcp.md +1 -1
  132. package/skills/e2e/references/setup.md +12 -3
  133. package/skills/e2e/references/writing-tests.md +1 -1
@@ -15,7 +15,10 @@
15
15
  * and a document this runner cannot replay verbatim comes back as
16
16
  * `undefined`, never as a repaired or partially trusted entry.
17
17
  */
18
+ import { type KeyModifier } from '../engine/contract.ts';
18
19
  import type { ScrollDirection } from '../types.ts';
20
+ import { type RecordedModel } from './trace-fields.ts';
21
+ export type { RecordedModel } from './trace-fields.ts';
19
22
  export declare const TRACE_SCHEMA_VERSION = "trace-1";
20
23
  /** Mirrors the default step action budget; overflow marks the trace truncated. */
21
24
  export declare const MAX_TRACE_ACTIONS = 50;
@@ -121,6 +124,8 @@ interface ActionBase {
121
124
  /** The verbs that act on one node and carry no input: tap and its variants, hover, scroll into view. */
122
125
  declare const NODE_ACTION_NAMES: readonly ['tap', 'doubleTap', 'longPress', 'secondaryTap', 'hover', 'scrollTo'];
123
126
  export type NodeActionName = (typeof NODE_ACTION_NAMES)[number];
127
+ /** Whether a node verb clicks, so it may carry `modifiers`. */
128
+ export declare const isTapActionName: (value: unknown) => value is "doubleTap" | "secondaryTap" | "tap";
124
129
  /**
125
130
  * Whether an action is one of the node verbs. The every-layer switches over
126
131
  * an action union (prose, recording, templating, replay) take these six
@@ -134,6 +139,8 @@ export declare function isNodeAction<A extends {
134
139
  export interface NodeAction extends ActionBase {
135
140
  readonly name: NodeActionName;
136
141
  readonly target: TraceTargetDescriptor;
142
+ /** Keys a tap-family action held for the click; replayed with the action. */
143
+ readonly modifiers?: readonly KeyModifier[];
137
144
  }
138
145
  export interface TypeAction extends ActionBase {
139
146
  readonly name: 'type';
@@ -283,6 +290,8 @@ export interface ActionTrace {
283
290
  };
284
291
  /** Which test, target, and instruction recorded it; absent on older entries. */
285
292
  readonly recordedFor?: TraceProvenance;
293
+ /** Configured model when recorded; absent on older or unattributed entries. */
294
+ readonly recordedBy?: RecordedModel;
286
295
  /** The recorded run's verdict summary. */
287
296
  readonly summary: string;
288
297
  /**
@@ -372,4 +381,3 @@ export type TraceTextMap = (text: string) => string | undefined;
372
381
  * `undefined` from `map` makes the whole result `undefined`.
373
382
  */
374
383
  export declare function mapTraceText(trace: ActionTrace, map: TraceTextMap): ActionTrace | undefined;
375
- export {};
@@ -15,8 +15,10 @@
15
15
  * and a document this runner cannot replay verbatim comes back as
16
16
  * `undefined`, never as a repaired or partially trusted entry.
17
17
  */
18
+ import { isKeyModifier, KEY_MODIFIERS } from '../engine/contract.js';
18
19
  import { timestamp } from '../internal/ids.js';
19
20
  import { bound } from '../internal/text.js';
21
+ import { readBoundedText, readRecordedModel } from './trace-fields.js';
20
22
  export const TRACE_SCHEMA_VERSION = 'trace-1';
21
23
  /** Mirrors the default step action budget; overflow marks the trace truncated. */
22
24
  export const MAX_TRACE_ACTIONS = 50;
@@ -81,6 +83,10 @@ const isTraceAnchorState = oneOf(TRACE_ANCHOR_STATES);
81
83
  /** The verbs that act on one node and carry no input: tap and its variants, hover, scroll into view. */
82
84
  const NODE_ACTION_NAMES = ['tap', 'doubleTap', 'longPress', 'secondaryTap', 'hover', 'scrollTo'];
83
85
  const isNodeActionName = oneOf(NODE_ACTION_NAMES);
86
+ /** The node verbs that click: the only ones that may hold key modifiers. */
87
+ const TAP_ACTION_NAMES = ['tap', 'doubleTap', 'secondaryTap'];
88
+ /** Whether a node verb clicks, so it may carry `modifiers`. */
89
+ export const isTapActionName = oneOf(TAP_ACTION_NAMES);
84
90
  /**
85
91
  * Whether an action is one of the node verbs. The every-layer switches over
86
92
  * an action union (prose, recording, templating, replay) take these six
@@ -172,6 +178,7 @@ function readActionTrace(document) {
172
178
  const recordedFor = recordedForRaw === undefined ? undefined : readProvenance(recordedForRaw);
173
179
  if (recordedForRaw !== undefined && recordedFor === undefined)
174
180
  return undefined;
181
+ const recordedBy = readRecordedModel(raw['recordedBy'], MAX_TRACE_DESCRIPTOR_CHARS);
175
182
  const summary = readBoundedText(raw['summary'], MAX_TRACE_SUMMARY_CHARS);
176
183
  if (summary === undefined)
177
184
  return undefined;
@@ -211,6 +218,7 @@ function readActionTrace(document) {
211
218
  ...(executorVersion === undefined ? {} : { version: executorVersion }),
212
219
  },
213
220
  ...(recordedFor === undefined ? {} : { recordedFor }),
221
+ ...(recordedBy === undefined ? {} : { recordedBy }),
214
222
  summary,
215
223
  ...(startPath === undefined ? {} : { startPath }),
216
224
  ...(endPath === undefined ? {} : { endPath }),
@@ -229,6 +237,18 @@ function readAnchors(document) {
229
237
  return undefined;
230
238
  return each(document, readDescriptor);
231
239
  }
240
+ /** The `modifiers` of a recorded tap-family action: a non-empty list of distinct key modifiers, or undefined when malformed. */
241
+ function readModifiers(document) {
242
+ if (!Array.isArray(document) || document.length === 0 || document.length > KEY_MODIFIERS.length)
243
+ return undefined;
244
+ const modifiers = [];
245
+ for (const value of document) {
246
+ if (!isKeyModifier(value) || modifiers.includes(value))
247
+ return undefined;
248
+ modifiers.push(value);
249
+ }
250
+ return modifiers;
251
+ }
232
252
  function readRecordedAction(document) {
233
253
  if (typeof document !== 'object' || document === null || Array.isArray(document)) {
234
254
  return undefined;
@@ -240,7 +260,12 @@ function readRecordedAction(document) {
240
260
  const name = raw['name'];
241
261
  if (isNodeActionName(name)) {
242
262
  const target = readDescriptor(raw['target']);
243
- return target === undefined ? undefined : { name, summary, target };
263
+ if (target === undefined)
264
+ return undefined;
265
+ if (raw['modifiers'] === undefined)
266
+ return { name, summary, target };
267
+ const modifiers = isTapActionName(name) ? readModifiers(raw['modifiers']) : undefined;
268
+ return modifiers === undefined ? undefined : { name, summary, target, modifiers };
244
269
  }
245
270
  switch (name) {
246
271
  case 'type': {
@@ -635,12 +660,6 @@ function readProvenance(document) {
635
660
  ...(agent === undefined ? {} : { agent }),
636
661
  };
637
662
  }
638
- function readBoundedText(value, maxChars) {
639
- if (typeof value !== 'string' || value.trim() === '' || value.length > maxChars) {
640
- return undefined;
641
- }
642
- return value;
643
- }
644
663
  /** One to `MAX_TRACE_UPLOAD_PATHS` verbatim paths, each a replay input like a typed value. */
645
664
  function readInputPaths(value) {
646
665
  if (!Array.isArray(value) || value.length === 0 || value.length > MAX_TRACE_UPLOAD_PATHS)
@@ -107,7 +107,8 @@ async function latestRelease(fetchImpl) {
107
107
  const response = await fetchImpl(LATEST_URL, { signal: AbortSignal.timeout(LATEST_TIMEOUT_MS) });
108
108
  if (!response.ok)
109
109
  return undefined;
110
- const { version } = (await response.json());
110
+ const body = await response.json();
111
+ const version = typeof body === 'object' && body !== null && 'version' in body ? body.version : undefined;
111
112
  return typeof version === 'string' && RELEASE.test(version) ? version : undefined;
112
113
  }
113
114
  catch {
@@ -1 +1 @@
1
- {"@e2e-dev/web":"0.15.1","@e2e-dev/mobile":"0.12.2"}
1
+ {"@e2e-dev/web":"0.15.2","@e2e-dev/mobile":"0.12.3"}
@@ -592,7 +592,9 @@ function shardItem(pair) {
592
592
  * come from as few files as possible; the slices differ in size by at most
593
593
  * one item, the first shards taking the remainder. Applied after every other
594
594
  * filter and the serial closure, before the setup tests are chosen, so each
595
- * shard brings only the setups it needs.
595
+ * shard brings only the setups it needs. A skipped test is not an item: the
596
+ * shard holding the item before it in report order reports it, the first
597
+ * shard when none comes before, so the shards together report it once.
596
598
  */
597
599
  function applyShard(pairs, shard) {
598
600
  const items = shardItems(pairs);
@@ -601,8 +603,16 @@ function applyShard(pairs, shard) {
601
603
  const from = (shard.index - 1) * size + Math.min(shard.index - 1, remainder);
602
604
  const to = from + size + (shard.index - 1 < remainder ? 1 : 0);
603
605
  const kept = new Set(items.slice(from, to));
606
+ let previousKept = shard.index === 1;
604
607
  return pairs.map((pair) => {
605
- if (pair.disposition !== 'run' || pair.test.kind !== 'test' || kept.has(shardItem(pair)))
608
+ if (pair.test.kind !== 'test')
609
+ return pair;
610
+ if (pair.disposition === 'filtered')
611
+ return pair;
612
+ if (pair.disposition === 'skip')
613
+ return previousKept ? pair : { ...pair, disposition: 'filtered' };
614
+ previousKept = kept.has(shardItem(pair));
615
+ if (previousKept)
606
616
  return pair;
607
617
  return { ...pair, disposition: 'filtered', skip: { cause: 'filtered', reason: FILTERED_REASON.shard } };
608
618
  });
@@ -22,7 +22,7 @@ export interface ResolvedModel {
22
22
  readonly model: ModelInstance;
23
23
  }
24
24
  /** A resolved model as reports and diagnostics name it: the instance's `provider/model-id`. */
25
- export declare function modelLabel(model: ResolvedModel): string;
25
+ export declare function modelLabel(model: Pick<ResolvedModel, 'provider' | 'id'>): string;
26
26
  export interface ResolvedAgentConfig {
27
27
  /**
28
28
  * The custom brain `agent.act()` dispatches to (`agents.<name>.executor`);
@@ -8,6 +8,7 @@ import { existsSync, readFileSync } from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { withHint } from '../internal/errors.js';
11
+ import { isPlainObject } from '../internal/options.js';
11
12
  import { addDevDependencyCommand, detectPackageManager } from '../internal/package-manager.js';
12
13
  import { didYouMean, suggest } from '../internal/suggest.js';
13
14
  /** The runtime exports of `e2e`; a unit test keeps this list equal to the real module. */
@@ -104,9 +105,12 @@ function subpathHint(subpath, manifestPath) {
104
105
  catch {
105
106
  return '';
106
107
  }
107
- const name = typeof parsed.name === 'string' ? parsed.name : path.basename(path.dirname(manifestPath));
108
- const entries = typeof parsed.exports === 'object' && parsed.exports !== null && !Array.isArray(parsed.exports)
109
- ? Object.keys(parsed.exports).filter((key) => key.startsWith('.'))
108
+ if (!isPlainObject(parsed))
109
+ return '';
110
+ const name = typeof parsed['name'] === 'string' ? parsed['name'] : path.basename(path.dirname(manifestPath));
111
+ const exported = parsed['exports'];
112
+ const entries = typeof exported === 'object' && exported !== null && !Array.isArray(exported)
113
+ ? Object.keys(exported).filter((key) => key.startsWith('.'))
110
114
  : ['.'];
111
115
  const specifiers = entries.map((entry) => (entry === '.' ? name : `${name}/${entry.slice(2)}`));
112
116
  const attempted = `${name}/${subpath.replace(/^\.\//, '')}`;
@@ -136,6 +140,8 @@ function nearestManifest(dir) {
136
140
  if (existsSync(manifestPath)) {
137
141
  try {
138
142
  const parsed = JSON.parse(readFileSync(manifestPath, 'utf8'));
143
+ if (!isPlainObject(parsed))
144
+ return undefined;
139
145
  const dependencies = new Set();
140
146
  for (const block of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
141
147
  const section = parsed[block];
@@ -89,13 +89,14 @@ function directoryMain(directory, view) {
89
89
  const manifest = path.join(directory, 'package.json');
90
90
  if (!view.isFile(manifest))
91
91
  return undefined;
92
- let main;
92
+ let parsed;
93
93
  try {
94
- ({ main } = JSON.parse(readFileSync(manifest, 'utf8')));
94
+ parsed = JSON.parse(readFileSync(manifest, 'utf8'));
95
95
  }
96
96
  catch {
97
97
  return undefined;
98
98
  }
99
+ const main = typeof parsed === 'object' && parsed !== null && 'main' in parsed ? parsed.main : undefined;
99
100
  return typeof main === 'string' && main !== '' ? path.resolve(directory, main) : undefined;
100
101
  }
101
102
  /**
@@ -555,8 +555,9 @@ function resolveProjectId(explicit, projectRoot) {
555
555
  if (existsSync(packageJsonPath)) {
556
556
  try {
557
557
  const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
558
- if (typeof parsed.name === 'string' && parsed.name.length > 0)
559
- return parsed.name;
558
+ const name = isRecord(parsed) ? parsed['name'] : undefined;
559
+ if (typeof name === 'string' && name.length > 0)
560
+ return name;
560
561
  }
561
562
  catch {
562
563
  // fall through to hashed identity
@@ -11,6 +11,8 @@ import remapping, {} from '@jridgewell/remapping';
11
11
  import { createRequire } from 'node:module';
12
12
  import { pathToFileURL } from 'node:url';
13
13
  import { InfrastructureError } from '../internal/errors.js';
14
+ import { obj } from '../internal/objects.js';
15
+ import { isPlainObject } from '../internal/options.js';
14
16
  import { HOOKED_REQUIRE_KEY } from './compiled-files.js';
15
17
  const ownRequire = createRequire(import.meta.url);
16
18
  /** oxc's transformer and parser, loaded on first use. */
@@ -220,6 +222,53 @@ function withOwnHelpers(code, helpers, format) {
220
222
  }
221
223
  /** An inline source map at the end of `source`, as another loader earlier in the chain leaves one. */
222
224
  const INLINE_MAP = /\n\/\/# sourceMappingURL=data:application\/json(?:;charset=utf-8)?;base64,([A-Za-z0-9+/=]+)\s*$/;
225
+ /** Whether `value` is an array whose every entry passes `is`. */
226
+ function isArrayOf(value, is) {
227
+ return Array.isArray(value) && value.every(is);
228
+ }
229
+ /** Whether `entry` is a string. */
230
+ function isString(entry) {
231
+ return typeof entry === 'string';
232
+ }
233
+ /** Whether `entry` is a string or null, as a source map's `sources` and `sourcesContent` hold. */
234
+ function isStringOrNull(entry) {
235
+ return entry === null || typeof entry === 'string';
236
+ }
237
+ /** Whether `entry` is an integer, as a source map's `ignoreList` holds. */
238
+ function isInteger(entry) {
239
+ return Number.isInteger(entry);
240
+ }
241
+ /**
242
+ * The source map an earlier loader inlined as base64 JSON, rebuilt from the
243
+ * fields composing it reads, or undefined when it is malformed. A malformed
244
+ * map is left out rather than failing the import, so positions lead to the
245
+ * code that loader handed over.
246
+ */
247
+ function inlineSourceMap(base64) {
248
+ let parsed;
249
+ try {
250
+ parsed = JSON.parse(Buffer.from(base64, 'base64').toString('utf8'));
251
+ }
252
+ catch {
253
+ return undefined;
254
+ }
255
+ if (!isPlainObject(parsed))
256
+ return undefined;
257
+ const { mappings, sources } = parsed;
258
+ const names = parsed['names'] ?? [];
259
+ const sourceRoot = parsed['sourceRoot'] ?? undefined;
260
+ const sourcesContent = parsed['sourcesContent'] ?? undefined;
261
+ const ignoreList = parsed['ignoreList'] ?? parsed['x_google_ignoreList'] ?? undefined;
262
+ if (typeof mappings !== 'string' ||
263
+ !isArrayOf(sources, isStringOrNull) ||
264
+ !isArrayOf(names, isString) ||
265
+ (sourceRoot !== undefined && typeof sourceRoot !== 'string') ||
266
+ (sourcesContent !== undefined && !isArrayOf(sourcesContent, isStringOrNull)) ||
267
+ (ignoreList !== undefined && !isArrayOf(ignoreList, isInteger))) {
268
+ return undefined;
269
+ }
270
+ return { version: 3, mappings, sources, names, ...obj({ sourceRoot, sourcesContent, ignoreList }) };
271
+ }
223
272
  /**
224
273
  * `source`, the content of `file` as the next loader handed it over,
225
274
  * compiled to JavaScript for its kind with `compilerOptions`, with an
@@ -247,7 +296,8 @@ export function compileTypeScript(file, source, kind, compilerOptions) {
247
296
  ({ code, mappings } = runnableCommonJs(file, code, mappings));
248
297
  // Absolute, so a frame names the file without the loader's query, whatever characters its name holds.
249
298
  const own = { version: 3, mappings, names: result.map?.names ?? [], sources: [pathToFileURL(file).href] };
250
- const incoming = INLINE_MAP.exec(source)?.[1];
251
- const map = incoming === undefined ? own : remapping([own, JSON.parse(Buffer.from(incoming, 'base64').toString('utf8'))], () => null);
299
+ const encoded = INLINE_MAP.exec(source)?.[1];
300
+ const incoming = encoded === undefined ? undefined : inlineSourceMap(encoded);
301
+ const map = incoming === undefined ? own : remapping([own, incoming], () => null);
252
302
  return `${code}\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(map)).toString('base64')}\n`;
253
303
  }
@@ -148,8 +148,9 @@ export interface SemanticNode {
148
148
  * The node's test id, when the platform gives it one: the value of the
149
149
  * project's test-id attribute on a document platform, an accessibility
150
150
  * identifier on iOS, a resource id on Android, an automation id on a
151
- * desktop. The `testId` query resolves against exactly this field, so the
152
- * harness never knows where a platform keeps it.
151
+ * desktop. The `testId` query resolves against this field, so the harness
152
+ * never knows where a platform keeps it. An engine may also match the id
153
+ * without its platform's namespacing.
153
154
  */
154
155
  readonly testId?: string;
155
156
  /** What a field takes, deciding which secrets may fill it. */
@@ -155,7 +155,7 @@ export function defineEngine(spec) {
155
155
  }
156
156
  const name = spec.name;
157
157
  if (typeof spec.version !== 'string' || spec.version.trim() === '') {
158
- throw invalid(name, 'version must be a non-empty string; it is provenance and keys the replay cache');
158
+ throw invalid(name, 'version must be a non-empty string; it is recorded in reports and keys saved sessions');
159
159
  }
160
160
  if (spec.spiVersion !== ENGINE_SPI_VERSION) {
161
161
  throw invalid(name, `declares spiVersion ${String(spec.spiVersion)}; this runner supports ${ENGINE_SPI_VERSION}`);
@@ -285,7 +285,7 @@ export function defineEngine(spec) {
285
285
  handle['secrets'] = declaredSecrets(name, spec.secrets);
286
286
  if (spec.session !== undefined)
287
287
  handle['session'] = hookManifest(name, 'session', spec.session);
288
- // Assembled key by key above, so the record is an Engine by construction.
288
+ // oxlint-disable-next-line e2e/no-chained-type-assertions -- assembled key by key above, so the record is an Engine by construction
289
289
  return Object.freeze({
290
290
  ...handle,
291
291
  [engineBrand]: true,
@@ -136,10 +136,10 @@ export function asEngineError(value) {
136
136
  return value;
137
137
  if (!(value instanceof Error) || value.name !== 'EngineError')
138
138
  return undefined;
139
- const code = value.code;
139
+ const code = 'code' in value ? value.code : undefined;
140
140
  if (!isEngineErrorCode(code))
141
141
  return undefined;
142
- const retryable = value.retryable === true;
142
+ const retryable = 'retryable' in value && value.retryable === true;
143
143
  if (retryable && !RETRYABLE_ENGINE_ERROR_CODES.has(code)) {
144
144
  return { code: 'ENGINE_FAILURE', message: value.message, retryable: false };
145
145
  }
@@ -178,8 +178,11 @@ const CATEGORIES = new Set([
178
178
  */
179
179
  export function isE2EError(value) {
180
180
  return (value instanceof Error &&
181
+ E2E_ERROR_MARKER in value &&
181
182
  value[E2E_ERROR_MARKER] === true &&
183
+ 'category' in value &&
182
184
  CATEGORIES.has(value.category) &&
185
+ 'code' in value &&
183
186
  typeof value.code === 'string');
184
187
  }
185
188
  export class ConfigurationError extends E2EError {
@@ -35,6 +35,8 @@ export function userFrame(stack, projectRoot) {
35
35
  if (match === null)
36
36
  continue;
37
37
  const [, location, line, column] = match;
38
+ if (location === undefined || line === undefined || column === undefined)
39
+ continue;
38
40
  const withoutQuery = location.split('?')[0];
39
41
  let file;
40
42
  try {
@@ -0,0 +1,5 @@
1
+ /** A model constructor's mark on a provider failure meaning the account cannot use the model, with the explanation a user reads; a well-known symbol, so it survives two loaded copies of the package. */
2
+ /** Marks `failure` as a model the account cannot use, with the explanation a user reads, and returns it. */
3
+ export declare function markUnservedModel<Failure extends Error>(failure: Failure, explanation: string): Failure;
4
+ /** The explanation `failure`, or the last attempt of its spent retry chain, was marked with; read by shape, before any SDK loaded. */
5
+ export declare function unservedModelExplanation(failure: unknown): string | undefined;
@@ -0,0 +1,22 @@
1
+ /** A model constructor's mark on a provider failure meaning the account cannot use the model, with the explanation a user reads; a well-known symbol, so it survives two loaded copies of the package. */
2
+ const UNSERVED_MODEL = Symbol.for('e2e.unserved-model.v1');
3
+ /** Marks `failure` as a model the account cannot use, with the explanation a user reads, and returns it. */
4
+ export function markUnservedModel(failure, explanation) {
5
+ Object.defineProperty(failure, UNSERVED_MODEL, { value: explanation });
6
+ return failure;
7
+ }
8
+ /** The explanation `failure`, or the last attempt of its spent retry chain, was marked with; read by shape, before any SDK loaded. */
9
+ export function unservedModelExplanation(failure) {
10
+ const attempt = lastAttempt(failure);
11
+ if (!(attempt instanceof Error))
12
+ return undefined;
13
+ const explanation = Reflect.get(attempt, UNSERVED_MODEL);
14
+ return typeof explanation === 'string' ? explanation : undefined;
15
+ }
16
+ /** The last attempt of a spent retry chain, else `failure` itself; a chain the deadline cut short stays whole. */
17
+ function lastAttempt(failure) {
18
+ if (!(failure instanceof Error) || failure.name !== 'AI_RetryError')
19
+ return failure;
20
+ const chain = failure;
21
+ return chain.reason !== 'abort' && chain.lastError !== undefined ? chain.lastError : failure;
22
+ }
@@ -12,7 +12,7 @@ import { obj } from '../internal/objects.js';
12
12
  import { isTextMatch, normalizeText } from '../internal/text.js';
13
13
  import { attributeOf, denySecureRead, isNodeVisible, locatorDetails } from './engine.js';
14
14
  import { describeExpression, filterExpression, indexExpression, roleQuery, testIdQuery, textQuery, } from './expression.js';
15
- import { cutOffAtDeadline, isOperationTimeout, POLL_INTERVAL_MS, pollCondition, sleep } from '../internal/time.js';
15
+ import { cutOffAtDeadline, isOperationTimeout, POLL_INTERVAL_MS, pollCondition } from '../internal/time.js';
16
16
  import { SampleHistory } from '../expect/samples.js';
17
17
  /**
18
18
  * Internals hang off the locator object itself under a global symbol so that
@@ -194,7 +194,6 @@ class ScreenImpl {
194
194
  }
195
195
  throw cause;
196
196
  }
197
- await sleep(Math.min(POLL_INTERVAL_MS, deadline.remaining()), engine.signal);
198
197
  }
199
198
  });
200
199
  }
@@ -10,8 +10,9 @@ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
10
10
  import { OAuthError } from './errors.js';
11
11
  import { createOAuthFetch } from './fetch.js';
12
12
  import { USER_AGENT } from '../internal/client-identity.js';
13
+ import { markUnservedModel } from '../internal/unserved-model.js';
13
14
  import { loginHint } from './providers.js';
14
- import { COPILOT_API_URL, copilotProtocolFor, createCopilotProvider } from './providers/github-copilot.js';
15
+ import { COPILOT_API_URL, copilotProtocolFor, copilotRefusal, createCopilotProvider, isCopilotRefusal } from './providers/github-copilot.js';
15
16
  import { withoutServerStorage } from './responses.js';
16
17
  import { defaultCredentialStore } from './store.js';
17
18
  /** How long the first call waits on the model listing before it calls over chat and asks again next time. */
@@ -30,7 +31,7 @@ export function copilot(modelId) {
30
31
  fetch,
31
32
  includeUsage: true,
32
33
  }).chatModel(modelId);
33
- return protocolDelegate(chat, () => responsesModel(modelId, fetch), () => copilotProtocolFor(modelId, fetch, AbortSignal.timeout(LISTING_TIMEOUT_MS)));
34
+ return protocolDelegate(chat, () => responsesModel(modelId, fetch), () => copilotProtocolFor(modelId, fetch, AbortSignal.timeout(LISTING_TIMEOUT_MS)), modelId);
34
35
  }
35
36
  /**
36
37
  * The Responses model for `modelId`. `@ai-sdk/openai` is imported only here,
@@ -52,8 +53,9 @@ async function responsesModel(modelId, fetch) {
52
53
  * own timeout, so a caller that gives up never fails the others. Only a
53
54
  * listing that was read is remembered: when it could not be read, that call
54
55
  * goes over chat, the endpoint most models use, and the next call asks again.
56
+ * A call Copilot refuses with `model_not_supported` is marked for the runner.
55
57
  */
56
- function protocolDelegate(chat, responses, lookup) {
58
+ function protocolDelegate(chat, responses, lookup, modelId) {
57
59
  let resolved;
58
60
  let pending;
59
61
  const settle = () => {
@@ -70,6 +72,8 @@ function protocolDelegate(chat, responses, lookup) {
70
72
  });
71
73
  return pending;
72
74
  };
75
+ /** The failure as the runner reads it: a refusal of the model to this account carries the explanation. */
76
+ const refused = (cause) => (isCopilotRefusal(cause) ? markUnservedModel(cause, copilotRefusal(modelId)) : cause);
73
77
  return {
74
78
  specificationVersion: chat.specificationVersion,
75
79
  get provider() {
@@ -81,8 +85,22 @@ function protocolDelegate(chat, responses, lookup) {
81
85
  get supportedUrls() {
82
86
  return resolved?.supportedUrls ?? chat.supportedUrls;
83
87
  },
84
- doGenerate: async (options) => (await untilAborted(settle(), options.abortSignal)).doGenerate(options),
85
- doStream: async (options) => (await untilAborted(settle(), options.abortSignal)).doStream(options),
88
+ doGenerate: async (options) => {
89
+ try {
90
+ return await (await untilAborted(settle(), options.abortSignal)).doGenerate(options);
91
+ }
92
+ catch (cause) {
93
+ throw refused(cause);
94
+ }
95
+ },
96
+ doStream: async (options) => {
97
+ try {
98
+ return await (await untilAborted(settle(), options.abortSignal)).doStream(options);
99
+ }
100
+ catch (cause) {
101
+ throw refused(cause);
102
+ }
103
+ },
86
104
  };
87
105
  }
88
106
  /** Waits for `promise` unless `signal` aborts first, leaving the shared promise running for other callers. */
@@ -5,8 +5,9 @@
5
5
  * `rfc8628Flow` is the standard shape itself, which GitHub and SpaceXAI both
6
6
  * speak apart from the status code they answer "pending" with.
7
7
  */
8
- import { OAuthError, describeResponse } from './errors.js';
9
- import { positiveSeconds, postForm } from './token-endpoint.js';
8
+ import { z } from 'zod';
9
+ import { OAuthError, describeResponse, readJsonBody } from './errors.js';
10
+ import { parseTokens, positiveSeconds, postForm } from './token-endpoint.js';
10
11
  const DEFAULT_INTERVAL_S = 5;
11
12
  const MIN_INTERVAL_MS = 1_000;
12
13
  const SLOW_DOWN_INCREMENT_MS = 5_000;
@@ -69,6 +70,23 @@ function throwIfCancelled(signal) {
69
70
  if (signal?.aborted)
70
71
  throw new OAuthError('CANCELLED', 'the login was cancelled');
71
72
  }
73
+ /** The device authorization response, as far as the flow reads it; the timing values stay raw for `positiveSeconds`. */
74
+ const DeviceCodeSchema = z.object({
75
+ device_code: z.string(),
76
+ user_code: z.string(),
77
+ verification_uri: z.string(),
78
+ verification_uri_complete: z.string().optional().catch(undefined),
79
+ expires_in: z.unknown().optional(),
80
+ interval: z.unknown().optional(),
81
+ });
82
+ /** The error fields of a poll that granted nothing; a field of another type, or a body that is not an object, names nothing. */
83
+ const DevicePollErrorSchema = z
84
+ .object({
85
+ error: z.string().optional().catch(undefined),
86
+ error_description: z.string().optional().catch(undefined),
87
+ interval: z.number().optional().catch(undefined),
88
+ })
89
+ .catch({});
72
90
  /** The standard device flow; the poll reads the body before the status, since vendors disagree on the status for "pending". */
73
91
  export function rfc8628Flow(options) {
74
92
  const { vendor, clientId, callbacks } = options;
@@ -78,37 +96,35 @@ export function rfc8628Flow(options) {
78
96
  const response = await postForm(options.deviceCodeUrl, { client_id: clientId, ...options.request }, signal);
79
97
  if (!response.ok)
80
98
  throw new OAuthError('FLOW_FAILED', `${vendor} device login could not start: ${await describeResponse(response)}`);
81
- const json = (await response.json());
82
- const { device_code, user_code, verification_uri, verification_uri_complete } = json;
83
- if (typeof device_code !== 'string' || typeof user_code !== 'string' || typeof verification_uri !== 'string') {
84
- throw new OAuthError('FLOW_FAILED', `the ${vendor} device code response is missing fields`);
85
- }
99
+ const device = await readJsonBody(response, DeviceCodeSchema, `the ${vendor} device code response`, 'is missing fields');
86
100
  return {
87
- deviceCode: device_code,
88
- userCode: user_code,
89
- verificationUri: verification_uri,
90
- ...(typeof verification_uri_complete === 'string' ? { verificationUriComplete: verification_uri_complete } : {}),
91
- expiresIn: Number(json['expires_in']),
92
- interval: Number(json['interval']),
101
+ deviceCode: device.device_code,
102
+ userCode: device.user_code,
103
+ verificationUri: device.verification_uri,
104
+ ...(device.verification_uri_complete === undefined ? {} : { verificationUriComplete: device.verification_uri_complete }),
105
+ expiresIn: Number(device.expires_in),
106
+ interval: Number(device.interval),
93
107
  };
94
108
  },
95
109
  async poll(authorization, signal) {
96
110
  const response = await postForm(options.tokenUrl, { grant_type: 'urn:ietf:params:oauth:grant-type:device_code', client_id: clientId, device_code: authorization.deviceCode }, signal);
97
- const json = (await response.json().catch(() => ({})));
98
- if (typeof json['access_token'] === 'string' && json['access_token'] !== '')
99
- return { status: 'granted', value: json };
100
- switch (json['error']) {
111
+ const body = await response.json().catch(() => undefined);
112
+ const tokens = parseTokens(body);
113
+ if (tokens !== undefined)
114
+ return { status: 'granted', value: tokens };
115
+ const { error, error_description, interval } = DevicePollErrorSchema.parse(body);
116
+ switch (error) {
101
117
  case 'authorization_pending':
102
118
  return { status: 'pending' };
103
119
  case 'slow_down':
104
- return typeof json['interval'] === 'number' ? { status: 'slow_down', intervalSeconds: json['interval'] } : { status: 'slow_down' };
120
+ return interval === undefined ? { status: 'slow_down' } : { status: 'slow_down', intervalSeconds: interval };
105
121
  case 'access_denied':
106
122
  case 'authorization_denied':
107
123
  return { status: 'denied' };
108
124
  case 'expired_token':
109
125
  return { status: 'expired' };
110
126
  default:
111
- throw new OAuthError('FLOW_FAILED', `${vendor} device login failed: ${String(json['error_description'] ?? json['error'] ?? response.status)}`);
127
+ throw new OAuthError('FLOW_FAILED', `${vendor} device login failed: ${error_description ?? error ?? response.status}`);
112
128
  }
113
129
  },
114
130
  });
@@ -1,3 +1,4 @@
1
+ import { z } from 'zod';
1
2
  export type OAuthErrorCode =
2
3
  /** No stored credentials for the provider. */
3
4
  'NOT_LOGGED_IN'
@@ -20,3 +21,5 @@ export declare class OAuthError extends Error {
20
21
  }
21
22
  /** Reads an error body for a message without assuming JSON. */
22
23
  export declare function describeResponse(response: Response): Promise<string>;
24
+ /** A response's or request's JSON body checked against `schema`; FLOW_FAILED as `<subject> is not JSON` or `<subject> <invalid>` otherwise. */
25
+ export declare function readJsonBody<T>(source: Pick<Response, 'text'>, schema: z.ZodType<T>, subject: string, invalid?: string): Promise<T>;