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.
- package/README.md +2 -0
- package/dist/.build.json +1 -1
- package/dist/agent/act-validation.js +1 -0
- package/dist/agent/act.js +7 -3
- package/dist/agent/action-dispatcher.js +30 -8
- package/dist/agent/actions.d.ts +2 -0
- package/dist/agent/actions.js +5 -2
- package/dist/agent/error.js +1 -0
- package/dist/agent/executor.d.ts +14 -6
- package/dist/agent/index.js +5 -1
- package/dist/agent/invocation.d.ts +3 -0
- package/dist/agent/model/sdk.d.ts +5 -0
- package/dist/agent/model/sdk.js +18 -1
- package/dist/agent/observation.js +1 -0
- package/dist/agent/primitives.js +14 -3
- package/dist/agent/replay.js +9 -3
- package/dist/agent/step-cache.d.ts +3 -2
- package/dist/agent/step-cache.js +7 -4
- package/dist/agent/tool-loop.js +4 -1
- package/dist/cache/context.js +1 -1
- package/dist/cache/recorder.d.ts +2 -1
- package/dist/cache/recorder.js +11 -2
- package/dist/cache/trace-fields.d.ts +10 -0
- package/dist/cache/trace-fields.js +17 -0
- package/dist/cache/trace.d.ts +9 -1
- package/dist/cache/trace.js +26 -7
- package/dist/cli/feedback.js +2 -1
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/collect/select.js +12 -2
- package/dist/config/agent.d.ts +1 -1
- package/dist/config/diagnose.js +9 -3
- package/dist/config/esm-hooks.js +3 -2
- package/dist/config/resolve.js +3 -2
- package/dist/config/typescript.js +52 -2
- package/dist/engine/contract.d.ts +3 -2
- package/dist/engine/manifest.js +2 -2
- package/dist/internal/errors.js +5 -2
- package/dist/internal/source.js +2 -0
- package/dist/internal/unserved-model.d.ts +5 -0
- package/dist/internal/unserved-model.js +22 -0
- package/dist/locator/screen.js +1 -2
- package/dist/oauth/copilot.js +23 -5
- package/dist/oauth/device-code.js +35 -19
- package/dist/oauth/errors.d.ts +3 -0
- package/dist/oauth/errors.js +26 -11
- package/dist/oauth/providers/github-copilot.d.ts +4 -0
- package/dist/oauth/providers/github-copilot.js +64 -16
- package/dist/oauth/providers/openai.d.ts +2 -2
- package/dist/oauth/providers/openai.js +47 -19
- package/dist/oauth/providers/opencode-console.js +36 -15
- package/dist/oauth/providers/xai.js +18 -9
- package/dist/oauth/token-endpoint.d.ts +15 -6
- package/dist/oauth/token-endpoint.js +18 -6
- package/dist/report/code-frame.js +9 -3
- package/dist/report/failure-text.d.ts +10 -2
- package/dist/report/failure-text.js +24 -8
- package/dist/report/list-model.d.ts +13 -0
- package/dist/report/list-steps.d.ts +6 -1
- package/dist/report/list-steps.js +29 -0
- package/dist/report/list.d.ts +1 -0
- package/dist/report/list.js +16 -7
- package/dist/report/live-window.js +2 -1
- package/dist/report/markdown.js +1 -1
- package/dist/run/fixtures.js +2 -0
- package/dist/run/host-trace.d.ts +3 -2
- package/dist/run/host-trace.js +2 -1
- package/dist/run/last-run.d.ts +9 -0
- package/dist/run/last-run.js +28 -0
- package/dist/run/open-session.d.ts +3 -1
- package/dist/run/open-session.js +1 -1
- package/dist/run/runner.js +6 -2
- package/dist/run/sessions.js +3 -3
- package/dist/run/standalone-attempt.d.ts +2 -1
- package/dist/run/standalone-attempt.js +3 -1
- package/dist/run/steps.d.ts +3 -1
- package/dist/run/units.d.ts +8 -0
- package/dist/run/units.js +14 -0
- package/dist/run/worker-models.d.ts +2 -0
- package/dist/run/worker-models.js +7 -0
- package/dist/types.d.ts +2 -2
- package/docs/authentication.mdx +26 -23
- package/docs/browser.mdx +7 -4
- package/docs/bug-bash.mdx +2 -2
- package/docs/cache.mdx +6 -1
- package/docs/ci/bitrise.mdx +1 -1
- package/docs/ci/codemagic.mdx +2 -2
- package/docs/ci/eas-workflows.mdx +1 -1
- package/docs/coding-agents.mdx +7 -35
- package/docs/debugging.mdx +24 -36
- package/docs/executors.mdx +1 -1
- package/docs/github.mdx +2 -1
- package/docs/integrations/eas.mdx +1 -1
- package/docs/integrations/index.mdx +6 -0
- package/docs/integrations/lightpanda.mdx +192 -0
- package/docs/integrations/testmu.mdx +152 -0
- package/docs/mcp.mdx +123 -0
- package/docs/migrate/detox.mdx +1 -1
- package/docs/migrate/maestro.mdx +101 -105
- package/docs/migrate/playwright.mdx +65 -70
- package/docs/mobile.mdx +18 -3
- package/docs/models.mdx +4 -2
- package/docs/quickstart.mdx +7 -0
- package/docs/reference/agent.mdx +369 -277
- package/docs/reference/app.mdx +141 -32
- package/docs/reference/cli.mdx +674 -558
- package/docs/reference/config.mdx +1406 -752
- package/docs/reference/engine.mdx +1062 -256
- package/docs/reference/environment.mdx +156 -84
- package/docs/reference/errors.mdx +106 -72
- package/docs/reference/expect.mdx +752 -372
- package/docs/reference/mcp.mdx +366 -141
- package/docs/reference/mobile.mdx +844 -403
- package/docs/reference/reporters.mdx +312 -106
- package/docs/reference/runner.mdx +373 -174
- package/docs/reference/screen.mdx +931 -398
- package/docs/reference/security.mdx +106 -40
- package/docs/reference/test.mdx +626 -273
- package/docs/reference/web.mdx +956 -533
- package/docs/security.mdx +6 -5
- package/docs/snippets/model-provider.jsx +763 -0
- package/docs/snippets/setup-prompt-card.jsx +22 -0
- package/docs/snippets/show-more.jsx +22 -0
- package/docs/snippets/video-card.jsx +34 -0
- package/docs/snippets/video.jsx +25 -0
- package/docs/subscriptions.mdx +10 -3
- package/docs/telemetry.mdx +39 -39
- package/package.json +3 -2
- package/schema/report-v1.schema.json +6 -0
- package/skills/e2e/references/agent.md +8 -3
- package/skills/e2e/references/debugging.md +1 -1
- package/skills/e2e/references/mcp.md +1 -1
- package/skills/e2e/references/setup.md +12 -3
- package/skills/e2e/references/writing-tests.md +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* each block to its page. Both read the facts from here, so the two never disagree.
|
|
9
9
|
*/
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { modelLabel } from '../config/agent.js';
|
|
11
12
|
import { isLoopbackHost } from '../internal/urls.js';
|
|
12
13
|
import { cell, code, formatDuration, link, MAX_CELL_CHARS, MAX_ID_CHARS, MAX_LABEL_CHARS, MAX_PATH_CHARS, MAX_TITLE_CHARS, plural } from './markdown-text.js';
|
|
13
14
|
import { repeatSuffix } from './format.js';
|
|
@@ -86,11 +87,25 @@ export function toldAttempt(result, final) {
|
|
|
86
87
|
}
|
|
87
88
|
return final.final;
|
|
88
89
|
}
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
/**
|
|
91
|
+
* The step whose error ended the attempt, with its position: the step in the
|
|
92
|
+
* error's phase whose own error carries its code and line, else the last with
|
|
93
|
+
* its code, since a caught failure comes earlier, else the step a timeout or
|
|
94
|
+
* interrupt cut.
|
|
95
|
+
*/
|
|
96
|
+
export function failedStepOf(attempt) {
|
|
97
|
+
const error = attempt.error;
|
|
98
|
+
if (error === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
const phase = error.phase;
|
|
101
|
+
const candidates = attempt.steps.flatMap((step, index) => step.status !== 'passed' && (phase === undefined || (step.phase ?? 'body') === phase) ? [{ index, step: step }] : []);
|
|
102
|
+
const alike = candidates.filter(({ step }) => step.error?.code === error.code);
|
|
103
|
+
const line = error.source;
|
|
104
|
+
if (line !== undefined && alike.length > 0) {
|
|
105
|
+
// The step's own error, not its source: that is the call site, a line below the `await` of a split call.
|
|
106
|
+
return alike.findLast(({ step }) => step.error?.source?.file === line.file && step.error.source.line === line.line);
|
|
107
|
+
}
|
|
108
|
+
return alike.at(-1) ?? candidates.findLast(({ step }) => step.status === 'cancelled');
|
|
94
109
|
}
|
|
95
110
|
/**
|
|
96
111
|
* How the attempts compare, when there was more than one: the same code at
|
|
@@ -105,7 +120,7 @@ export function attemptsLine(result, final) {
|
|
|
105
120
|
.filter((attempt) => attempt.status !== 'passed' && attempt.status !== 'skipped' && attempt.status !== 'interrupted' && (attempt.error !== undefined || attempt.steps.length > 0))
|
|
106
121
|
.map((attempt) => ({
|
|
107
122
|
code: cell(attempt.error?.code ?? attempt.status, 128),
|
|
108
|
-
step: failedStepOf(attempt
|
|
123
|
+
step: failedStepOf(attempt)?.index,
|
|
109
124
|
}));
|
|
110
125
|
const [first] = failed;
|
|
111
126
|
if (first === undefined || final.attempts.length < 2 || failed.length < 2)
|
|
@@ -139,7 +154,7 @@ export function failureSource(result, told) {
|
|
|
139
154
|
const fromError = told.error?.source;
|
|
140
155
|
if (fromError !== undefined)
|
|
141
156
|
return fromError;
|
|
142
|
-
const step = failedStepOf(told
|
|
157
|
+
const step = failedStepOf(told)?.step;
|
|
143
158
|
if (step !== undefined && step.source.file !== 'unknown')
|
|
144
159
|
return step.source;
|
|
145
160
|
return result.source;
|
|
@@ -315,6 +330,7 @@ function cacheLine(cache, cacheDir) {
|
|
|
315
330
|
: `no replay: ${CACHE_REASON_TEXT[cache.reason ?? 'no-entry']}`;
|
|
316
331
|
const parts = [
|
|
317
332
|
`cache: ${how}`,
|
|
333
|
+
...(cache.recordedBy === undefined ? [] : [`recorded with ${code(modelLabel({ provider: cache.recordedBy.provider, id: cache.recordedBy.model }), MAX_DETAIL_CHARS)}`]),
|
|
318
334
|
...(cache.detail === undefined ? [] : [cell(cache.detail, MAX_DETAIL_CHARS)]),
|
|
319
335
|
...(cache.write === undefined ? [] : [CACHE_WRITE_TEXT[cache.write]]),
|
|
320
336
|
...(cache.notRecorded === 'param-collision' ? ['nothing recorded: a unique() value collides with another param'] : []),
|
|
@@ -450,7 +466,7 @@ export function renderTracePage(report, result, final, options = {}) {
|
|
|
450
466
|
lines.push(`Passed after ${plural(final.failedAttempts, 'failed attempt')}; this page tells the last failure. `);
|
|
451
467
|
if (lines.at(-1) !== '')
|
|
452
468
|
lines.push('');
|
|
453
|
-
const at = failedStepOf(told
|
|
469
|
+
const at = failedStepOf(told);
|
|
454
470
|
if (told.steps.length > 0) {
|
|
455
471
|
const videos = told.artifacts
|
|
456
472
|
.filter((artifact) => artifact.kind === 'video' && artifact.startedAt !== undefined)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* each consumer renders at its own indent and width when it prints.
|
|
5
5
|
*/
|
|
6
6
|
import type { ResultStatus } from '../run/records.ts';
|
|
7
|
+
import type { RecordedModel } from '../cache/trace.ts';
|
|
7
8
|
import type { StepActivity, StepEvent, StepKind, StepProgress } from '../run/steps.ts';
|
|
8
9
|
import type { AiUsage } from './format.ts';
|
|
9
10
|
/** Step events worth a glance in the live window; polls and policy decisions stay quiet. */
|
|
@@ -60,6 +61,17 @@ export interface RunningTest {
|
|
|
60
61
|
*/
|
|
61
62
|
readonly steps: FinishedStep[];
|
|
62
63
|
}
|
|
64
|
+
/** The model and agent named in the run header. */
|
|
65
|
+
export interface ConfiguredModel {
|
|
66
|
+
readonly label: string;
|
|
67
|
+
readonly agent: string;
|
|
68
|
+
}
|
|
69
|
+
/** One recording mismatch, rendered with its test's finished steps. */
|
|
70
|
+
export interface ReplayNotice {
|
|
71
|
+
readonly api: string;
|
|
72
|
+
readonly label: string;
|
|
73
|
+
readonly source: RecordedModel;
|
|
74
|
+
}
|
|
63
75
|
/** One finished pair, held until its file's block prints. */
|
|
64
76
|
export interface TestLine {
|
|
65
77
|
readonly title: string;
|
|
@@ -71,6 +83,7 @@ export interface TestLine {
|
|
|
71
83
|
readonly firstErrorLine: string | undefined;
|
|
72
84
|
readonly skipReason: string | undefined;
|
|
73
85
|
readonly steps: readonly FinishedStep[];
|
|
86
|
+
readonly replays: readonly ReplayNotice[];
|
|
74
87
|
}
|
|
75
88
|
/**
|
|
76
89
|
* Every pair of one test file on one target: vitest's "test module". Its
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* finished step prints in a CI log, in a file block, and in the live window,
|
|
4
4
|
* each at its own indent, so nothing here knows where the line lands.
|
|
5
5
|
*/
|
|
6
|
+
import type { StepRecord } from '../run/steps.ts';
|
|
6
7
|
import { type Colors } from './format.ts';
|
|
7
|
-
import type { FinishedStep, ShownEvent } from './list-model.ts';
|
|
8
|
+
import type { ConfiguredModel, FinishedStep, ReplayNotice, ShownEvent } from './list-model.ts';
|
|
8
9
|
/** One-line, quoted step label; `maxColumns` clips it further to fit a row. */
|
|
9
10
|
export declare function stepLabel(label: string, maxColumns?: number): string;
|
|
10
11
|
export interface StepLineOptions {
|
|
@@ -18,6 +19,10 @@ export interface StepLineOptions {
|
|
|
18
19
|
* model calls, and a non-passed status.
|
|
19
20
|
*/
|
|
20
21
|
export declare function stepLine(pc: Colors, step: FinishedStep, options?: StepLineOptions): string;
|
|
22
|
+
/** Recording mismatches under one test, deduplicated across its attempts. */
|
|
23
|
+
export declare function replayNotices(attempts: readonly (readonly StepRecord[])[], configured: ConfiguredModel | undefined): ReplayNotice[];
|
|
24
|
+
/** A recording mismatch under its test in the file block. */
|
|
25
|
+
export declare function replayLine(pc: Colors, notice: ReplayNotice): string;
|
|
21
26
|
/**
|
|
22
27
|
* One model turn or tool call under the current step. A tool call reads as
|
|
23
28
|
* the act it performed when the engine described it, else as the tool name
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
* finished step prints in a CI log, in a file block, and in the live window,
|
|
4
4
|
* each at its own indent, so nothing here knows where the line lands.
|
|
5
5
|
*/
|
|
6
|
+
import { modelLabel } from '../config/agent.js';
|
|
6
7
|
import { sanitizeText, truncateUtf8 } from '../internal/errors.js';
|
|
7
8
|
import { collapseText } from '../internal/text.js';
|
|
8
9
|
import { F_CHECK, F_CROSS, fitColumns, formatTime, formatTokens, terminalColumns, visibleWidth, } from './format.js';
|
|
9
10
|
/** Step labels stay one glanceable line; the report holds the full text. */
|
|
10
11
|
const MAX_STEP_LABEL_COLUMNS = 72;
|
|
12
|
+
/** Recording model labels stay on one bounded terminal line. */
|
|
13
|
+
const MAX_RECORDED_MODEL_COLUMNS = 120;
|
|
11
14
|
/** A clipped step line keeps at least this much of its label, however narrow the terminal. */
|
|
12
15
|
const MIN_STEP_LABEL_COLUMNS = 24;
|
|
13
16
|
/** Columns a step line spends around its label: the quotes and the space before the tail. */
|
|
@@ -42,6 +45,32 @@ export function stepLine(pc, step, options = {}) {
|
|
|
42
45
|
: options.maxWidth - visibleWidth(head) - visibleWidth(tail) - LABEL_CHROME;
|
|
43
46
|
return `${head}${stepLabel(step.label, room)} ${tail}`;
|
|
44
47
|
}
|
|
48
|
+
/** Recording mismatches under one test, deduplicated across its attempts. */
|
|
49
|
+
export function replayNotices(attempts, configured) {
|
|
50
|
+
if (configured === undefined)
|
|
51
|
+
return [];
|
|
52
|
+
const notices = new Map();
|
|
53
|
+
for (const steps of attempts) {
|
|
54
|
+
for (const step of steps) {
|
|
55
|
+
if (step.agent !== undefined && step.agent !== configured.agent)
|
|
56
|
+
continue;
|
|
57
|
+
const source = step.cache?.recordedBy;
|
|
58
|
+
if (source === undefined || step.cache?.replayedActions === 0)
|
|
59
|
+
continue;
|
|
60
|
+
const label = modelLabel({ provider: source.provider, id: source.model });
|
|
61
|
+
if (label === configured.label)
|
|
62
|
+
continue;
|
|
63
|
+
const key = JSON.stringify([step.cache?.entry ?? [step.index, step.api, step.label], source.provider, source.model]);
|
|
64
|
+
notices.set(key, { api: step.api, label: step.label, source });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...notices.values()];
|
|
68
|
+
}
|
|
69
|
+
/** A recording mismatch under its test in the file block. */
|
|
70
|
+
export function replayLine(pc, notice) {
|
|
71
|
+
const label = modelLabel({ provider: notice.source.provider, id: notice.source.model });
|
|
72
|
+
return `${pc.dim(notice.api)} ${stepLabel(notice.label)} ${pc.dim(`replayed · recorded with ${fitColumns(collapseText(label), MAX_RECORDED_MODEL_COLUMNS)}`)}`;
|
|
73
|
+
}
|
|
45
74
|
/**
|
|
46
75
|
* `(↑in ↓out)` for one model call, the total when only that is known, or
|
|
47
76
|
* nothing when the provider reported no usage.
|
package/dist/report/list.d.ts
CHANGED
|
@@ -88,6 +88,7 @@ export declare class ListReporter implements Reporter {
|
|
|
88
88
|
* there only when no step named one.
|
|
89
89
|
*/
|
|
90
90
|
private models;
|
|
91
|
+
private configuredModel;
|
|
91
92
|
/**
|
|
92
93
|
* The exploration an `e2e explore` run reports through `explore` events;
|
|
93
94
|
* while present, its view stands in for the one synthetic test's file
|
package/dist/report/list.js
CHANGED
|
@@ -14,7 +14,7 @@ import { codeFrame, userFrame } from './code-frame.js';
|
|
|
14
14
|
import { addCacheTally, addModelTally, addUsage, aiSegment, bounded, cacheText, durationText, fitColumns, emptyCacheTally, emptyCounters, emptyUsage, F_CHECK, F_CROSS, F_POINTER, fileOutcome, formatClock, formatTime, modelsText, padTitle, rule, stateString, statusBucket, stepsCacheTally, repeatSuffix, stepsModelTally, stepsUsage, sumUsage, tally, terminalColumns, usageText, } from './format.js';
|
|
15
15
|
import { ExploreView } from './list-explore.js';
|
|
16
16
|
import { isShownEvent } from './list-model.js';
|
|
17
|
-
import { stepLine } from './list-steps.js';
|
|
17
|
+
import { replayLine, replayNotices, stepLine } from './list-steps.js';
|
|
18
18
|
import { LiveWindow } from './live-window.js';
|
|
19
19
|
import { repeatGroups, repeatLine, repeatSummary } from './repeats.js';
|
|
20
20
|
import { RunningTree } from './running-tree.js';
|
|
@@ -53,7 +53,7 @@ function appLogTally(appLog) {
|
|
|
53
53
|
return parts.length === 0 ? undefined : parts.join(', ');
|
|
54
54
|
}
|
|
55
55
|
/** Details of an ordinary result: summed over its attempts, the error from the last. */
|
|
56
|
-
function attemptDetails(attempts) {
|
|
56
|
+
function attemptDetails(attempts, configured) {
|
|
57
57
|
const steps = attempts.map((attempt) => attempt.steps);
|
|
58
58
|
// A retry the run interrupted tells nothing; the failure before it is the verdict.
|
|
59
59
|
const told = attempts.findLast((attempt) => attempt.status !== 'interrupted') ?? attempts.at(-1);
|
|
@@ -67,6 +67,7 @@ function attemptDetails(attempts) {
|
|
|
67
67
|
...failureOf(told),
|
|
68
68
|
appLog: appLogTally(told?.appLog ?? []),
|
|
69
69
|
skipReason: told?.status === 'skipped' ? undefined : told?.skip?.reason,
|
|
70
|
+
replays: replayNotices(steps, configured),
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
/** The failure evidence of one attempt, and the path of the screen text it points at. */
|
|
@@ -96,7 +97,7 @@ function videoPaths(attempts) {
|
|
|
96
97
|
* that attempt never ran (skipped, or absent after a launch failure) inherits
|
|
97
98
|
* the attempt's error, since that is what stopped it.
|
|
98
99
|
*/
|
|
99
|
-
function serialMemberDetails(group, testId) {
|
|
100
|
+
function serialMemberDetails(group, testId, configured) {
|
|
100
101
|
const runs = group.attempts.map((attempt) => ({
|
|
101
102
|
attempt,
|
|
102
103
|
member: attempt.members.find((member) => member.testId === testId),
|
|
@@ -116,6 +117,7 @@ function serialMemberDetails(group, testId) {
|
|
|
116
117
|
...failureOf(own === undefined ? undefined : { failure: own.failure, artifacts: last?.attempt.artifacts ?? [] }),
|
|
117
118
|
appLog: appLogTally(own?.appLog ?? []),
|
|
118
119
|
skipReason: neverRan ? undefined : own.skip?.reason,
|
|
120
|
+
replays: replayNotices(steps, configured),
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
const DEFAULT_OUTPUT = {
|
|
@@ -192,6 +194,7 @@ export class ListReporter {
|
|
|
192
194
|
* there only when no step named one.
|
|
193
195
|
*/
|
|
194
196
|
models;
|
|
197
|
+
configuredModel;
|
|
195
198
|
/**
|
|
196
199
|
* The exploration an `e2e explore` run reports through `explore` events;
|
|
197
200
|
* while present, its view stands in for the one synthetic test's file
|
|
@@ -378,6 +381,7 @@ export class ListReporter {
|
|
|
378
381
|
}
|
|
379
382
|
runStarted(event) {
|
|
380
383
|
const { pc } = this;
|
|
384
|
+
this.configuredModel = event.model === undefined ? undefined : { label: event.model, agent: event.agents?.[0] ?? 'default' };
|
|
381
385
|
this.projectRoot = event.projectRoot;
|
|
382
386
|
this.artifactsRoot = event.artifactsRoot;
|
|
383
387
|
this.targets = event.targets;
|
|
@@ -568,15 +572,16 @@ export class ListReporter {
|
|
|
568
572
|
* rather than failing the run.
|
|
569
573
|
*/
|
|
570
574
|
detailsOf(result) {
|
|
575
|
+
const configured = this.configuredModel?.agent === result.agent ? this.configuredModel : undefined;
|
|
571
576
|
if (result.serialGroupId === undefined)
|
|
572
|
-
return attemptDetails(result.attempts);
|
|
577
|
+
return attemptDetails(result.attempts, configured);
|
|
573
578
|
const pending = this.pendingSerial.get(result.serialGroupId);
|
|
574
579
|
if (pending === undefined)
|
|
575
|
-
return attemptDetails([]);
|
|
580
|
+
return attemptDetails([], configured);
|
|
576
581
|
pending.remaining -= 1;
|
|
577
582
|
if (pending.remaining <= 0)
|
|
578
583
|
this.pendingSerial.delete(result.serialGroupId);
|
|
579
|
-
return serialMemberDetails(pending.group, result.test.id);
|
|
584
|
+
return serialMemberDetails(pending.group, result.test.id, configured);
|
|
580
585
|
}
|
|
581
586
|
testFinished(result) {
|
|
582
587
|
// Unselected pairs are report-only: they never print and the plan never
|
|
@@ -591,7 +596,7 @@ export class ListReporter {
|
|
|
591
596
|
// Read before `detailsOf` releases the result's serial group.
|
|
592
597
|
const skippedAfter = failureBeforeSkip(result, (id) => this.pendingSerial.get(id)?.group);
|
|
593
598
|
const id = resultId(result.test.id, result.target.name, result.agent, result.repeat);
|
|
594
|
-
const { durationMs, usage, models, cache, error, videos, failure, screenPath, appLog, skipReason } = this.detailsOf(result);
|
|
599
|
+
const { durationMs, usage, models, cache, error, videos, failure, screenPath, appLog, skipReason, replays } = this.detailsOf(result);
|
|
595
600
|
addUsage(this.runUsage, usage);
|
|
596
601
|
addModelTally(this.runModels, models);
|
|
597
602
|
addCacheTally(this.runCache, cache);
|
|
@@ -614,6 +619,7 @@ export class ListReporter {
|
|
|
614
619
|
firstErrorLine: error === undefined ? undefined : bounded(error.message).split('\n')[0],
|
|
615
620
|
skipReason: result.skip === undefined ? undefined : bounded(result.skip.reason),
|
|
616
621
|
steps,
|
|
622
|
+
replays,
|
|
617
623
|
};
|
|
618
624
|
group.lines.push(line);
|
|
619
625
|
this.runs.push({
|
|
@@ -697,6 +703,7 @@ export class ListReporter {
|
|
|
697
703
|
counts.interrupted > 0 ||
|
|
698
704
|
counts.flaky > 0 ||
|
|
699
705
|
this.groups.size === 1 ||
|
|
706
|
+
group.lines.some((line) => line.replays.length > 0) ||
|
|
700
707
|
(this.live && group.lines.some((line) => line.steps.length > 0));
|
|
701
708
|
if (!verbose)
|
|
702
709
|
return;
|
|
@@ -706,6 +713,8 @@ export class ListReporter {
|
|
|
706
713
|
this.print(rendered);
|
|
707
714
|
for (const step of line.steps)
|
|
708
715
|
this.print(`${STEP_INDENT}${stepLine(pc, step)}`);
|
|
716
|
+
for (const replay of line.replays)
|
|
717
|
+
this.print(`${STEP_INDENT}${replayLine(pc, replay)}`);
|
|
709
718
|
}
|
|
710
719
|
}
|
|
711
720
|
/** One test row under its file line, plus the first error line for a failure. */
|
|
@@ -25,9 +25,10 @@ const SYNC_END = `${ESC}[?2026l`;
|
|
|
25
25
|
* when counting and never cutting inside a grapheme cluster: a glyph or emoji
|
|
26
26
|
* sequence that would cross the limit is dropped whole. The erase sequence
|
|
27
27
|
* counts physical rows, so a line that wraps would leave stale fragments
|
|
28
|
-
* behind on every repaint.
|
|
28
|
+
* behind on every repaint. An entry with a line break still paints as one row.
|
|
29
29
|
*/
|
|
30
30
|
function clampToWidth(text, columns) {
|
|
31
|
+
text = text.replace(/\r\n?|\n/g, ' ');
|
|
31
32
|
const max = Math.max(4, columns - WIDTH_MARGIN);
|
|
32
33
|
let width = 0;
|
|
33
34
|
let out = '';
|
package/dist/report/markdown.js
CHANGED
|
@@ -210,7 +210,7 @@ function runErrorLine(error) {
|
|
|
210
210
|
function failureBlock({ result, final }, manyTargets, options) {
|
|
211
211
|
const kind = statusBucket(result.status);
|
|
212
212
|
const told = toldAttempt(result, final);
|
|
213
|
-
const at = failedStepOf(told
|
|
213
|
+
const at = failedStepOf(told);
|
|
214
214
|
const error = told.error;
|
|
215
215
|
const name = error === undefined ? `**${cell(result.status, MAX_ID_CHARS)}**` : errorName(error);
|
|
216
216
|
const lead = at === undefined ? name : `${name} ${stepClause(told.steps, at)}`;
|
package/dist/run/fixtures.js
CHANGED
|
@@ -102,11 +102,13 @@ export function createFixtures(environment) {
|
|
|
102
102
|
engineCapabilities: environment.target.engine?.capabilities ?? new Set(),
|
|
103
103
|
steps: environment.steps,
|
|
104
104
|
select,
|
|
105
|
+
reportUnservedModel: environment.models.unservedModel,
|
|
105
106
|
config: environment.config,
|
|
106
107
|
target: {
|
|
107
108
|
name: environment.target.name,
|
|
108
109
|
platform: environment.target.platform,
|
|
109
110
|
verbs: environment.session.verbs,
|
|
111
|
+
tapModifiers: environment.session.tapModifiers,
|
|
110
112
|
},
|
|
111
113
|
app: environment.target.app,
|
|
112
114
|
attempt: environment.attempt,
|
package/dist/run/host-trace.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** The per-session `trace.md` a hosted attempt writes at close, through the run report's renderer. */
|
|
2
2
|
import type { ResolvedConfig, ResolvedTarget } from '../config/resolve.ts';
|
|
3
|
+
import type { Report } from '../types.ts';
|
|
3
4
|
import type { AttemptRecord } from './records.ts';
|
|
4
5
|
export interface HostTraceOptions {
|
|
5
6
|
readonly config: ResolvedConfig;
|
|
@@ -17,5 +18,5 @@ export interface HostTraceOptions {
|
|
|
17
18
|
readonly tracePath: string;
|
|
18
19
|
readonly redact: (text: string) => string;
|
|
19
20
|
}
|
|
20
|
-
/** Writes one session's trace page as a single-result run
|
|
21
|
-
export declare function writeHostTrace(options: HostTraceOptions): Promise<
|
|
21
|
+
/** Writes one session's trace page as a single-result run and returns its Report. */
|
|
22
|
+
export declare function writeHostTrace(options: HostTraceOptions): Promise<Report>;
|
package/dist/run/host-trace.js
CHANGED
|
@@ -5,7 +5,7 @@ import { buildReport } from '../report/build.js';
|
|
|
5
5
|
import { renderTracePage } from '../report/failure-text.js';
|
|
6
6
|
import { outcome } from '../report/outcome.js';
|
|
7
7
|
import { toPosixPath, writeTextReport } from '../report/write.js';
|
|
8
|
-
/** Writes one session's trace page as a single-result run
|
|
8
|
+
/** Writes one session's trace page as a single-result run and returns its Report. */
|
|
9
9
|
export async function writeHostTrace(options) {
|
|
10
10
|
const { config, attempt, title } = options;
|
|
11
11
|
const status = attempt.status;
|
|
@@ -34,4 +34,5 @@ export async function writeHostTrace(options) {
|
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
await writeTextReport(options.tracePath, options.redact(page));
|
|
37
|
+
return report;
|
|
37
38
|
}
|
package/dist/run/last-run.d.ts
CHANGED
|
@@ -52,4 +52,13 @@ export declare function carryForward(lastRun: Report1Document, current: Report1D
|
|
|
52
52
|
* `--last-failed` rerun keeps of the tree when it starts.
|
|
53
53
|
*/
|
|
54
54
|
export declare function reportArtifactPaths(document: Report1Document): ReadonlySet<string>;
|
|
55
|
+
/**
|
|
56
|
+
* How long each file took for each target in the previous run, keyed by
|
|
57
|
+
* `target::file`: every attempt of every result and serial group summed. A
|
|
58
|
+
* file with no attempt (skipped, or never reached) has no entry. No readable
|
|
59
|
+
* report is no history, never an error.
|
|
60
|
+
*/
|
|
61
|
+
export declare function previousFileDurations(reportPath: string): Promise<ReadonlyMap<string, number>>;
|
|
62
|
+
/** The key `previousFileDurations` files one target's file under. */
|
|
63
|
+
export declare function fileDurationKey(targetName: string, file: string): string;
|
|
55
64
|
export {};
|
package/dist/run/last-run.js
CHANGED
|
@@ -232,3 +232,31 @@ function isError(value) {
|
|
|
232
232
|
typeof scope.targetId === 'string' &&
|
|
233
233
|
Array.isArray(scope.titlePath))));
|
|
234
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* How long each file took for each target in the previous run, keyed by
|
|
237
|
+
* `target::file`: every attempt of every result and serial group summed. A
|
|
238
|
+
* file with no attempt (skipped, or never reached) has no entry. No readable
|
|
239
|
+
* report is no history, never an error.
|
|
240
|
+
*/
|
|
241
|
+
export async function previousFileDurations(reportPath) {
|
|
242
|
+
const durations = new Map();
|
|
243
|
+
let report;
|
|
244
|
+
try {
|
|
245
|
+
report = await readLastRun(reportPath);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return durations;
|
|
249
|
+
}
|
|
250
|
+
for (const unit of [...report.run.results, ...report.run.serialGroups]) {
|
|
251
|
+
if (unit.attempts.length === 0)
|
|
252
|
+
continue;
|
|
253
|
+
const key = fileDurationKey(unit.targetId, unit.file);
|
|
254
|
+
const ms = unit.attempts.reduce((sum, attempt) => sum + attempt.durationMs, 0);
|
|
255
|
+
durations.set(key, (durations.get(key) ?? 0) + ms);
|
|
256
|
+
}
|
|
257
|
+
return durations;
|
|
258
|
+
}
|
|
259
|
+
/** The key `previousFileDurations` files one target's file under. */
|
|
260
|
+
export function fileDurationKey(targetName, file) {
|
|
261
|
+
return `${targetName}::${file}`;
|
|
262
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Hosted test runners keep a worker alive while opening isolated per-test attempts. */
|
|
2
2
|
import { type SerializedError } from '../internal/errors.ts';
|
|
3
|
-
import type { E2EConfig, TestFixtures } from '../types.ts';
|
|
3
|
+
import type { E2EConfig, Report, TestFixtures } from '../types.ts';
|
|
4
4
|
import type { StepProgress, StepRecord } from './steps.ts';
|
|
5
5
|
export interface OpenHostOptions {
|
|
6
6
|
/** Config search directory, or project root of a config value; default `process.cwd()`. */
|
|
@@ -48,6 +48,8 @@ export interface E2ESession<Fixtures extends object = Record<string, unknown>> e
|
|
|
48
48
|
/** Absolute per-session trace path. Written before close settles successfully. */
|
|
49
49
|
readonly tracePath: string;
|
|
50
50
|
steps(): readonly StepRecord[];
|
|
51
|
+
/** The session Report after close and a successful trace write; otherwise undefined. */
|
|
52
|
+
report(): Report | undefined;
|
|
51
53
|
/** Idempotent. Returns soft assertion, abandoned-operation, and cleanup failures. Disposal throws them as AggregateError. */
|
|
52
54
|
close(): Promise<readonly SerializedError[]>;
|
|
53
55
|
}
|
package/dist/run/open-session.js
CHANGED
|
@@ -50,7 +50,7 @@ async function openHostWorker(options) {
|
|
|
50
50
|
runId: attempt.runId, attemptId: attempt.attemptId, target: target.name,
|
|
51
51
|
fixtures: attempt.fixtures,
|
|
52
52
|
artifactsDir: attempt.artifactsDir, tracePath: attempt.tracePath,
|
|
53
|
-
steps: () => attempt.steps.all(), close: attempt.close,
|
|
53
|
+
steps: () => attempt.steps.all(), report: attempt.report, close: attempt.close,
|
|
54
54
|
[Symbol.asyncDispose]: () => dispose(attempt.close, 'session'),
|
|
55
55
|
};
|
|
56
56
|
}),
|
package/dist/run/runner.js
CHANGED
|
@@ -21,12 +21,12 @@ import { allocateAppPorts } from './app-ports.js';
|
|
|
21
21
|
import { inProcessSpawner } from './in-process.js';
|
|
22
22
|
import { someSkippedAfterFailure, tracedResultIds } from './records.js';
|
|
23
23
|
import { runUnits } from './scheduler.js';
|
|
24
|
-
import { buildWorkPlans, plannedSlots } from './units.js';
|
|
24
|
+
import { buildWorkPlans, longestFirst, plannedSlots } from './units.js';
|
|
25
25
|
import { SessionStore } from './sessions.js';
|
|
26
26
|
import { outputLayout } from './output.js';
|
|
27
27
|
import { registerStaticSecrets } from './secrecy.js';
|
|
28
28
|
import { nextRerunDir, pruneArtifacts } from './artifacts.js';
|
|
29
|
-
import { carryForward, lastFailedIds, readLastRun, reportArtifactPaths } from './last-run.js';
|
|
29
|
+
import { carryForward, fileDurationKey, lastFailedIds, previousFileDurations, readLastRun, reportArtifactPaths } from './last-run.js';
|
|
30
30
|
import { childProcessSpawner } from './worker/handle.js';
|
|
31
31
|
import { setSecretRegistry } from '../secrets.js';
|
|
32
32
|
import { withAbort } from '../internal/time.js';
|
|
@@ -461,6 +461,10 @@ export async function run(options = {}) {
|
|
|
461
461
|
const workerConfigPath = options.tests === undefined ? config.configPath : undefined;
|
|
462
462
|
const inProcess = workerConfigPath === undefined;
|
|
463
463
|
const runWorkers = inProcess ? 1 : config.workers;
|
|
464
|
+
if (runWorkers > 1) {
|
|
465
|
+
const previous = await previousFileDurations(outputLayout(config.output).report);
|
|
466
|
+
plans = longestFirst(plans, (targetName, file) => previous.get(fileDurationKey(targetName, file)));
|
|
467
|
+
}
|
|
464
468
|
const timeLimit = options.maxDuration !== undefined
|
|
465
469
|
? { ms: options.maxDuration, source: '--max-duration' }
|
|
466
470
|
: config.maxDuration === undefined
|
package/dist/run/sessions.js
CHANGED
|
@@ -287,12 +287,12 @@ function parseEnvelope(rawText, name) {
|
|
|
287
287
|
/** Parses an authenticated payload; one this runner did not write is SESSION_INVALID. */
|
|
288
288
|
function parsePayload(text, name) {
|
|
289
289
|
const parsed = JSON.parse(text);
|
|
290
|
-
const secrets = parsed?.secrets;
|
|
291
290
|
if (typeof parsed !== 'object' ||
|
|
292
291
|
parsed === null ||
|
|
293
292
|
!('data' in parsed) ||
|
|
294
|
-
!
|
|
295
|
-
!
|
|
293
|
+
!('secrets' in parsed) ||
|
|
294
|
+
!Array.isArray(parsed.secrets) ||
|
|
295
|
+
!parsed.secrets.every((entry) => Array.isArray(entry) && entry.length === 2 && entry.every((part) => typeof part === 'string'))) {
|
|
296
296
|
throw new ConfigurationError('SESSION_INVALID', `session "${name}" has an unexpected payload`);
|
|
297
297
|
}
|
|
298
298
|
return parsed;
|
|
@@ -6,7 +6,7 @@ import { PollScope } from '../expect/poll-scope.ts';
|
|
|
6
6
|
import { SoftFailures } from '../expect/soft.ts';
|
|
7
7
|
import type { DebugTrace } from '../internal/debug.ts';
|
|
8
8
|
import { type SerializedError } from '../internal/errors.ts';
|
|
9
|
-
import type { TestFixtures } from '../types.ts';
|
|
9
|
+
import type { Report, TestFixtures } from '../types.ts';
|
|
10
10
|
import { AttemptBudget } from './budget.ts';
|
|
11
11
|
import { type TargetExecutor } from './execute.ts';
|
|
12
12
|
import type { AttemptRecord } from './records.ts';
|
|
@@ -37,6 +37,7 @@ export interface StandaloneAttempt {
|
|
|
37
37
|
readonly budget: AttemptBudget;
|
|
38
38
|
readonly artifactsDir: string;
|
|
39
39
|
readonly tracePath: string;
|
|
40
|
+
report(): Report | undefined;
|
|
40
41
|
close(): Promise<readonly SerializedError[]>;
|
|
41
42
|
}
|
|
42
43
|
/** The prepared worker an attempt runs on. */
|
|
@@ -124,6 +124,7 @@ export async function launchAttempt(worker, reservation, onClosed) {
|
|
|
124
124
|
const startedMs = Date.now();
|
|
125
125
|
const tracePath = path.join(artifacts.dir, 'trace.md');
|
|
126
126
|
let ending;
|
|
127
|
+
let report;
|
|
127
128
|
const close = () => {
|
|
128
129
|
ending ??= (async () => {
|
|
129
130
|
clearTimeout(expiry);
|
|
@@ -167,7 +168,7 @@ export async function launchAttempt(worker, reservation, onClosed) {
|
|
|
167
168
|
if (worker.kind === 'hosted') {
|
|
168
169
|
try {
|
|
169
170
|
const { error, status } = verdict();
|
|
170
|
-
await writeHostTrace({
|
|
171
|
+
report = await writeHostTrace({
|
|
171
172
|
config, target, runId, testId: identity.testId, agent, tracePath, redact,
|
|
172
173
|
title: processSecrets.redact(redact(options.name ?? target.name)),
|
|
173
174
|
resultsDir: worker.resultsDir, segments: identity.segments,
|
|
@@ -195,6 +196,7 @@ export async function launchAttempt(worker, reservation, onClosed) {
|
|
|
195
196
|
return {
|
|
196
197
|
runId, attemptId, fixtures: acquired.fixtures, agentRuntime: acquired.agentRuntime, session: opened, steps, budget,
|
|
197
198
|
artifactsDir: artifacts.dir, tracePath, close,
|
|
199
|
+
report: () => report && structuredClone(report),
|
|
198
200
|
};
|
|
199
201
|
}
|
|
200
202
|
/**
|
package/dist/run/steps.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { ReplayHandOffReason } from '../agent/executor.ts';
|
|
3
3
|
import type { CacheWrite } from '../cache/context.ts';
|
|
4
4
|
import type { TraceReplayMissReason } from '../cache/decide.ts';
|
|
5
|
-
import type { DerivedReason } from '../cache/trace.ts';
|
|
5
|
+
import type { RecordedModel, DerivedReason } from '../cache/trace.ts';
|
|
6
6
|
import { TestError, type SerializedError } from '../internal/errors.ts';
|
|
7
7
|
import { type SourceLocation } from '../internal/source.ts';
|
|
8
8
|
import type { AppLogEntry, AppLogSource } from '../engine/index.ts';
|
|
@@ -123,6 +123,8 @@ export interface StepCacheInfo {
|
|
|
123
123
|
* screen the recording started on against the one the step did.
|
|
124
124
|
*/
|
|
125
125
|
detail?: string;
|
|
126
|
+
/** Configured model when the consulted entry was recorded; absent on older entries. */
|
|
127
|
+
recordedBy?: RecordedModel;
|
|
126
128
|
}
|
|
127
129
|
/** The cache detail as the report keeps it: what `agent.act()` returns, and what became of the recording once the attempt settled. */
|
|
128
130
|
export type StepCacheRecord = StepCacheInfo & {
|
package/dist/run/units.d.ts
CHANGED
|
@@ -37,6 +37,14 @@ export interface TargetWorkPlan {
|
|
|
37
37
|
}
|
|
38
38
|
/** Builds per-target work plans from a selection, preserving report order. */
|
|
39
39
|
export declare function buildWorkPlans(selection: Selection, collection: Collection, projectRoot: string): TargetWorkPlan[];
|
|
40
|
+
/**
|
|
41
|
+
* Orders each plan's file units longest first by the time the previous run
|
|
42
|
+
* spent in them, so a long file starts while short ones fill the other
|
|
43
|
+
* workers instead of starting last and running alone. Files without a
|
|
44
|
+
* timing go first, being of unknown length; ties keep collection order.
|
|
45
|
+
* Report order does not depend on dispatch order.
|
|
46
|
+
*/
|
|
47
|
+
export declare function longestFirst(plans: readonly TargetWorkPlan[], durationOf: (targetName: string, file: string) => number | undefined): TargetWorkPlan[];
|
|
40
48
|
/**
|
|
41
49
|
* The worker slots the run will start for a plan, `0` to `slots - 1`: the
|
|
42
50
|
* run's worker cap, the engine's declared `workers`, and the units to
|
package/dist/run/units.js
CHANGED
|
@@ -55,6 +55,20 @@ export function buildWorkPlans(selection, collection, projectRoot) {
|
|
|
55
55
|
return { target, setupUnits, fileUnits, immediate };
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Orders each plan's file units longest first by the time the previous run
|
|
60
|
+
* spent in them, so a long file starts while short ones fill the other
|
|
61
|
+
* workers instead of starting last and running alone. Files without a
|
|
62
|
+
* timing go first, being of unknown length; ties keep collection order.
|
|
63
|
+
* Report order does not depend on dispatch order.
|
|
64
|
+
*/
|
|
65
|
+
export function longestFirst(plans, durationOf) {
|
|
66
|
+
return plans.map((plan) => ({
|
|
67
|
+
...plan,
|
|
68
|
+
fileUnits: plan.fileUnits.toSorted((a, b) => (durationOf(b.targetName, b.file) ?? Number.POSITIVE_INFINITY) -
|
|
69
|
+
(durationOf(a.targetName, a.file) ?? Number.POSITIVE_INFINITY) || 0),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
58
72
|
/**
|
|
59
73
|
* The worker slots the run will start for a plan, `0` to `slots - 1`: the
|
|
60
74
|
* run's worker cap, the engine's declared `workers`, and the units to
|
|
@@ -22,6 +22,8 @@ export declare class WorkerModels {
|
|
|
22
22
|
* need none, and judgment methods still fail on first use.
|
|
23
23
|
*/
|
|
24
24
|
readonly preflight: (agent: ResolvedAgentConfig, name: string) => void;
|
|
25
|
+
/** A model the provider refused, met by a step's call: reported to the run once, and every later preflight rethrows it. */
|
|
26
|
+
readonly unservedModel: (error: E2EError) => void;
|
|
25
27
|
/** Adapter for one resolved model; each distinct model is built once per worker. A missing one names the agent `name`. */
|
|
26
28
|
readonly build: (model: ResolvedModel | undefined, name: string) => ModelAdapter;
|
|
27
29
|
}
|
|
@@ -40,6 +40,13 @@ export class WorkerModels {
|
|
|
40
40
|
if (this.failure !== undefined)
|
|
41
41
|
throw this.failure;
|
|
42
42
|
};
|
|
43
|
+
/** A model the provider refused, met by a step's call: reported to the run once, and every later preflight rethrows it. */
|
|
44
|
+
unservedModel = (error) => {
|
|
45
|
+
if (this.failure !== undefined)
|
|
46
|
+
return;
|
|
47
|
+
this.failure = error;
|
|
48
|
+
this.onFailure(error);
|
|
49
|
+
};
|
|
43
50
|
/** Adapter for one resolved model; each distinct model is built once per worker. A missing one names the agent `name`. */
|
|
44
51
|
build = (model, name) => {
|
|
45
52
|
if (model === undefined)
|
package/dist/types.d.ts
CHANGED
|
@@ -292,7 +292,7 @@ export interface Screen {
|
|
|
292
292
|
getByText(text: TextMatch, options?: TextMatchOptions): Locator;
|
|
293
293
|
/** Creates a lazy displayed-value query. */
|
|
294
294
|
getByDisplayValue(value: TextMatch, options?: TextMatchOptions): Locator;
|
|
295
|
-
/** Creates a lazy test-id query: a string matches the whole id, case-sensitive; a RegExp tests it. */
|
|
295
|
+
/** Creates a lazy test-id query: a string matches the whole id, case-sensitive; a RegExp tests it. An engine may also match an id without its platform's namespacing. */
|
|
296
296
|
getByTestId(id: TextMatch, options?: {
|
|
297
297
|
visible?: boolean;
|
|
298
298
|
}): Locator;
|
|
@@ -1336,7 +1336,7 @@ export interface E2EConfig {
|
|
|
1336
1336
|
/**
|
|
1337
1337
|
* The directory a run writes its results to, relative to the project root;
|
|
1338
1338
|
* default `.e2e`. It holds `report.json`, `junit.xml`, `summary.md`,
|
|
1339
|
-
* `ai-trace.json`, `
|
|
1339
|
+
* `ai-trace.json`, `results/` (cleared once the run's tests start, except on a `--last-failed` rerun),
|
|
1340
1340
|
* `sessions/`, and the `e2e mcp` session videos under `videos/`.
|
|
1341
1341
|
* `--output <dir>` overrides it for one run. It must be inside the project
|
|
1342
1342
|
* root and not the root itself, may not hold a test glob's directory, and
|