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/report/junit.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** JUnit XML reporter: the report-1 document as `.e2e/junit.xml`. */
|
|
2
2
|
import { sanitizeText } from '../internal/errors.js';
|
|
3
3
|
import { repeatSuffix } from './format.js';
|
|
4
|
+
import { toldAttempt } from './failure-text.js';
|
|
4
5
|
import { outcome } from './outcome.js';
|
|
5
6
|
import { fileReporter } from './write.js';
|
|
6
7
|
/**
|
|
@@ -60,11 +61,9 @@ function addCounts(total, part) {
|
|
|
60
61
|
total.skipped += part.skipped;
|
|
61
62
|
total.timeMs += part.timeMs;
|
|
62
63
|
}
|
|
63
|
-
/** `<failure>` for a product verdict, `<error>` for anything that prevented one. */
|
|
64
|
-
function verdictElement(error
|
|
65
|
-
|
|
66
|
-
return error.category === 'test' ? 'failure' : 'error';
|
|
67
|
-
return status === 'interrupted' ? 'error' : 'failure';
|
|
64
|
+
/** `<failure>` for a product verdict, `<error>` for anything else that prevented one. */
|
|
65
|
+
function verdictElement(error) {
|
|
66
|
+
return error === undefined || error.category === 'test' ? 'failure' : 'error';
|
|
68
67
|
}
|
|
69
68
|
function errorBody(error) {
|
|
70
69
|
const phase = error.phase === undefined ? '' : ` (${error.phase})`;
|
|
@@ -93,18 +92,18 @@ function renderResult(result, groups) {
|
|
|
93
92
|
};
|
|
94
93
|
}
|
|
95
94
|
case 'skipped':
|
|
95
|
+
case 'interrupted': {
|
|
96
|
+
// JUnit has no interrupted outcome; a test the run stopped reached no verdict, so it reads as skipped with why.
|
|
97
|
+
const message = result.status === 'skipped' ? (result.skip?.reason ?? 'skipped') : `interrupted: ${final.final.error?.message ?? 'the run was stopped'}`;
|
|
96
98
|
return {
|
|
97
|
-
lines: [
|
|
98
|
-
`${open}>`,
|
|
99
|
-
` <skipped ${attributes({ message: result.skip?.reason ?? 'skipped' })}/>`,
|
|
100
|
-
'</testcase>',
|
|
101
|
-
],
|
|
99
|
+
lines: [`${open}>`, ` <skipped ${attributes({ message })}/>`, '</testcase>'],
|
|
102
100
|
outcome: 'skipped',
|
|
103
101
|
durationMs: final.durationMs,
|
|
104
102
|
};
|
|
103
|
+
}
|
|
105
104
|
default: {
|
|
106
|
-
const
|
|
107
|
-
const
|
|
105
|
+
const error = toldAttempt(result, final).error;
|
|
106
|
+
const element = verdictElement(error);
|
|
108
107
|
const message = error?.message ?? result.status;
|
|
109
108
|
const type = error?.code ?? result.status;
|
|
110
109
|
const body = error === undefined ? result.status : errorBody(error);
|
package/dist/report/list.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export declare class ListReporter implements Reporter {
|
|
|
49
49
|
/** Whether a live window paints; without one, finished steps stream permanently. */
|
|
50
50
|
private readonly live;
|
|
51
51
|
private readonly failures;
|
|
52
|
+
private readonly skippedFailures;
|
|
52
53
|
/** Every finished test's run, for the `Repeats` summary of a `--repeat-each` run. */
|
|
53
54
|
private readonly runs;
|
|
54
55
|
/**
|
|
@@ -119,6 +120,17 @@ export declare class ListReporter implements Reporter {
|
|
|
119
120
|
* the test's result, or the end of the run.
|
|
120
121
|
*/
|
|
121
122
|
private testOutput;
|
|
123
|
+
/**
|
|
124
|
+
* Text user code wrote in the runner's own process (the config's top
|
|
125
|
+
* level, a test file's while it is collected, a reporter), printed like a
|
|
126
|
+
* test's output under a `stdout | runner` heading. The runner hands it
|
|
127
|
+
* here already redacted, only while this reporter shows the run.
|
|
128
|
+
*/
|
|
129
|
+
processOutput(stream: 'stdout' | 'stderr', text: string): void;
|
|
130
|
+
/** Prints what `processOutput` left unfinished: the runner hands nothing more once the run's events are all dispatched. */
|
|
131
|
+
endProcessOutput(): void;
|
|
132
|
+
/** Prints the lines `text` ends under the heading of `stream` and `source`, holding the unfinished rest. */
|
|
133
|
+
private sourcedOutput;
|
|
122
134
|
/** Prints finished output lines under their heading, unless the heading is the one printed last. */
|
|
123
135
|
private printOutput;
|
|
124
136
|
/** Prints the unfinished output of `pair`, or of every source when undefined. */
|
package/dist/report/list.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import picocolors from 'picocolors';
|
|
10
10
|
import { packageVersion } from '../internal/package-version.js';
|
|
11
|
+
import { failureBeforeSkip } from '../run/records.js';
|
|
11
12
|
import { codeFrame, userFrame } from './code-frame.js';
|
|
12
13
|
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';
|
|
13
14
|
import { ExploreView } from './list-explore.js';
|
|
@@ -43,14 +44,16 @@ function pairKey(testId, agent, target, repeat) {
|
|
|
43
44
|
/** Details of an ordinary result: summed over its attempts, the error from the last. */
|
|
44
45
|
function attemptDetails(attempts) {
|
|
45
46
|
const steps = attempts.map((attempt) => attempt.steps);
|
|
47
|
+
// A retry the run interrupted tells nothing; the failure before it is the verdict.
|
|
48
|
+
const told = attempts.findLast((attempt) => attempt.status !== 'interrupted') ?? attempts.at(-1);
|
|
46
49
|
return {
|
|
47
50
|
durationMs: attempts.reduce((total, attempt) => total + attempt.durationMs, 0),
|
|
48
51
|
usage: stepsUsage(steps),
|
|
49
52
|
models: stepsModelTally(steps),
|
|
50
53
|
cache: stepsCacheTally(steps),
|
|
51
|
-
error:
|
|
54
|
+
error: told?.error,
|
|
52
55
|
videos: videoPaths(attempts),
|
|
53
|
-
...failureOf(
|
|
56
|
+
...failureOf(told),
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
/** The failure evidence of one attempt, and the path of the screen text it points at. */
|
|
@@ -85,7 +88,7 @@ function serialMemberDetails(group, testId) {
|
|
|
85
88
|
attempt,
|
|
86
89
|
member: attempt.members.find((member) => member.testId === testId),
|
|
87
90
|
}));
|
|
88
|
-
const last = runs
|
|
91
|
+
const last = runs.findLast((run) => run.attempt.status !== 'interrupted') ?? runs.at(-1);
|
|
89
92
|
const own = last?.member;
|
|
90
93
|
const neverRan = own === undefined || own.status === 'skipped';
|
|
91
94
|
const steps = runs.map((run) => run.member?.steps ?? []);
|
|
@@ -135,6 +138,7 @@ export class ListReporter {
|
|
|
135
138
|
/** Whether a live window paints; without one, finished steps stream permanently. */
|
|
136
139
|
live;
|
|
137
140
|
failures = [];
|
|
141
|
+
skippedFailures = [];
|
|
138
142
|
/** Every finished test's run, for the `Repeats` summary of a `--repeat-each` run. */
|
|
139
143
|
runs = [];
|
|
140
144
|
/**
|
|
@@ -268,20 +272,37 @@ export class ListReporter {
|
|
|
268
272
|
* the test's result, or the end of the run.
|
|
269
273
|
*/
|
|
270
274
|
testOutput(event) {
|
|
271
|
-
const { pc } = this;
|
|
272
|
-
if (event.text === '')
|
|
273
|
-
return;
|
|
274
275
|
const pair = event.pair === undefined ? undefined : pairKey(event.pair.testId, event.pair.agent, event.target, event.pair.repeat);
|
|
275
276
|
const running = pair === undefined ? undefined : this.pairs.get(pair);
|
|
276
277
|
const source = running === undefined ? this.badge(event.target) : `${this.badge(running.group.target)} ${bounded(running.group.file)}${this.separator}${running.title}`;
|
|
277
|
-
|
|
278
|
+
this.sourcedOutput(event.stream, source, pair, event.text);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Text user code wrote in the runner's own process (the config's top
|
|
282
|
+
* level, a test file's while it is collected, a reporter), printed like a
|
|
283
|
+
* test's output under a `stdout | runner` heading. The runner hands it
|
|
284
|
+
* here already redacted, only while this reporter shows the run.
|
|
285
|
+
*/
|
|
286
|
+
processOutput(stream, text) {
|
|
287
|
+
this.sourcedOutput(stream, this.pc.dim('runner'), undefined, text);
|
|
288
|
+
}
|
|
289
|
+
/** Prints what `processOutput` left unfinished: the runner hands nothing more once the run's events are all dispatched. */
|
|
290
|
+
endProcessOutput() {
|
|
291
|
+
this.flushOutput();
|
|
292
|
+
}
|
|
293
|
+
/** Prints the lines `text` ends under the heading of `stream` and `source`, holding the unfinished rest. */
|
|
294
|
+
sourcedOutput(stream, source, pair, text) {
|
|
295
|
+
const { pc } = this;
|
|
296
|
+
if (text === '')
|
|
297
|
+
return;
|
|
298
|
+
const key = `${stream}\u0000${source}`;
|
|
278
299
|
let entry = this.pendingOutput.get(key);
|
|
279
300
|
if (entry === undefined) {
|
|
280
|
-
const label =
|
|
301
|
+
const label = stream === 'stderr' ? pc.yellow(stream) : pc.dim(stream);
|
|
281
302
|
entry = { heading: `${label} ${pc.dim('|')} ${source}`, pair, fragment: '' };
|
|
282
303
|
this.pendingOutput.set(key, entry);
|
|
283
304
|
}
|
|
284
|
-
const lines = (entry.fragment +
|
|
305
|
+
const lines = (entry.fragment + text).split('\n');
|
|
285
306
|
entry.fragment = lines.pop() ?? '';
|
|
286
307
|
if (entry.fragment === '')
|
|
287
308
|
this.pendingOutput.delete(key);
|
|
@@ -550,15 +571,17 @@ export class ListReporter {
|
|
|
550
571
|
const steps = this.pairs.get(key)?.steps ?? [];
|
|
551
572
|
this.pairs.delete(key);
|
|
552
573
|
const group = this.group(result.test.file, result.target.name);
|
|
574
|
+
// Read before `detailsOf` releases the result's serial group.
|
|
575
|
+
const skippedAfter = failureBeforeSkip(result, (id) => this.pendingSerial.get(id)?.group);
|
|
553
576
|
const { durationMs, usage, models, cache, error, videos, failure, screenPath } = this.detailsOf(result);
|
|
554
577
|
addUsage(this.runUsage, usage);
|
|
555
578
|
addModelTally(this.runModels, models);
|
|
556
579
|
addCacheTally(this.runCache, cache);
|
|
557
580
|
const title = this.titledAs(result.test.titlePath.join(' > '), result.agent, result.repeat);
|
|
558
581
|
if (this.explore !== undefined) {
|
|
559
|
-
// The exploration's verdict is its findings; any other error is a failure of its own.
|
|
582
|
+
// The exploration's verdict is its findings; any other error is a failure of its own, unless the run stopped it.
|
|
560
583
|
this.explore.result(result.attempts.flatMap((attempt) => attempt.artifacts));
|
|
561
|
-
if (error !== undefined && !this.explore.isVerdict(error)) {
|
|
584
|
+
if (error !== undefined && result.status !== 'interrupted' && !this.explore.isVerdict(error)) {
|
|
562
585
|
this.failures.push({ group, title, status: result.status, error, videos, failure, screenPath });
|
|
563
586
|
}
|
|
564
587
|
this.window.redraw();
|
|
@@ -584,6 +607,9 @@ export class ListReporter {
|
|
|
584
607
|
if (statusBucket(result.status) === 'failed') {
|
|
585
608
|
this.failures.push({ group, title, status: result.status, error, videos, failure, screenPath });
|
|
586
609
|
}
|
|
610
|
+
if (skippedAfter !== undefined) {
|
|
611
|
+
this.skippedFailures.push({ group, title, status: result.status, error: skippedAfter.error, videos, ...failureOf(skippedAfter) });
|
|
612
|
+
}
|
|
587
613
|
if (group.planned !== undefined && group.lines.length >= group.planned)
|
|
588
614
|
this.printGroup(group);
|
|
589
615
|
this.window.redraw();
|
|
@@ -623,12 +649,15 @@ export class ListReporter {
|
|
|
623
649
|
const outcome = fileOutcome(counts);
|
|
624
650
|
const symbol = outcome === 'failed'
|
|
625
651
|
? pc.red(F_POINTER)
|
|
626
|
-
: outcome === '
|
|
627
|
-
? pc.
|
|
628
|
-
:
|
|
652
|
+
: outcome === 'interrupted'
|
|
653
|
+
? pc.yellow(F_POINTER)
|
|
654
|
+
: outcome === 'skipped'
|
|
655
|
+
? pc.dim(pc.gray(F_DOWN))
|
|
656
|
+
: pc.green(F_CHECK);
|
|
629
657
|
const state = [
|
|
630
658
|
pc.dim(`${counts.total} test${counts.total === 1 ? '' : 's'}`),
|
|
631
659
|
counts.failed > 0 ? pc.red(`${counts.failed} failed`) : undefined,
|
|
660
|
+
counts.interrupted > 0 ? pc.yellow(`${counts.interrupted} interrupted`) : undefined,
|
|
632
661
|
counts.flaky > 0 ? pc.yellow(`${counts.flaky} flaky`) : undefined,
|
|
633
662
|
counts.skipped > 0 ? pc.yellow(`${counts.skipped} skipped`) : undefined,
|
|
634
663
|
]
|
|
@@ -647,6 +676,7 @@ export class ListReporter {
|
|
|
647
676
|
parts.push(pc.dim(ai));
|
|
648
677
|
this.print(parts.join(' '));
|
|
649
678
|
const verbose = counts.failed > 0 ||
|
|
679
|
+
counts.interrupted > 0 ||
|
|
650
680
|
counts.flaky > 0 ||
|
|
651
681
|
this.groups.size === 1 ||
|
|
652
682
|
(this.live && group.lines.some((line) => line.steps.length > 0));
|
|
@@ -678,6 +708,8 @@ export class ListReporter {
|
|
|
678
708
|
: pc.dim(pc.gray(` [${line.skipReason}]`));
|
|
679
709
|
return [` ${pc.dim(pc.gray(F_DOWN))} ${line.title}${reason}`];
|
|
680
710
|
}
|
|
711
|
+
case 'interrupted':
|
|
712
|
+
return [` ${pc.yellow(F_CROSS)} ${line.title} ${pc.yellow('(interrupted)')} ${duration}${ai}`];
|
|
681
713
|
default: {
|
|
682
714
|
const status = line.status === 'failed' ? '' : pc.red(` (${line.status})`);
|
|
683
715
|
const rows = [` ${pc.red(`${F_CROSS} ${line.title}`)}${status} ${duration}${ai}`];
|
|
@@ -716,6 +748,9 @@ export class ListReporter {
|
|
|
716
748
|
rows.push(padTitle(pc, 'Cache') + cache);
|
|
717
749
|
if (final)
|
|
718
750
|
rows.push(...this.repeatRows());
|
|
751
|
+
if (this.skippedFailures.length > 0) {
|
|
752
|
+
rows.push(padTitle(pc, 'Warnings') + pc.yellow(`${this.skippedFailures.length} skipped after failure`));
|
|
753
|
+
}
|
|
719
754
|
if (this.errors.length > 0) {
|
|
720
755
|
const count = this.errors.length;
|
|
721
756
|
rows.push(padTitle(pc, 'Errors') + pc.bold(pc.red(`${count} error${count === 1 ? '' : 's'}`)));
|
|
@@ -736,11 +771,12 @@ export class ListReporter {
|
|
|
736
771
|
const groups = repeatGroups(this.runs, (entry) => entry);
|
|
737
772
|
if (groups.length === 0)
|
|
738
773
|
return [];
|
|
739
|
-
|
|
774
|
+
// A test is listed when it missed a run: a flake first, then one the run cut short.
|
|
775
|
+
const missed = groups.filter((group) => group.passed < group.runs.length).toSorted((a, b) => Number(b.unstable) - Number(a.unstable));
|
|
740
776
|
const summary = repeatSummary(groups);
|
|
741
777
|
return [
|
|
742
|
-
padTitle(pc, 'Repeats') + (
|
|
743
|
-
...
|
|
778
|
+
padTitle(pc, 'Repeats') + (missed.some((group) => group.unstable) ? pc.yellow(summary) : pc.green(summary)),
|
|
779
|
+
...missed.map((group) => `${padTitle(pc, '')}${group.unstable ? pc.yellow(F_CROSS) : pc.dim(pc.gray(F_DOWN))} ${group.label} ${pc.dim(repeatLine(group))}`),
|
|
744
780
|
];
|
|
745
781
|
}
|
|
746
782
|
/** A zero counter's label: the plain text, or the interrupt when that is why nothing is counted. */
|
|
@@ -808,15 +844,15 @@ export class ListReporter {
|
|
|
808
844
|
this.print(line);
|
|
809
845
|
}
|
|
810
846
|
/** vitest's `Failed Tests` section: a banner, then each failure with its code frame. */
|
|
811
|
-
printFailures() {
|
|
847
|
+
printFailures(heading, failures) {
|
|
812
848
|
const { pc } = this;
|
|
813
|
-
if (
|
|
849
|
+
if (failures.length === 0)
|
|
814
850
|
return;
|
|
815
851
|
this.print('');
|
|
816
|
-
this.print(this.errorBanner(
|
|
852
|
+
this.print(this.errorBanner(`${heading} ${failures.length}`));
|
|
817
853
|
this.print('');
|
|
818
|
-
|
|
819
|
-
this.print(`${pc.bold(pc.bgRed(' FAIL '))} ${this.badge(group.target)} ${bounded(group.file)}${this.separator}${title}`);
|
|
854
|
+
failures.forEach(({ group, title, status, error, videos, failure, screenPath }, index) => {
|
|
855
|
+
this.print(`${pc.bold(pc.bgRed(status === 'skipped' ? ' SKIP ' : ' FAIL '))} ${this.badge(group.target)} ${bounded(group.file)}${this.separator}${title}`);
|
|
820
856
|
if (error === undefined) {
|
|
821
857
|
this.print(pc.red(`${pc.bold(status)}: no error was recorded`));
|
|
822
858
|
}
|
|
@@ -829,7 +865,7 @@ export class ListReporter {
|
|
|
829
865
|
}
|
|
830
866
|
this.printEvidence(failure, screenPath);
|
|
831
867
|
this.printVideos(videos);
|
|
832
|
-
const marker = `[${index + 1}/${
|
|
868
|
+
const marker = `[${index + 1}/${failures.length}]`;
|
|
833
869
|
const { before, after } = rule(marker, 'right');
|
|
834
870
|
this.print('');
|
|
835
871
|
this.print(pc.red(pc.dim(`${before}${marker}${after}`)));
|
|
@@ -925,7 +961,8 @@ export class ListReporter {
|
|
|
925
961
|
this.printGroup(group);
|
|
926
962
|
}
|
|
927
963
|
this.printExplore();
|
|
928
|
-
this.printFailures();
|
|
964
|
+
this.printFailures('Failed Tests', this.failures);
|
|
965
|
+
this.printFailures('Skipped After Failure', this.skippedFailures);
|
|
929
966
|
this.printErrors();
|
|
930
967
|
this.print('');
|
|
931
968
|
for (const row of this.summaryRows(true))
|
|
@@ -50,7 +50,7 @@ export declare function renderMarkdownReport(report: Report1Document, options?:
|
|
|
50
50
|
* `summary.md` beside `report.json`, with evidence listed as paths from the
|
|
51
51
|
* project root, for a reader with the checkout in front of it: a pull
|
|
52
52
|
* request description, a coding agent's handoff, a wiki page. Every test
|
|
53
|
-
* that
|
|
53
|
+
* that failed or was flaky gets a page of its own under `failures/`, with the
|
|
54
54
|
* screen at failure inline; the run page links each block to its page. The
|
|
55
55
|
* directory is the reporter's: what an earlier run left there is removed
|
|
56
56
|
* first, so a stale page never describes a failure this run did not have.
|
package/dist/report/markdown.js
CHANGED
|
@@ -45,11 +45,11 @@ const MAX_GOAL_CHARS = 400;
|
|
|
45
45
|
const TRUNCATED_NOTE = '_Truncated to fit a pull request comment; the full report is in `report.json`._';
|
|
46
46
|
/** Screenshots first, then the recording, then the trace; the compiler fails when a kind is missing here. */
|
|
47
47
|
const KIND_RANK = { screenshot: 0, video: 1, trace: 2, download: 3, log: 4 };
|
|
48
|
-
const ICON = { failed: '🔴', flaky: '⚠️', skipped: '⏭️', passed: '🟢' };
|
|
48
|
+
const ICON = { failed: '🔴', interrupted: '⏹️', flaky: '⚠️', skipped: '⏭️', passed: '🟢' };
|
|
49
49
|
/** Worst first: the order failures are listed, files are sorted, and a file's glyph is chosen in. */
|
|
50
|
-
const WORST_FIRST = ['failed', 'flaky', 'skipped', 'passed'];
|
|
51
|
-
/** The order counts read in: `1 failed, 1 flaky, 3 passed, 1 skipped`. */
|
|
52
|
-
const COUNT_ORDER = ['failed', 'flaky', 'passed', 'skipped'];
|
|
50
|
+
const WORST_FIRST = ['failed', 'interrupted', 'flaky', 'skipped', 'passed'];
|
|
51
|
+
/** The order counts read in: `1 failed, 2 interrupted, 1 flaky, 3 passed, 1 skipped`. */
|
|
52
|
+
const COUNT_ORDER = ['failed', 'interrupted', 'flaky', 'passed', 'skipped'];
|
|
53
53
|
function worstBucket(results) {
|
|
54
54
|
const present = new Set(results.map((result) => statusBucket(result.status)));
|
|
55
55
|
return WORST_FIRST.find((key) => present.has(key)) ?? 'passed';
|
|
@@ -65,9 +65,25 @@ function pageName(options) {
|
|
|
65
65
|
const title = options.title?.trim() ?? '';
|
|
66
66
|
return title === '' ? 'e2e' : `e2e ${cell(title, MAX_ID_CHARS)}`;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The headline's glyph: red for a failed run or any failed test, the
|
|
70
|
+
* interrupted glyph for a run or a listed test the run stopped, else green.
|
|
71
|
+
* A `--last-failed` rerun that passed can still list a test the first pass
|
|
72
|
+
* never finished, and the page must not read as all green.
|
|
73
|
+
*/
|
|
74
|
+
function runIcon(run, results) {
|
|
75
|
+
if (run.status !== 'passed' && run.status !== 'interrupted')
|
|
76
|
+
return ICON.failed;
|
|
77
|
+
const worst = worstBucket(results);
|
|
78
|
+
if (worst === 'failed')
|
|
79
|
+
return ICON.failed;
|
|
80
|
+
if (worst === 'interrupted' || run.status === 'interrupted')
|
|
81
|
+
return ICON.interrupted;
|
|
82
|
+
return ICON.passed;
|
|
83
|
+
}
|
|
68
84
|
function headline(run, results, options) {
|
|
69
85
|
const summary = countText(tally(results)) || (run.errors.length > 0 ? 'no tests ran' : 'no tests selected');
|
|
70
|
-
return `### ${run
|
|
86
|
+
return `### ${runIcon(run, results)} ${pageName(options)}: ${summary}`;
|
|
71
87
|
}
|
|
72
88
|
/** The model calls an agent step made; the step's metrics are the source of truth. */
|
|
73
89
|
function modelCalls(step) {
|
|
@@ -236,6 +252,12 @@ function flakyFold(entries, manyTargets, options) {
|
|
|
236
252
|
const summary = `${ICON.flaky} ${plural(entries.length, 'flaky test')} passed on a retry`;
|
|
237
253
|
return [['<details>', `<summary>${summary}</summary>`, '', shown.join('\n\n'), '</details>'].join('\n')];
|
|
238
254
|
}
|
|
255
|
+
/** A test that missed a run: flaky when a run failed, else interrupted when the run stopped one, else skipped. */
|
|
256
|
+
function missIcon(group) {
|
|
257
|
+
if (group.unstable)
|
|
258
|
+
return ICON.flaky;
|
|
259
|
+
return group.runs.some((run) => run.status === 'interrupted') ? ICON.interrupted : ICON.skipped;
|
|
260
|
+
}
|
|
239
261
|
/**
|
|
240
262
|
* A `--repeat-each` run's tally: how many tests passed every run, then each
|
|
241
263
|
* test that did not, with the runs that failed. Empty for a run that
|
|
@@ -254,10 +276,11 @@ function repeatsSection(entries, manyTargets) {
|
|
|
254
276
|
}));
|
|
255
277
|
if (groups.length === 0)
|
|
256
278
|
return [];
|
|
257
|
-
|
|
279
|
+
// A test is listed when it missed a run: a flake first, then one the run cut short.
|
|
280
|
+
const missed = groups.filter((group) => group.passed < group.runs.length).toSorted((a, b) => Number(b.unstable) - Number(a.unstable));
|
|
258
281
|
return [
|
|
259
282
|
`**Repeats:** ${repeatSummary(groups)}`,
|
|
260
|
-
...(
|
|
283
|
+
...(missed.length === 0 ? [] : ['', ...missed.map((group) => `- ${missIcon(group)} ${group.label}: ${cell(repeatLine(group))}`)]),
|
|
261
284
|
];
|
|
262
285
|
}
|
|
263
286
|
/** Results grouped by file and target, the worst file first, then by path. */
|
|
@@ -353,12 +376,12 @@ function footer(run, targets, options) {
|
|
|
353
376
|
return [`<sub>${parts.join(' · ')}</sub>`];
|
|
354
377
|
}
|
|
355
378
|
// --- the exploration record ---
|
|
356
|
-
function exploreHeadline(run, explore, options) {
|
|
379
|
+
function exploreHeadline(run, results, explore, options) {
|
|
357
380
|
const issues = explore.findings.filter((finding) => finding.kind === 'issue').length;
|
|
358
381
|
const warnings = explore.findings.length - issues;
|
|
359
382
|
const counts = [...(issues > 0 ? [plural(issues, 'issue')] : []), ...(warnings > 0 ? [plural(warnings, 'warning')] : [])];
|
|
360
383
|
const summary = counts.length > 0 ? counts.join(', ') : run.status === 'blocked' ? 'explored nothing' : run.status === 'passed' ? 'no findings' : 'did not finish';
|
|
361
|
-
return `### ${run
|
|
384
|
+
return `### ${runIcon(run, results)} ${pageName(options)} explore: ${summary}`;
|
|
362
385
|
}
|
|
363
386
|
/** `3 of 8 steps · 2 passed · 1 failed · the agent covered the goal`. */
|
|
364
387
|
function exploreSteps(explore) {
|
|
@@ -448,7 +471,7 @@ export function renderMarkdownReport(report, options = {}) {
|
|
|
448
471
|
failures.push([`and ${failed.length - failures.length} more failed`]);
|
|
449
472
|
const flaky = flakyFold(notPassed.filter(({ result }) => statusBucket(result.status) === 'flaky'), manyTargets, options);
|
|
450
473
|
const spend = spendLine(run, entries);
|
|
451
|
-
const head = [explore === undefined ? headline(run, selected, options) : exploreHeadline(run, explore, options), ...(spend === undefined ? [] : [spend]), ''];
|
|
474
|
+
const head = [explore === undefined ? headline(run, selected, options) : exploreHeadline(run, selected, explore, options), ...(spend === undefined ? [] : [spend]), ''];
|
|
452
475
|
const groups = fileGroups(entries);
|
|
453
476
|
const sections = explore === undefined
|
|
454
477
|
? [errors, ...failures, flaky, repeatsSection(entries, manyTargets), allTests(groups, selected.length, manyTargets)]
|
|
@@ -468,9 +491,9 @@ function failurePageName(result) {
|
|
|
468
491
|
return `${sanitizePathSegment(`${result.file}-${result.titlePath.join('-')}`)}-${result.id.slice(0, 8)}.md`;
|
|
469
492
|
}
|
|
470
493
|
/**
|
|
471
|
-
* The results that get a page: every one that failed, timed out, was
|
|
472
|
-
*
|
|
473
|
-
*
|
|
494
|
+
* The results that get a page: every one that failed, timed out, or was
|
|
495
|
+
* flaky, except an exploration's own verdict, which its findings already
|
|
496
|
+
* tell. An interrupted test reached no verdict, so it has no failure to tell.
|
|
474
497
|
*/
|
|
475
498
|
function pagedResults(report) {
|
|
476
499
|
const explore = report.run.explore;
|
|
@@ -487,7 +510,7 @@ function pagedResults(report) {
|
|
|
487
510
|
* `summary.md` beside `report.json`, with evidence listed as paths from the
|
|
488
511
|
* project root, for a reader with the checkout in front of it: a pull
|
|
489
512
|
* request description, a coding agent's handoff, a wiki page. Every test
|
|
490
|
-
* that
|
|
513
|
+
* that failed or was flaky gets a page of its own under `failures/`, with the
|
|
491
514
|
* screen at failure inline; the run page links each block to its page. The
|
|
492
515
|
* directory is the reporter's: what an earlier run left there is removed
|
|
493
516
|
* first, so a stale page never describes a failure this run did not have.
|
package/dist/report/repeats.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export interface RepeatGroup {
|
|
|
15
15
|
readonly runs: readonly RepeatRun[];
|
|
16
16
|
/** Runs that passed on their first attempt; a flaky run needed a retry and does not count. */
|
|
17
17
|
readonly passed: number;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a run that reached a verdict did not pass: a flake. A run the
|
|
20
|
+
* run interrupted, or never started, says nothing about the test.
|
|
21
|
+
*/
|
|
22
|
+
readonly unstable: boolean;
|
|
18
23
|
}
|
|
19
24
|
/**
|
|
20
25
|
* Groups runs by `key`, keeping only tests that ran more than once and did
|
|
@@ -27,5 +32,5 @@ export declare function repeatGroups<T>(items: readonly T[], describe: (item: T)
|
|
|
27
32
|
}): RepeatGroup[];
|
|
28
33
|
/** `2 of 3 tests passed all 5 runs`; the run count is the most any test had. */
|
|
29
34
|
export declare function repeatSummary(groups: readonly RepeatGroup[]): string;
|
|
30
|
-
/** `3/5 passed · repeat 0 ASSERTION_FAILED · repeat 2 flaky`, naming each run that did not pass. */
|
|
35
|
+
/** `3/5 passed · repeat 0 ASSERTION_FAILED · repeat 2 flaky · repeat 4 interrupted`, naming each run that did not pass. */
|
|
31
36
|
export declare function repeatLine(group: RepeatGroup): string;
|
package/dist/report/repeats.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* of one test, target, and agent side by side, so a flake reads as `3/5
|
|
4
4
|
* passed` with the runs that did not, instead of five results to count.
|
|
5
5
|
*/
|
|
6
|
+
/** Statuses of a run that reached no verdict: stopped by the run, or never started. */
|
|
7
|
+
const UNDECIDED = new Set(['interrupted', 'skipped']);
|
|
6
8
|
/**
|
|
7
9
|
* Groups runs by `key`, keeping only tests that ran more than once and did
|
|
8
10
|
* not skip every run, in first-seen order.
|
|
@@ -19,7 +21,12 @@ export function repeatGroups(items, describe) {
|
|
|
19
21
|
.filter((group) => group.runs.length > 1 && group.runs.some((run) => run.status !== 'skipped'))
|
|
20
22
|
.map((group) => {
|
|
21
23
|
const runs = group.runs.toSorted((a, b) => a.repeat - b.repeat);
|
|
22
|
-
return {
|
|
24
|
+
return {
|
|
25
|
+
label: group.label,
|
|
26
|
+
runs,
|
|
27
|
+
passed: runs.filter((run) => run.status === 'passed').length,
|
|
28
|
+
unstable: runs.some((run) => run.status !== 'passed' && !UNDECIDED.has(run.status)),
|
|
29
|
+
};
|
|
23
30
|
});
|
|
24
31
|
}
|
|
25
32
|
/** `2 of 3 tests passed all 5 runs`; the run count is the most any test had. */
|
|
@@ -28,13 +35,15 @@ export function repeatSummary(groups) {
|
|
|
28
35
|
const runs = Math.max(...groups.map((group) => group.runs.length));
|
|
29
36
|
return `${stable} of ${groups.length} test${groups.length === 1 ? '' : 's'} passed all ${runs} runs`;
|
|
30
37
|
}
|
|
31
|
-
/** A run that did not pass, in a word: its error code,
|
|
38
|
+
/** A run that did not pass, in a word: its error code, `flaky` with the code its retry recovered from, or why it reached no verdict. */
|
|
32
39
|
function outcomeOf(run) {
|
|
33
40
|
if (run.status === 'flaky')
|
|
34
41
|
return run.code === undefined ? 'flaky' : `flaky (${run.code})`;
|
|
42
|
+
if (UNDECIDED.has(run.status))
|
|
43
|
+
return run.status;
|
|
35
44
|
return run.code ?? run.status;
|
|
36
45
|
}
|
|
37
|
-
/** `3/5 passed · repeat 0 ASSERTION_FAILED · repeat 2 flaky`, naming each run that did not pass. */
|
|
46
|
+
/** `3/5 passed · repeat 0 ASSERTION_FAILED · repeat 2 flaky · repeat 4 interrupted`, naming each run that did not pass. */
|
|
38
47
|
export function repeatLine(group) {
|
|
39
48
|
const misses = group.runs
|
|
40
49
|
.filter((run) => run.status !== 'passed')
|
package/dist/run/artifacts.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { ArtifactStore } from '../types.ts';
|
|
3
3
|
import type { ArtifactSink } from './fixtures.ts';
|
|
4
4
|
import type { ArtifactRecord } from './records.ts';
|
|
5
|
-
import type
|
|
5
|
+
import { type SessionSecrecy } from './secrecy.ts';
|
|
6
6
|
export interface AttemptArtifacts {
|
|
7
7
|
/** Absolute attempt artifact directory, created eagerly. */
|
|
8
8
|
readonly dir: string;
|
|
@@ -38,9 +38,9 @@ export declare function createAttemptArtifacts(options: {
|
|
|
38
38
|
/**
|
|
39
39
|
* The secrecy of the session the attempt runs on, read when a download is
|
|
40
40
|
* registered; undefined (no session open yet) leaves every download as
|
|
41
|
-
* served.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* served. While its ledger holds a value (`redactsRecordings`), a
|
|
42
|
+
* text-like download is rewritten through it before it is hashed or
|
|
43
|
+
* stored.
|
|
44
44
|
*/
|
|
45
45
|
secrecy?: () => SessionSecrecy | undefined;
|
|
46
46
|
/**
|
|
@@ -56,6 +56,24 @@ export declare function createAttemptArtifacts(options: {
|
|
|
56
56
|
readonly attemptId?: string;
|
|
57
57
|
};
|
|
58
58
|
}): AttemptArtifacts;
|
|
59
|
+
/**
|
|
60
|
+
* Empties an artifact tree down to the files `keep` names, by their report
|
|
61
|
+
* paths, and removes the directories left empty. A `--last-failed` rerun
|
|
62
|
+
* starts this way rather than from an empty tree: the evidence the report it
|
|
63
|
+
* reruns names stays, since that report's results fold into the rerun's and
|
|
64
|
+
* its carried tests are not run again.
|
|
65
|
+
*/
|
|
66
|
+
export declare function pruneArtifacts(root: string, keep: ReadonlySet<string>): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Claims the directory a `--last-failed` rerun's attempts write under, beside
|
|
69
|
+
* the evidence it kept, and returns its name: `rerun-<n>`, past every number
|
|
70
|
+
* already there, so the tree reads in run order. The directory is created
|
|
71
|
+
* exclusively, so no attempt of the rerun writes into a directory an earlier
|
|
72
|
+
* run's report still names.
|
|
73
|
+
*/
|
|
74
|
+
export declare function claimRerunDir(root: string): Promise<string>;
|
|
75
|
+
/** An attempt's report segments, under the rerun's directory on a `--last-failed` rerun. */
|
|
76
|
+
export declare function attemptSegments(rerunDir: string | undefined, segments: readonly string[]): readonly string[];
|
|
59
77
|
/**
|
|
60
78
|
* Restricts a report path segment to a safe filename alphabet and length. A
|
|
61
79
|
* value that is already safe and within the cap is unchanged. Any other value
|
package/dist/run/artifacts.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** Attempt-scoped artifact directory and report registration. */
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { createReadStream, mkdirSync } from 'node:fs';
|
|
4
|
-
import { readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { mkdir, readdir, readFile, rm, rmdir, stat } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { writeFileAtomic } from '../internal/atomic-write.js';
|
|
8
|
+
import { redactsRecordings } from './secrecy.js';
|
|
8
9
|
/**
|
|
9
10
|
* How much of each kind the runner masked, unless the registration says. A
|
|
10
11
|
* screenshot masks secure fields at the source; a log passes through the
|
|
@@ -60,7 +61,7 @@ export function createAttemptArtifacts(options) {
|
|
|
60
61
|
const reportPath = path.posix.join(...options.segments, relativePath);
|
|
61
62
|
const secrecy = kind === 'download' && registration?.redaction === undefined ? options.secrecy?.() : undefined;
|
|
62
63
|
pending.push((async () => {
|
|
63
|
-
if (secrecy !== undefined && secrecy
|
|
64
|
+
if (secrecy !== undefined && redactsRecordings(secrecy) && isTextLike(record.mediaType)) {
|
|
64
65
|
record.redaction = await redactDownload(absolute, secrecy);
|
|
65
66
|
}
|
|
66
67
|
// Without a store the file is streamed for its size and digest only;
|
|
@@ -156,6 +157,73 @@ export function createAttemptArtifacts(options) {
|
|
|
156
157
|
};
|
|
157
158
|
return { dir, records, sink, settle };
|
|
158
159
|
}
|
|
160
|
+
/** What a `--last-failed` rerun's attempt directory is called, `rerun-<n>`, numbered from 1. */
|
|
161
|
+
const RERUN_DIR = /^rerun-([1-9][0-9]*)$/;
|
|
162
|
+
/**
|
|
163
|
+
* Empties an artifact tree down to the files `keep` names, by their report
|
|
164
|
+
* paths, and removes the directories left empty. A `--last-failed` rerun
|
|
165
|
+
* starts this way rather than from an empty tree: the evidence the report it
|
|
166
|
+
* reruns names stays, since that report's results fold into the rerun's and
|
|
167
|
+
* its carried tests are not run again.
|
|
168
|
+
*/
|
|
169
|
+
export async function pruneArtifacts(root, keep) {
|
|
170
|
+
const prune = async (dir, relative) => {
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
174
|
+
}
|
|
175
|
+
catch (cause) {
|
|
176
|
+
if (cause.code === 'ENOENT')
|
|
177
|
+
return true;
|
|
178
|
+
throw cause;
|
|
179
|
+
}
|
|
180
|
+
let empty = true;
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
const reportPath = relative === '' ? entry.name : `${relative}/${entry.name}`;
|
|
183
|
+
const absolute = path.join(dir, entry.name);
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
if (await prune(absolute, reportPath))
|
|
186
|
+
await rmdir(absolute);
|
|
187
|
+
else
|
|
188
|
+
empty = false;
|
|
189
|
+
}
|
|
190
|
+
else if (keep.has(reportPath)) {
|
|
191
|
+
empty = false;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
await rm(absolute, { force: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return empty;
|
|
198
|
+
};
|
|
199
|
+
await prune(root, '');
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Claims the directory a `--last-failed` rerun's attempts write under, beside
|
|
203
|
+
* the evidence it kept, and returns its name: `rerun-<n>`, past every number
|
|
204
|
+
* already there, so the tree reads in run order. The directory is created
|
|
205
|
+
* exclusively, so no attempt of the rerun writes into a directory an earlier
|
|
206
|
+
* run's report still names.
|
|
207
|
+
*/
|
|
208
|
+
export async function claimRerunDir(root) {
|
|
209
|
+
await mkdir(root, { recursive: true });
|
|
210
|
+
const taken = (await readdir(root)).map((name) => Number(RERUN_DIR.exec(name)?.[1] ?? 0));
|
|
211
|
+
for (let n = Math.max(0, ...taken) + 1;; n += 1) {
|
|
212
|
+
const name = `rerun-${n}`;
|
|
213
|
+
try {
|
|
214
|
+
await mkdir(path.join(root, name));
|
|
215
|
+
return name;
|
|
216
|
+
}
|
|
217
|
+
catch (cause) {
|
|
218
|
+
if (cause.code !== 'EEXIST')
|
|
219
|
+
throw cause;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** An attempt's report segments, under the rerun's directory on a `--last-failed` rerun. */
|
|
224
|
+
export function attemptSegments(rerunDir, segments) {
|
|
225
|
+
return rerunDir === undefined ? segments : [rerunDir, ...segments];
|
|
226
|
+
}
|
|
159
227
|
/**
|
|
160
228
|
* Rewrites a text-like download through the session's ledger, the way a
|
|
161
229
|
* trace's text entries are, and returns the redaction the record can claim:
|
|
@@ -168,7 +236,7 @@ async function redactDownload(absolute, secrecy) {
|
|
|
168
236
|
try {
|
|
169
237
|
const bytes = await readFile(absolute);
|
|
170
238
|
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
171
|
-
const redacted = secrecy.ledger.
|
|
239
|
+
const redacted = secrecy.ledger.redactFragments(text);
|
|
172
240
|
if (redacted !== text)
|
|
173
241
|
await writeFileAtomic(absolute, redacted);
|
|
174
242
|
return 'complete';
|