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
|
@@ -92,31 +92,46 @@ export async function sendCopilotRequest(request, credentials, upstream) {
|
|
|
92
92
|
url.host = new URL(copilotBaseUrl(credentials.enterpriseUrl)).host;
|
|
93
93
|
const headers = new Headers(request.headers);
|
|
94
94
|
headers.set('openai-intent', 'conversation-edits');
|
|
95
|
-
let initiator = 'user';
|
|
96
|
-
let vision = false;
|
|
97
95
|
const text = request.method === 'POST' ? await request.text() : undefined;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
102
|
-
if (messages.at(-1)?.role !== 'user')
|
|
103
|
-
initiator = 'agent';
|
|
104
|
-
vision = messages.some((message) => Array.isArray(message.content) && message.content.some((part) => part?.type === 'image_url'));
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
// Not JSON; the headers stay at their defaults.
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
headers.set('x-initiator', initiator);
|
|
111
|
-
if (vision)
|
|
96
|
+
const turn = copilotTurn(text);
|
|
97
|
+
headers.set('x-initiator', turn.initiator);
|
|
98
|
+
if (turn.vision)
|
|
112
99
|
headers.set('copilot-vision-request', 'true');
|
|
113
100
|
return upstream(new Request(url, { method: request.method, headers, signal: request.signal, ...(text === undefined ? {} : { body: text }) }));
|
|
114
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Reads who initiated the turn and whether images are present, whichever
|
|
104
|
+
* protocol carried the body. Chat completions put turns in `messages[]` and
|
|
105
|
+
* images in `image_url` parts; the Responses API puts them in `input[]` with
|
|
106
|
+
* `input_image`. A body shaped like neither keeps the defaults.
|
|
107
|
+
*/
|
|
108
|
+
function copilotTurn(text) {
|
|
109
|
+
if (text === undefined)
|
|
110
|
+
return { initiator: 'user', vision: false };
|
|
111
|
+
try {
|
|
112
|
+
const body = JSON.parse(text);
|
|
113
|
+
// A Responses body has `input` and no `messages`; anything else stays on the chat path.
|
|
114
|
+
if (!Array.isArray(body.messages) && Array.isArray(body.input)) {
|
|
115
|
+
const input = body.input;
|
|
116
|
+
return { initiator: input.at(-1)?.role === 'user' ? 'user' : 'agent', vision: input.some((item) => hasPart(item.content, 'input_image')) };
|
|
117
|
+
}
|
|
118
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
119
|
+
return { initiator: messages.at(-1)?.role === 'user' ? 'user' : 'agent', vision: messages.some((message) => hasPart(message.content, 'image_url')) };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Not JSON; the headers stay at their defaults.
|
|
123
|
+
return { initiator: 'user', vision: false };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Whether a turn's content carries a part of the given type (`image_url` for chat, `input_image` for Responses). */
|
|
127
|
+
function hasPart(content, type) {
|
|
128
|
+
return Array.isArray(content) && content.some((part) => part?.type === type);
|
|
129
|
+
}
|
|
115
130
|
/**
|
|
116
131
|
* The chat models the Copilot plan serves; embeddings and other kinds are
|
|
117
|
-
* left out because only chat models fit `copilot()`. A model
|
|
118
|
-
* not enabled, or one Copilot
|
|
119
|
-
*
|
|
132
|
+
* left out because only chat models fit `copilot()`. A model it cannot call
|
|
133
|
+
* at all is listed but marked: one the plan has not enabled, or one Copilot
|
|
134
|
+
* serves only over APIs `copilot()` does not speak (such as Messages). The
|
|
120
135
|
* request goes through `send`, so an enterprise login lists its own host.
|
|
121
136
|
*/
|
|
122
137
|
async function listCopilotModels(fetch) {
|
|
@@ -135,7 +150,7 @@ async function listCopilotModels(fetch) {
|
|
|
135
150
|
supports?.vision === true ? 'vision' : undefined,
|
|
136
151
|
model.preview === true ? 'preview' : undefined,
|
|
137
152
|
typeof model.policy?.state === 'string' && model.policy.state !== 'enabled' ? 'not enabled' : undefined,
|
|
138
|
-
|
|
153
|
+
copilotProtocol(model) === undefined ? 'no chat or responses' : undefined,
|
|
139
154
|
].filter((part) => part !== undefined);
|
|
140
155
|
return {
|
|
141
156
|
id: model.id,
|
|
@@ -144,7 +159,46 @@ async function listCopilotModels(fetch) {
|
|
|
144
159
|
};
|
|
145
160
|
});
|
|
146
161
|
}
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Which endpoint `copilot()` reaches `modelId` on, read from the Copilot
|
|
164
|
+
* API's `GET /models`, or `undefined` when the listing could not be read
|
|
165
|
+
* (an error status, a body that is not JSON, the network, the signal), so
|
|
166
|
+
* the caller can ask again instead of remembering a guess. A model the
|
|
167
|
+
* listing does not name, or names with no endpoint `copilot()` speaks, is
|
|
168
|
+
* chat: the vendor's own error then explains it. A rejected login is not a
|
|
169
|
+
* missing listing and rejects.
|
|
170
|
+
*/
|
|
171
|
+
export async function copilotProtocolFor(modelId, fetch, signal) {
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(new Request(`${COPILOT_API_URL}/models`, { signal }));
|
|
174
|
+
if (!response.ok)
|
|
175
|
+
return undefined;
|
|
176
|
+
const payload = (await response.json());
|
|
177
|
+
if (!Array.isArray(payload.data))
|
|
178
|
+
return undefined;
|
|
179
|
+
return copilotProtocol(payload.data.find((model) => model.id === modelId)) ?? 'chat';
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error instanceof OAuthError)
|
|
183
|
+
throw error;
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The protocol one `/models` entry is served on, or `undefined` when
|
|
189
|
+
* `copilot()` cannot call it. An entry that names no endpoints is chat, as
|
|
190
|
+
* Copilot's older models are; chat completions wins when both are named, so
|
|
191
|
+
* a model that worked over chat keeps doing so. Whether the plan enabled the
|
|
192
|
+
* model is a separate question: Copilot rejects a disabled model the same
|
|
193
|
+
* way on either endpoint.
|
|
194
|
+
*/
|
|
195
|
+
function copilotProtocol(model) {
|
|
196
|
+
const endpoints = model?.supported_endpoints;
|
|
197
|
+
if (model === undefined || !Array.isArray(endpoints))
|
|
198
|
+
return 'chat';
|
|
199
|
+
if (endpoints.includes('/chat/completions'))
|
|
200
|
+
return 'chat';
|
|
201
|
+
if (endpoints.includes('/responses'))
|
|
202
|
+
return 'responses';
|
|
203
|
+
return undefined;
|
|
150
204
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Zen and Go through an OpenCode Console sign-in. Console speaks RFC
|
|
3
|
+
* 8628 device authorization; approving the code binds the login to one
|
|
4
|
+
* workspace, and its token is a bearer for both the Console API and inference.
|
|
5
|
+
* Refresh tokens rotate. The workspace config says which models are served and
|
|
6
|
+
* over which protocol: Zen is its `opencode` provider, Go `opencode-go`, present
|
|
7
|
+
* only for the Go subscriber. Go ids take a `go/` prefix here.
|
|
8
|
+
*/
|
|
9
|
+
import type { FetchFunction, OAuthCredentials, OAuthProvider } from '../types.ts';
|
|
10
|
+
export interface OpencodeConsoleCredentials extends OAuthCredentials {
|
|
11
|
+
/** The workspace the login is bound to; absent for a service API key. */
|
|
12
|
+
readonly orgId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface OpencodeConsoleProviderOptions {
|
|
15
|
+
/** Test seam. */
|
|
16
|
+
readonly consoleUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function createOpencodeConsoleProvider(options?: OpencodeConsoleProviderOptions): OAuthProvider<OpencodeConsoleCredentials, Record<string, never>>;
|
|
19
|
+
export interface OpencodeConsoleRoute {
|
|
20
|
+
readonly go: boolean;
|
|
21
|
+
readonly npm: string;
|
|
22
|
+
readonly baseURL: string;
|
|
23
|
+
readonly modelId: string;
|
|
24
|
+
}
|
|
25
|
+
/** The plan's chat completions route, used until the config is read. */
|
|
26
|
+
export declare function opencodeConsoleChatRoute(modelId: string): OpencodeConsoleRoute;
|
|
27
|
+
/**
|
|
28
|
+
* The model's route from the workspace config, or `undefined` when the config
|
|
29
|
+
* could not be read, so the caller asks again rather than remembering a guess.
|
|
30
|
+
*/
|
|
31
|
+
export declare function opencodeConsoleRouteFor(modelId: string, fetch: FetchFunction, signal: AbortSignal): Promise<OpencodeConsoleRoute | undefined>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Zen and Go through an OpenCode Console sign-in. Console speaks RFC
|
|
3
|
+
* 8628 device authorization; approving the code binds the login to one
|
|
4
|
+
* workspace, and its token is a bearer for both the Console API and inference.
|
|
5
|
+
* Refresh tokens rotate. The workspace config says which models are served and
|
|
6
|
+
* over which protocol: Zen is its `opencode` provider, Go `opencode-go`, present
|
|
7
|
+
* only for the Go subscriber. Go ids take a `go/` prefix here.
|
|
8
|
+
*/
|
|
9
|
+
import { rfc8628Flow } from '../device-code.js';
|
|
10
|
+
import { OAuthError, describeResponse } from '../errors.js';
|
|
11
|
+
import { expiryFrom, requestTokens } from '../token-endpoint.js';
|
|
12
|
+
/** Console accepts any client id. */
|
|
13
|
+
const CLIENT_ID = 'e2e';
|
|
14
|
+
const CONSOLE_URL = 'https://opencode.ai/console';
|
|
15
|
+
const INFERENCE_URL = 'https://opencode.ai/inference';
|
|
16
|
+
const GO_PREFIX = 'go/';
|
|
17
|
+
const ZEN_PROVIDER = 'opencode';
|
|
18
|
+
const GO_PROVIDER = 'opencode-go';
|
|
19
|
+
export function createOpencodeConsoleProvider(options = {}) {
|
|
20
|
+
const consoleUrl = options.consoleUrl ?? CONSOLE_URL;
|
|
21
|
+
const tokenUrl = `${consoleUrl}/auth/device/token`;
|
|
22
|
+
return {
|
|
23
|
+
id: 'opencode-console',
|
|
24
|
+
name: 'OpenCode Console',
|
|
25
|
+
async login(callbacks) {
|
|
26
|
+
const deviceCodeUrl = `${consoleUrl}/auth/device/code`;
|
|
27
|
+
const tokens = await rfc8628Flow({
|
|
28
|
+
vendor: 'OpenCode Console',
|
|
29
|
+
deviceCodeUrl,
|
|
30
|
+
tokenUrl,
|
|
31
|
+
clientId: CLIENT_ID,
|
|
32
|
+
request: { supports_org_scope: 'true' },
|
|
33
|
+
// Console answers with a relative verification URL.
|
|
34
|
+
callbacks: {
|
|
35
|
+
...callbacks,
|
|
36
|
+
onAuth: (info) => {
|
|
37
|
+
const url = new URL(info.url, deviceCodeUrl).href;
|
|
38
|
+
callbacks.onAuth({
|
|
39
|
+
...info,
|
|
40
|
+
url,
|
|
41
|
+
instructions: `Open ${url} on any device, pick the workspace to sign in to${info.userCode === undefined ? '' : `, and confirm the code ${info.userCode}`}.`,
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return toCredentials(tokens);
|
|
47
|
+
},
|
|
48
|
+
async refresh(credentials) {
|
|
49
|
+
if (credentials.refresh === '')
|
|
50
|
+
throw new OAuthError('LOGIN_REQUIRED', 'OpenCode Console has no refresh token for this login');
|
|
51
|
+
const tokens = await requestTokens('OpenCode Console', tokenUrl, { grant_type: 'refresh_token', refresh_token: credentials.refresh, client_id: CLIENT_ID }, 'LOGIN_REQUIRED');
|
|
52
|
+
return toCredentials(tokens, credentials);
|
|
53
|
+
},
|
|
54
|
+
send(request, credentials, upstream) {
|
|
55
|
+
return sendOpencodeConsoleRequest(request, credentials, upstream, consoleUrl);
|
|
56
|
+
},
|
|
57
|
+
models(fetch) {
|
|
58
|
+
return listOpencodeConsoleModels(fetch, consoleUrl);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Names the workspace as each API expects, and drops the Google SDK's placeholder key header. */
|
|
63
|
+
async function sendOpencodeConsoleRequest(request, credentials, upstream, consoleUrl) {
|
|
64
|
+
const headers = new Headers(request.headers);
|
|
65
|
+
headers.delete('x-goog-api-key');
|
|
66
|
+
if (credentials.orgId !== undefined)
|
|
67
|
+
headers.set(request.url.startsWith(`${consoleUrl}/`) ? 'x-org-id' : 'x-opencode-org-id', credentials.orgId);
|
|
68
|
+
const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
|
|
69
|
+
return upstream(new Request(request.url, { method: request.method, headers, signal: request.signal, ...(body === undefined ? {} : { body }) }));
|
|
70
|
+
}
|
|
71
|
+
async function readConfig(fetch, consoleUrl, signal) {
|
|
72
|
+
const response = await fetch(new Request(`${consoleUrl}/api/config`, signal === undefined ? {} : { signal }));
|
|
73
|
+
if (!response.ok)
|
|
74
|
+
throw new OAuthError('FLOW_FAILED', `OpenCode Console did not return the workspace config: ${await describeResponse(response)}`);
|
|
75
|
+
const payload = (await response.json());
|
|
76
|
+
const providers = payload.config?.provider;
|
|
77
|
+
return typeof providers === 'object' && providers !== null ? providers : {};
|
|
78
|
+
}
|
|
79
|
+
/** Leaves out disabled models and Zen's free ones, which only OpenCode's own clients may use. */
|
|
80
|
+
async function listOpencodeConsoleModels(fetch, consoleUrl) {
|
|
81
|
+
const providers = await readConfig(fetch, consoleUrl);
|
|
82
|
+
const listed = (key, prefix, plan) => Object.entries(providers[key]?.models ?? {})
|
|
83
|
+
.filter(([, model]) => model.disabled !== true && (key === GO_PROVIDER || !isFree(model)))
|
|
84
|
+
.map(([id, model]) => {
|
|
85
|
+
const detail = [
|
|
86
|
+
plan,
|
|
87
|
+
typeof model.cost?.input === 'number' && typeof model.cost.output === 'number' && key === ZEN_PROVIDER ? `$${model.cost.input} in, $${model.cost.output} out per 1M` : undefined,
|
|
88
|
+
Array.isArray(model.modalities?.input) && model.modalities.input.includes('image') ? 'vision' : undefined,
|
|
89
|
+
].filter((part) => part !== undefined);
|
|
90
|
+
return { id: `${prefix}${id}`, ...(typeof model.name === 'string' && model.name !== '' ? { name: model.name } : {}), detail: detail.join(', ') };
|
|
91
|
+
});
|
|
92
|
+
return [...listed(ZEN_PROVIDER, '', 'Zen'), ...listed(GO_PROVIDER, GO_PREFIX, 'Go')];
|
|
93
|
+
}
|
|
94
|
+
function isFree(model) {
|
|
95
|
+
return model.cost?.input === 0 && model.cost.output === 0;
|
|
96
|
+
}
|
|
97
|
+
const CHAT_NPM = '@ai-sdk/openai-compatible';
|
|
98
|
+
function splitModelId(modelId) {
|
|
99
|
+
const go = modelId.startsWith(GO_PREFIX);
|
|
100
|
+
return { go, id: go ? modelId.slice(GO_PREFIX.length) : modelId };
|
|
101
|
+
}
|
|
102
|
+
/** The plan's chat completions route, used until the config is read. */
|
|
103
|
+
export function opencodeConsoleChatRoute(modelId) {
|
|
104
|
+
const { go, id } = splitModelId(modelId);
|
|
105
|
+
return { go, npm: CHAT_NPM, baseURL: `${INFERENCE_URL}${go ? '/go' : ''}/openai/v1`, modelId: id };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The model's route from the workspace config, or `undefined` when the config
|
|
109
|
+
* could not be read, so the caller asks again rather than remembering a guess.
|
|
110
|
+
*/
|
|
111
|
+
export async function opencodeConsoleRouteFor(modelId, fetch, signal) {
|
|
112
|
+
let providers;
|
|
113
|
+
try {
|
|
114
|
+
providers = await readConfig(fetch, CONSOLE_URL, signal);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error instanceof OAuthError && error.code !== 'FLOW_FAILED')
|
|
118
|
+
throw error;
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const { go, id } = splitModelId(modelId);
|
|
122
|
+
const provider = providers[go ? GO_PROVIDER : ZEN_PROVIDER];
|
|
123
|
+
if (provider === undefined) {
|
|
124
|
+
throw new OAuthError('MISCONFIGURED', go
|
|
125
|
+
? 'OpenCode Go is not available to this login: the workspace has no Go subscription, or the signed-in user is not its subscriber; sign in to the Go workspace with `npx e2e login opencode-console`'
|
|
126
|
+
: 'OpenCode Zen is turned off for this OpenCode Console workspace');
|
|
127
|
+
}
|
|
128
|
+
const model = provider.models?.[id];
|
|
129
|
+
if (model === undefined || model.disabled === true) {
|
|
130
|
+
throw new OAuthError('MISCONFIGURED', `the OpenCode Console workspace does not serve ${modelId}; \`npx e2e models opencode-console\` lists the ids`);
|
|
131
|
+
}
|
|
132
|
+
const npm = typeof model.provider?.npm === 'string' ? model.provider.npm : provider.npm;
|
|
133
|
+
const baseURL = typeof model.provider?.api === 'string' ? model.provider.api : provider.api;
|
|
134
|
+
if (typeof npm !== 'string' || typeof baseURL !== 'string')
|
|
135
|
+
return undefined;
|
|
136
|
+
return { go, npm, baseURL, modelId: typeof model.id === 'string' && model.id !== '' ? model.id : id };
|
|
137
|
+
}
|
|
138
|
+
/** A refresh keeps the workspace and the refresh token when the grant omits them. */
|
|
139
|
+
function toCredentials(tokens, previous) {
|
|
140
|
+
const orgId = tokens.org_id;
|
|
141
|
+
const workspace = typeof orgId === 'string' && orgId !== '' ? orgId : previous?.orgId;
|
|
142
|
+
return {
|
|
143
|
+
access: tokens.access_token,
|
|
144
|
+
refresh: tokens.refresh_token ?? previous?.refresh ?? '',
|
|
145
|
+
expires: expiryFrom(tokens.expires_in),
|
|
146
|
+
...(workspace === undefined ? {} : { orgId: workspace }),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -5,16 +5,12 @@ import type { OAuthProvider } from './types.ts';
|
|
|
5
5
|
export interface LoginOptionsById {
|
|
6
6
|
readonly openai: CodexLoginOptions;
|
|
7
7
|
readonly 'github-copilot': CopilotLoginOptions;
|
|
8
|
+
readonly 'opencode-console': Record<string, never>;
|
|
8
9
|
readonly spacexai: Record<string, never>;
|
|
9
10
|
}
|
|
10
11
|
export type ProviderId = keyof LoginOptionsById;
|
|
11
|
-
export declare const PROVIDER_IDS: readonly ["openai", "github-copilot", "spacexai"];
|
|
12
|
+
export declare const PROVIDER_IDS: readonly ["openai", "github-copilot", "opencode-console", "spacexai"];
|
|
12
13
|
export declare function isProviderId(id: string): id is ProviderId;
|
|
13
14
|
export declare function getProvider(id: ProviderId): OAuthProvider;
|
|
14
|
-
/**
|
|
15
|
-
* How requests identify this harness to a vendor: `e2e/<version> (<platform>; <arch>)`,
|
|
16
|
-
* alongside the `originator` (ChatGPT) and `referrer` (SpaceXAI) the flows send. Never another client's name.
|
|
17
|
-
*/
|
|
18
|
-
export declare const USER_AGENT: string;
|
|
19
15
|
/** How a missing login is described to the user: the CLI command that fixes it. */
|
|
20
16
|
export declare function loginHint(id: ProviderId): string;
|
package/dist/oauth/providers.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/** The built-in providers by id, and the login options each one takes. */
|
|
2
|
-
import { packageVersion } from '../internal/package-version.js';
|
|
3
2
|
import { createCodexProvider } from './providers/openai.js';
|
|
4
3
|
import { createCopilotProvider } from './providers/github-copilot.js';
|
|
4
|
+
import { createOpencodeConsoleProvider } from './providers/opencode-console.js';
|
|
5
5
|
import { createXaiProvider } from './providers/xai.js';
|
|
6
|
-
export const PROVIDER_IDS = ['openai', 'github-copilot', 'spacexai'];
|
|
6
|
+
export const PROVIDER_IDS = ['openai', 'github-copilot', 'opencode-console', 'spacexai'];
|
|
7
7
|
// Method shorthand is bivariant, so each specialised provider fits the base interface without a cast.
|
|
8
8
|
const PROVIDERS = {
|
|
9
9
|
openai: createCodexProvider(),
|
|
10
10
|
'github-copilot': createCopilotProvider(),
|
|
11
|
+
'opencode-console': createOpencodeConsoleProvider(),
|
|
11
12
|
spacexai: createXaiProvider(),
|
|
12
13
|
};
|
|
13
14
|
export function isProviderId(id) {
|
|
@@ -16,11 +17,6 @@ export function isProviderId(id) {
|
|
|
16
17
|
export function getProvider(id) {
|
|
17
18
|
return PROVIDERS[id];
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
* How requests identify this harness to a vendor: `e2e/<version> (<platform>; <arch>)`,
|
|
21
|
-
* alongside the `originator` (ChatGPT) and `referrer` (SpaceXAI) the flows send. Never another client's name.
|
|
22
|
-
*/
|
|
23
|
-
export const USER_AGENT = `e2e/${packageVersion(import.meta.url, '../../package.json', '0.0.0')} (${process.platform}; ${process.arch})`;
|
|
24
20
|
/** How a missing login is described to the user: the CLI command that fixes it. */
|
|
25
21
|
export function loginHint(id) {
|
|
26
22
|
return `run \`npx e2e login ${id}\``;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Helpers shared by the subscription models that ride the Responses API. */
|
|
2
|
+
import type { LanguageModelV4 } from '@ai-sdk/provider';
|
|
3
|
+
/**
|
|
4
|
+
* Tells the SDK what a subscription backend enforces: nothing is stored
|
|
5
|
+
* server side. Believing storage is on, the SDK refers back to an earlier
|
|
6
|
+
* turn's reasoning by id (`item_reference`), which the backend then cannot
|
|
7
|
+
* find; told it is off, the SDK carries the encrypted reasoning itself.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withoutServerStorage(model: LanguageModelV4): LanguageModelV4;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Helpers shared by the subscription models that ride the Responses API. */
|
|
2
|
+
/**
|
|
3
|
+
* Tells the SDK what a subscription backend enforces: nothing is stored
|
|
4
|
+
* server side. Believing storage is on, the SDK refers back to an earlier
|
|
5
|
+
* turn's reasoning by id (`item_reference`), which the backend then cannot
|
|
6
|
+
* find; told it is off, the SDK carries the encrypted reasoning itself.
|
|
7
|
+
*/
|
|
8
|
+
export function withoutServerStorage(model) {
|
|
9
|
+
const storeOff = (options) => ({
|
|
10
|
+
...options,
|
|
11
|
+
providerOptions: { ...options.providerOptions, openai: { ...options.providerOptions?.['openai'], store: false } },
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
specificationVersion: model.specificationVersion,
|
|
15
|
+
provider: model.provider,
|
|
16
|
+
modelId: model.modelId,
|
|
17
|
+
get supportedUrls() {
|
|
18
|
+
return model.supportedUrls;
|
|
19
|
+
},
|
|
20
|
+
doGenerate: (options) => model.doGenerate(storeOff(options)),
|
|
21
|
+
doStream: (options) => model.doStream(storeOff(options)),
|
|
22
|
+
};
|
|
23
|
+
}
|
package/dist/report/build.d.ts
CHANGED
|
@@ -264,15 +264,36 @@ export interface ReportUsage {
|
|
|
264
264
|
maxActionStepsInStep: number;
|
|
265
265
|
estimatedCostUsd?: number;
|
|
266
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* The run's counts by result. `passed`, `failed` (timed out included),
|
|
269
|
+
* `interrupted`, `flaky`, and `skipped` count selected results only and add
|
|
270
|
+
* up to `selected`; `discovered` minus `selected` is what the selection left
|
|
271
|
+
* out.
|
|
272
|
+
*/
|
|
267
273
|
export interface ReportSummary {
|
|
268
274
|
discovered: number;
|
|
269
275
|
selected: number;
|
|
270
276
|
executed: number;
|
|
271
277
|
passed: number;
|
|
272
278
|
failed: number;
|
|
279
|
+
/** Stopped by the run (a signal, `--max-failures`, a run-level error) before reaching a verdict; never counted as failed. */
|
|
280
|
+
interrupted: number;
|
|
273
281
|
flaky: number;
|
|
274
282
|
skipped: number;
|
|
275
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* What earlier runs left unresolved that a `--last-failed` rerun did not run
|
|
286
|
+
* again, because another filter left it out: the results as those runs
|
|
287
|
+
* reported them, the serial groups their members' attempts live on, and the
|
|
288
|
+
* suite hook failures whose scope still holds a carried test. The next
|
|
289
|
+
* `--last-failed` selects these tests too, so a test that never ran again
|
|
290
|
+
* stays owed until it does.
|
|
291
|
+
*/
|
|
292
|
+
export interface ReportCarried {
|
|
293
|
+
results: readonly ReportResult[];
|
|
294
|
+
serialGroups: readonly ReportSerialGroup[];
|
|
295
|
+
errors: readonly ReportError[];
|
|
296
|
+
}
|
|
276
297
|
export interface Report1Document {
|
|
277
298
|
schemaVersion: 'report-1';
|
|
278
299
|
run: {
|
|
@@ -309,6 +330,11 @@ export interface Report1Document {
|
|
|
309
330
|
summary: ReportSummary;
|
|
310
331
|
limits: ReportLimits;
|
|
311
332
|
usage: ReportUsage;
|
|
333
|
+
/**
|
|
334
|
+
* Present on a `--last-failed` rerun that left out a test an earlier run
|
|
335
|
+
* did not pass. Not part of this run's status, exit code, or summary.
|
|
336
|
+
*/
|
|
337
|
+
carried?: ReportCarried | undefined;
|
|
312
338
|
/** Present on an `e2e explore` run only. */
|
|
313
339
|
explore?: ReportExplore | undefined;
|
|
314
340
|
};
|
package/dist/report/build.js
CHANGED
|
@@ -8,6 +8,7 @@ import { resultId, timestamp } from '../internal/ids.js';
|
|
|
8
8
|
import { obj } from '../internal/objects.js';
|
|
9
9
|
import { packageVersion } from '../internal/package-version.js';
|
|
10
10
|
import { projectRelativePath } from '../internal/source.js';
|
|
11
|
+
import { someSkippedAfterFailure } from '../run/records.js';
|
|
11
12
|
/**
|
|
12
13
|
* What a target's engine declaration says about it: the one description the
|
|
13
14
|
* runner grades against and the report records. A target without an engine is
|
|
@@ -155,20 +156,22 @@ function serializeTarget(config, target, provenance) {
|
|
|
155
156
|
...(provenance ?? describeTarget(target)),
|
|
156
157
|
};
|
|
157
158
|
}
|
|
158
|
-
/** Computes report-1 summary counts from results. */
|
|
159
|
+
/** Computes report-1 summary counts from results; the status counts are over the selected ones. */
|
|
159
160
|
function computeSummary(results) {
|
|
160
161
|
let selected = 0;
|
|
161
162
|
let executed = 0;
|
|
162
163
|
let passed = 0;
|
|
163
164
|
let failed = 0;
|
|
165
|
+
let interrupted = 0;
|
|
164
166
|
let flaky = 0;
|
|
165
167
|
let skipped = 0;
|
|
166
168
|
for (const result of results) {
|
|
167
|
-
if (result.selected)
|
|
168
|
-
selected += 1;
|
|
169
169
|
if (result.attempts.length > 0 || (result.serialGroupId !== undefined && result.status !== 'skipped')) {
|
|
170
170
|
executed += 1;
|
|
171
171
|
}
|
|
172
|
+
if (!result.selected)
|
|
173
|
+
continue;
|
|
174
|
+
selected += 1;
|
|
172
175
|
switch (result.status) {
|
|
173
176
|
case 'passed':
|
|
174
177
|
passed += 1;
|
|
@@ -178,15 +181,17 @@ function computeSummary(results) {
|
|
|
178
181
|
break;
|
|
179
182
|
case 'failed':
|
|
180
183
|
case 'timed-out':
|
|
181
|
-
case 'interrupted':
|
|
182
184
|
failed += 1;
|
|
183
185
|
break;
|
|
186
|
+
case 'interrupted':
|
|
187
|
+
interrupted += 1;
|
|
188
|
+
break;
|
|
184
189
|
case 'skipped':
|
|
185
190
|
skipped += 1;
|
|
186
191
|
break;
|
|
187
192
|
}
|
|
188
193
|
}
|
|
189
|
-
return { discovered: results.length, selected, executed, passed, failed, flaky, skipped };
|
|
194
|
+
return { discovered: results.length, selected, executed, passed, failed, interrupted, flaky, skipped };
|
|
190
195
|
}
|
|
191
196
|
/**
|
|
192
197
|
* A run is `blocked` — not failed — when it did not pass and *every*
|
|
@@ -195,14 +200,17 @@ function computeSummary(results) {
|
|
|
195
200
|
* nothing contradicts that. One genuine failure keeps the run failed, and so
|
|
196
201
|
* does any run-level error (a launch that never came up, a cleanup or report
|
|
197
202
|
* write that failed): it is its own fact about the run, not a blocked step,
|
|
198
|
-
* and it must stay visible to a host reading the status.
|
|
203
|
+
* and it must stay visible to a host reading the status. So does a skip after
|
|
204
|
+
* a failure under `failOnSkippedFailure`. Derivation requires
|
|
199
205
|
* positive evidence, never absence of it.
|
|
200
206
|
*/
|
|
201
|
-
function deriveRunStatus(status, results, serialGroups, runErrors) {
|
|
207
|
+
function deriveRunStatus(status, results, serialGroups, runErrors, failOnSkippedFailure) {
|
|
202
208
|
if (status !== 'failed' && status !== 'error')
|
|
203
209
|
return status;
|
|
204
210
|
if (runErrors.length > 0)
|
|
205
211
|
return status;
|
|
212
|
+
if (failOnSkippedFailure && someSkippedAfterFailure(results, serialGroups))
|
|
213
|
+
return status;
|
|
206
214
|
// Serial members carry no attempts of their own; their failing error lives
|
|
207
215
|
// on the group's last attempt (or its failing member).
|
|
208
216
|
const groupCode = new Map();
|
|
@@ -320,7 +328,7 @@ export function buildReport(options) {
|
|
|
320
328
|
name: 'e2e',
|
|
321
329
|
version: packageVersion(import.meta.url, '../../package.json', '0.0.0'),
|
|
322
330
|
},
|
|
323
|
-
status: deriveRunStatus(options.status, options.results, options.serialGroups, options.runErrors),
|
|
331
|
+
status: deriveRunStatus(options.status, options.results, options.serialGroups, options.runErrors, config?.failOnSkippedFailure === true),
|
|
324
332
|
exitCode: options.exitCode,
|
|
325
333
|
startedAt: options.startedAt,
|
|
326
334
|
finishedAt: timestamp(),
|
|
@@ -34,7 +34,12 @@ export declare function detailLines(error: ReportError | undefined): string[];
|
|
|
34
34
|
* called with, which reads as code.
|
|
35
35
|
*/
|
|
36
36
|
export declare function stepLabel(step: ReportStep, max?: number): string;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The attempt whose story a block tells: a flaky test's last failure; for a
|
|
39
|
+
* failed or timed-out test, the last attempt that reached that verdict, so a
|
|
40
|
+
* retry the run interrupted (or a serial member that retry never reached)
|
|
41
|
+
* tells nothing; otherwise the final one.
|
|
42
|
+
*/
|
|
38
43
|
export declare function toldAttempt(result: ReportResult, final: Outcome): AttemptView;
|
|
39
44
|
/** A step that did not pass. */
|
|
40
45
|
export type FailedStep = ReportStep & {
|
|
@@ -69,9 +69,19 @@ function screenUrl(url) {
|
|
|
69
69
|
return url;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* The attempt whose story a block tells: a flaky test's last failure; for a
|
|
74
|
+
* failed or timed-out test, the last attempt that reached that verdict, so a
|
|
75
|
+
* retry the run interrupted (or a serial member that retry never reached)
|
|
76
|
+
* tells nothing; otherwise the final one.
|
|
77
|
+
*/
|
|
73
78
|
export function toldAttempt(result, final) {
|
|
74
|
-
|
|
79
|
+
if (result.status === 'flaky')
|
|
80
|
+
return final.lastFailed ?? final.final;
|
|
81
|
+
if (result.status === 'failed' || result.status === 'timed-out') {
|
|
82
|
+
return final.attempts.findLast((attempt) => attempt.status === 'failed' || attempt.status === 'timed-out') ?? final.final;
|
|
83
|
+
}
|
|
84
|
+
return final.final;
|
|
75
85
|
}
|
|
76
86
|
/** The step a failure happened at: the first that did not pass, with its position. */
|
|
77
87
|
export function failedStepOf(steps) {
|
|
@@ -87,9 +97,9 @@ export function failedStepOf(steps) {
|
|
|
87
97
|
*/
|
|
88
98
|
export function attemptsLine(result, final) {
|
|
89
99
|
// An attempt that recorded nothing for this test (a serial group that never
|
|
90
|
-
// reached the member) says nothing about how the test fails.
|
|
100
|
+
// reached the member), or one the run interrupted, says nothing about how the test fails.
|
|
91
101
|
const failed = final.attempts
|
|
92
|
-
.filter((attempt) => attempt.status !== 'passed' && attempt.status !== 'skipped' && (attempt.error !== undefined || attempt.steps.length > 0))
|
|
102
|
+
.filter((attempt) => attempt.status !== 'passed' && attempt.status !== 'skipped' && attempt.status !== 'interrupted' && (attempt.error !== undefined || attempt.steps.length > 0))
|
|
93
103
|
.map((attempt) => ({
|
|
94
104
|
code: cell(attempt.error?.code ?? attempt.status, 128),
|
|
95
105
|
step: failedStepOf(attempt.steps)?.index,
|
package/dist/report/format.d.ts
CHANGED
|
@@ -58,20 +58,25 @@ export declare function rule(text: string, align: 'center' | 'right'): {
|
|
|
58
58
|
export interface Counters {
|
|
59
59
|
passed: number;
|
|
60
60
|
failed: number;
|
|
61
|
+
interrupted: number;
|
|
61
62
|
flaky: number;
|
|
62
63
|
skipped: number;
|
|
63
64
|
total: number;
|
|
64
65
|
}
|
|
65
66
|
export declare function emptyCounters(): Counters;
|
|
66
|
-
/**
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* The counter a result status lands in. A timeout is a failure; an
|
|
69
|
+
* interrupted test was stopped by the run, not by its own verdict, so it
|
|
70
|
+
* counts on its own.
|
|
71
|
+
*/
|
|
72
|
+
export declare function statusBucket(status: ResultStatus): 'passed' | 'failed' | 'interrupted' | 'flaky' | 'skipped';
|
|
68
73
|
/** Counts results by bucket; `total` is the number of items. */
|
|
69
74
|
export declare function tally(items: readonly {
|
|
70
75
|
readonly status: ResultStatus;
|
|
71
76
|
}[]): Counters;
|
|
72
|
-
/** A file's outcome from its tests: failed if any did, skipped if all
|
|
73
|
-
export declare function fileOutcome(counters: Counters): 'passed' | 'failed' | 'skipped';
|
|
74
|
-
/** `2 failed | 10 passed | 1 flaky (
|
|
77
|
+
/** A file's outcome from its tests: failed if any did, else interrupted if any was, skipped if all were, else passed. */
|
|
78
|
+
export declare function fileOutcome(counters: Counters): 'passed' | 'failed' | 'interrupted' | 'skipped';
|
|
79
|
+
/** `2 failed | 1 interrupted | 10 passed | 1 flaky (14)` in vitest's colors and order. */
|
|
75
80
|
export declare function stateString(pc: Colors, counters: Counters): string;
|
|
76
81
|
export interface AiUsage {
|
|
77
82
|
calls: number;
|
package/dist/report/format.js
CHANGED
|
@@ -96,11 +96,15 @@ export function rule(text, align) {
|
|
|
96
96
|
return { before: LONG_DASH.repeat(before), after: LONG_DASH.repeat(after) };
|
|
97
97
|
}
|
|
98
98
|
export function emptyCounters() {
|
|
99
|
-
return { passed: 0, failed: 0, flaky: 0, skipped: 0, total: 0 };
|
|
99
|
+
return { passed: 0, failed: 0, interrupted: 0, flaky: 0, skipped: 0, total: 0 };
|
|
100
100
|
}
|
|
101
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* The counter a result status lands in. A timeout is a failure; an
|
|
103
|
+
* interrupted test was stopped by the run, not by its own verdict, so it
|
|
104
|
+
* counts on its own.
|
|
105
|
+
*/
|
|
102
106
|
export function statusBucket(status) {
|
|
103
|
-
return status === '
|
|
107
|
+
return status === 'timed-out' ? 'failed' : status;
|
|
104
108
|
}
|
|
105
109
|
/** Counts results by bucket; `total` is the number of items. */
|
|
106
110
|
export function tally(items) {
|
|
@@ -111,18 +115,21 @@ export function tally(items) {
|
|
|
111
115
|
}
|
|
112
116
|
return counters;
|
|
113
117
|
}
|
|
114
|
-
/** A file's outcome from its tests: failed if any did, skipped if all
|
|
118
|
+
/** A file's outcome from its tests: failed if any did, else interrupted if any was, skipped if all were, else passed. */
|
|
115
119
|
export function fileOutcome(counters) {
|
|
116
120
|
if (counters.failed > 0)
|
|
117
121
|
return 'failed';
|
|
122
|
+
if (counters.interrupted > 0)
|
|
123
|
+
return 'interrupted';
|
|
118
124
|
if (counters.total > 0 && counters.skipped === counters.total)
|
|
119
125
|
return 'skipped';
|
|
120
126
|
return 'passed';
|
|
121
127
|
}
|
|
122
|
-
/** `2 failed | 10 passed | 1 flaky (
|
|
128
|
+
/** `2 failed | 1 interrupted | 10 passed | 1 flaky (14)` in vitest's colors and order. */
|
|
123
129
|
export function stateString(pc, counters) {
|
|
124
130
|
const parts = [
|
|
125
131
|
counters.failed > 0 ? pc.bold(pc.red(`${counters.failed} failed`)) : undefined,
|
|
132
|
+
counters.interrupted > 0 ? pc.bold(pc.yellow(`${counters.interrupted} interrupted`)) : undefined,
|
|
126
133
|
pc.bold(pc.green(`${counters.passed} passed`)),
|
|
127
134
|
counters.flaky > 0 ? pc.yellow(`${counters.flaky} flaky`) : undefined,
|
|
128
135
|
counters.skipped > 0 ? pc.gray(`${counters.skipped} skipped`) : undefined,
|