@dxos/test-utils 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.13.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/dist/lib/playwright.mjs +241 -29
- package/dist/lib/playwright.mjs.map +1 -1
- package/dist/types/src/claude-agent.d.ts +116 -0
- package/dist/types/src/claude-agent.d.ts.map +1 -0
- package/dist/types/src/errno-shim.d.ts +18 -0
- package/dist/types/src/errno-shim.d.ts.map +1 -0
- package/dist/types/src/errno-shim.test.d.ts +2 -0
- package/dist/types/src/errno-shim.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/playwright.d.ts +5 -0
- package/dist/types/src/playwright.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -4
- package/src/claude-agent.ts +294 -0
- package/src/errno-shim.test.ts +105 -0
- package/src/errno-shim.ts +146 -0
- package/src/index.ts +1 -1
- package/src/playwright.ts +126 -39
- package/src/webkit-workarounds.md +45 -0
package/dist/lib/playwright.mjs
CHANGED
|
@@ -1,8 +1,158 @@
|
|
|
1
1
|
import { devices } from "@playwright/test";
|
|
2
|
-
import { existsSync, readFileSync } from "@dxos/node-std/fs";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "@dxos/node-std/fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import { dirname, join, resolve } from "@dxos/node-std/path";
|
|
4
5
|
import pkgUp from "pkg-up";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { createHash } from "@dxos/node-std/crypto";
|
|
5
8
|
import { trigger } from "@dxos/async";
|
|
9
|
+
//#region src/errno-shim.ts
|
|
10
|
+
/**
|
|
11
|
+
* Shared object preloaded into Linux WebKit that restores errno around every signal handler installed with `sigaction`:
|
|
12
|
+
* WebKit's thread-suspend handler returns from `sigsuspend` with errno set to EINTR on the thread it interrupted, which
|
|
13
|
+
* fails an errno-checked parse in progress there (ICE candidates, SDP ports, GLib `getauxval`).
|
|
14
|
+
* Freestanding, for glibc on x86_64 and aarch64, whose `struct sigaction` share this layout.
|
|
15
|
+
*
|
|
16
|
+
* Delete this file once Playwright ships a WebKit that restores errno itself — see `webkit-workarounds.md`.
|
|
17
|
+
*/
|
|
18
|
+
var ERRNO_SHIM_SOURCE = `#define SA_SIGINFO_FLAG 4
|
|
19
|
+
#define SIGNAL_COUNT 65
|
|
20
|
+
#define RTLD_NEXT_HANDLE ((void *)-1L)
|
|
21
|
+
|
|
22
|
+
struct glibc_sigaction {
|
|
23
|
+
void *handler;
|
|
24
|
+
unsigned long mask[16];
|
|
25
|
+
int flags;
|
|
26
|
+
void (*restorer)(void);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
typedef int (*sigaction_fn)(int, const struct glibc_sigaction *, struct glibc_sigaction *);
|
|
30
|
+
|
|
31
|
+
extern void *dlsym(void *handle, const char *name);
|
|
32
|
+
extern int *__errno_location(void);
|
|
33
|
+
|
|
34
|
+
static sigaction_fn next_sigaction;
|
|
35
|
+
|
|
36
|
+
/* The handler the process installed for each signal, and whether it asked for siginfo. */
|
|
37
|
+
static void *volatile handlers[SIGNAL_COUNT];
|
|
38
|
+
static volatile int takes_siginfo[SIGNAL_COUNT];
|
|
39
|
+
|
|
40
|
+
/* Resolved at load, so a signal handler never has to call dlsym; a constructor that ran earlier resolves it lazily. */
|
|
41
|
+
__attribute__((constructor)) static void resolve_next_sigaction(void) {
|
|
42
|
+
if (!next_sigaction) {
|
|
43
|
+
next_sigaction = (sigaction_fn)dlsym(RTLD_NEXT_HANDLE, "sigaction");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Passes all three arguments, which a one-argument handler ignores on x86_64 and aarch64. */
|
|
48
|
+
static void trampoline(int signal, void *info, void *context) {
|
|
49
|
+
int *error = __errno_location();
|
|
50
|
+
int saved = *error;
|
|
51
|
+
((void (*)(int, void *, void *))handlers[signal])(signal, info, context);
|
|
52
|
+
*error = saved;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Reports the process's own handler where the kernel holds the trampoline. */
|
|
56
|
+
static void unwrap(struct glibc_sigaction *action, void *handler, int siginfo) {
|
|
57
|
+
if (action && action->handler == (void *)trampoline) {
|
|
58
|
+
action->handler = handler;
|
|
59
|
+
action->flags = (action->flags & ~SA_SIGINFO_FLAG) | siginfo;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
int sigaction(int signal, const struct glibc_sigaction *action, struct glibc_sigaction *previous) {
|
|
64
|
+
resolve_next_sigaction();
|
|
65
|
+
if (signal <= 0 || signal >= SIGNAL_COUNT) {
|
|
66
|
+
return next_sigaction(signal, action, previous);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void *old_handler = handlers[signal];
|
|
70
|
+
int old_siginfo = takes_siginfo[signal];
|
|
71
|
+
/* A trampoline read back through glibc's internal sigaction, as signal() returns it, keeps the recorded handler. */
|
|
72
|
+
if (action && action->handler == (void *)trampoline) {
|
|
73
|
+
struct glibc_sigaction restored = *action;
|
|
74
|
+
restored.flags |= SA_SIGINFO_FLAG;
|
|
75
|
+
int result = next_sigaction(signal, &restored, previous);
|
|
76
|
+
if (result == 0) {
|
|
77
|
+
unwrap(previous, old_handler, old_siginfo);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
if (!action || action->handler == (void *)0 || action->handler == (void *)1) {
|
|
82
|
+
int result = next_sigaction(signal, action, previous);
|
|
83
|
+
if (result == 0) {
|
|
84
|
+
unwrap(previous, old_handler, old_siginfo);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
handlers[signal] = action->handler;
|
|
90
|
+
takes_siginfo[signal] = action->flags & SA_SIGINFO_FLAG;
|
|
91
|
+
struct glibc_sigaction wrapped = *action;
|
|
92
|
+
wrapped.handler = (void *)trampoline;
|
|
93
|
+
wrapped.flags |= SA_SIGINFO_FLAG;
|
|
94
|
+
int result = next_sigaction(signal, &wrapped, previous);
|
|
95
|
+
if (result != 0) {
|
|
96
|
+
handlers[signal] = old_handler;
|
|
97
|
+
takes_siginfo[signal] = old_siginfo;
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
unwrap(previous, old_handler, old_siginfo);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
/** Whether {@link ERRNO_SHIM_SOURCE} fits this process's platform and architecture. */
|
|
105
|
+
var errnoShimSupported = () => process.platform === "linux" && (process.arch === "x64" || process.arch === "arm64");
|
|
106
|
+
/**
|
|
107
|
+
* Compiles {@link ERRNO_SHIM_SOURCE} into `cacheDir` once per source, compiler invocation and architecture,
|
|
108
|
+
* and returns its path.
|
|
109
|
+
*/
|
|
110
|
+
var buildErrnoShim = (cacheDir) => {
|
|
111
|
+
const hash = createHash("sha256").update(JSON.stringify([
|
|
112
|
+
ERRNO_SHIM_SOURCE,
|
|
113
|
+
compiler(),
|
|
114
|
+
COMPILE_ARGS,
|
|
115
|
+
LINK_ARGS
|
|
116
|
+
])).digest("hex").slice(0, 16);
|
|
117
|
+
const output = join(cacheDir, `errno-shim-${process.arch}-${hash}.so`);
|
|
118
|
+
if (existsSync(output)) return output;
|
|
119
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
120
|
+
const partial = `${output}.${process.pid}`;
|
|
121
|
+
try {
|
|
122
|
+
compileSharedObject(ERRNO_SHIM_SOURCE, partial);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
125
|
+
throw new Error(`Linux WebKit e2e needs a C compiler (\`cc\`, or \`CC\`) to build its errno shim: ${detail}`);
|
|
126
|
+
}
|
|
127
|
+
renameSync(partial, output);
|
|
128
|
+
return output;
|
|
129
|
+
};
|
|
130
|
+
var COMPILE_ARGS = [
|
|
131
|
+
"-shared",
|
|
132
|
+
"-fPIC",
|
|
133
|
+
"-O2",
|
|
134
|
+
"-x",
|
|
135
|
+
"c",
|
|
136
|
+
"-o"
|
|
137
|
+
];
|
|
138
|
+
var LINK_ARGS = ["-ldl"];
|
|
139
|
+
var compiler = () => process.env.CC || "cc";
|
|
140
|
+
var compileSharedObject = (source, output) => {
|
|
141
|
+
execFileSync(compiler(), [
|
|
142
|
+
...COMPILE_ARGS,
|
|
143
|
+
output,
|
|
144
|
+
"-",
|
|
145
|
+
...LINK_ARGS
|
|
146
|
+
], {
|
|
147
|
+
input: source,
|
|
148
|
+
stdio: [
|
|
149
|
+
"pipe",
|
|
150
|
+
"ignore",
|
|
151
|
+
"pipe"
|
|
152
|
+
]
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
//#endregion
|
|
6
156
|
//#region src/lock.ts
|
|
7
157
|
var Lock = class {
|
|
8
158
|
_lastPromise = Promise.resolve();
|
|
@@ -20,6 +170,10 @@ var Lock = class {
|
|
|
20
170
|
};
|
|
21
171
|
//#endregion
|
|
22
172
|
//#region src/playwright.ts
|
|
173
|
+
/** Values that turn a boolean `DX_E2E_*` knob off; anything else, including unset, leaves it on. */
|
|
174
|
+
var DISABLED = /* @__PURE__ */ new Set(["0", "false"]);
|
|
175
|
+
/** All three work around bugs in Playwright's WebKit; `webkit-workarounds.md` says how to retest and retire them. */
|
|
176
|
+
var webkitWorkarounds = () => !DISABLED.has(process.env.DX_E2E_WEBKIT_WORKAROUNDS ?? "");
|
|
23
177
|
var findWorkspaceRoot = (startDir) => {
|
|
24
178
|
let dir = resolve(startDir);
|
|
25
179
|
while (dir !== "/") {
|
|
@@ -43,6 +197,7 @@ var e2ePreset = (testDir) => {
|
|
|
43
197
|
const testResultOuputDir = join(workspaceRoot, "test-results/playwright/output", packageDirName);
|
|
44
198
|
const reporterOutputFile = join(workspaceRoot, "test-results/playwright/report", `${packageDirName}.json`);
|
|
45
199
|
const browser = process.env.PLAYWRIGHT_BROWSER || (process.env.CI ? "all" : "chromium");
|
|
200
|
+
const runsWebKit = browser === "all" || browser === "webkit";
|
|
46
201
|
const sandboxProxy = process.env.CLAUDE_CODE_REMOTE ? process.env.HTTPS_PROXY : void 0;
|
|
47
202
|
const sandboxChromium = sandboxProxy ? { launchOptions: {
|
|
48
203
|
executablePath: "/opt/pw-browsers/chromium",
|
|
@@ -65,10 +220,13 @@ var e2ePreset = (testDir) => {
|
|
|
65
220
|
name: "firefox",
|
|
66
221
|
use: { ...devices["Desktop Firefox"] }
|
|
67
222
|
},
|
|
68
|
-
{
|
|
223
|
+
...runsWebKit ? [{
|
|
69
224
|
name: "webkit",
|
|
70
|
-
use: {
|
|
71
|
-
|
|
225
|
+
use: {
|
|
226
|
+
...devices["Desktop Safari"],
|
|
227
|
+
launchOptions: { env: webKitLaunchEnv(workspaceRoot) }
|
|
228
|
+
}
|
|
229
|
+
}] : []
|
|
72
230
|
].filter((project) => {
|
|
73
231
|
return browser === "all" || project.name === browser;
|
|
74
232
|
});
|
|
@@ -92,36 +250,90 @@ var e2ePreset = (testDir) => {
|
|
|
92
250
|
projects
|
|
93
251
|
};
|
|
94
252
|
};
|
|
253
|
+
/** `process.env` without unset keys, the shape Playwright's launch `env` takes. */
|
|
254
|
+
var definedEnv = () => {
|
|
255
|
+
const env = {};
|
|
256
|
+
for (const [key, value] of Object.entries(process.env)) if (value !== void 0) env[key] = value;
|
|
257
|
+
return env;
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Environment for a WebKit launch. Replaces the browser's whole environment rather than extending it,
|
|
261
|
+
* so it carries `process.env` alongside the two crash workarounds.
|
|
262
|
+
*/
|
|
263
|
+
var webKitLaunchEnv = (workspaceRoot) => {
|
|
264
|
+
const disableWasmIPInt = webkitWorkarounds() && process.platform === "linux" && process.arch === "x64";
|
|
265
|
+
const errnoShim = webkitWorkarounds() && errnoShimSupported() ? buildErrnoShim(join(workspaceRoot, "node_modules/.cache/dxos-test-utils")) : void 0;
|
|
266
|
+
return {
|
|
267
|
+
...disableWasmIPInt ? { JSC_useWasmIPInt: "false" } : {},
|
|
268
|
+
...definedEnv(),
|
|
269
|
+
...errnoShim ? { LD_PRELOAD: [errnoShim, process.env.LD_PRELOAD].filter(Boolean).join(":") } : {}
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* WebKit keeps OPFS under the browser's data directory, and an ephemeral context has none, so every
|
|
274
|
+
* `navigator.storage` call there fails and no app backed by OPFS boots. A persistent context has one.
|
|
275
|
+
* Its temporary directory is per context, which keeps the isolation a fresh context gives.
|
|
276
|
+
*/
|
|
277
|
+
var createContext = async (browser) => {
|
|
278
|
+
const browserType = browser.browserType();
|
|
279
|
+
if (browserType.name() !== "webkit" || !webkitWorkarounds()) return { context: await browser.newContext() };
|
|
280
|
+
const userDataDir = mkdtempSync(join(tmpdir(), "dxos-webkit-"));
|
|
281
|
+
const context = await browserType.launchPersistentContext(userDataDir, { env: webKitLaunchEnv(findWorkspaceRoot(process.cwd())) });
|
|
282
|
+
context.once("close", () => rmSync(userDataDir, {
|
|
283
|
+
recursive: true,
|
|
284
|
+
force: true
|
|
285
|
+
}));
|
|
286
|
+
return {
|
|
287
|
+
context,
|
|
288
|
+
page: context.pages()[0]
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* Opens a page, creating a context for it unless handed one to borrow.
|
|
293
|
+
* `close()` disposes whatever this created: the context when it made one, otherwise just the page.
|
|
294
|
+
*/
|
|
95
295
|
var setupPage = async (browser, options = {}) => {
|
|
96
296
|
const { url, bridgeLogs, viewportSize } = options;
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
297
|
+
const created = "newContext" in browser ? await createContext(browser) : void 0;
|
|
298
|
+
const context = created?.context ?? browser;
|
|
299
|
+
const ownsContext = created !== void 0;
|
|
300
|
+
let page;
|
|
301
|
+
const close = async () => {
|
|
302
|
+
await (ownsContext ? context.close() : page?.close());
|
|
303
|
+
};
|
|
304
|
+
try {
|
|
305
|
+
page = created?.page ?? await context.newPage();
|
|
306
|
+
if (viewportSize) await page.setViewportSize(viewportSize);
|
|
307
|
+
if (bridgeLogs) {
|
|
308
|
+
const lock = new Lock();
|
|
309
|
+
page.on("pageerror", async (error) => {
|
|
110
310
|
await lock.executeSynchronized(async () => {
|
|
111
|
-
|
|
112
|
-
if (args.length > 0) console.log(...args);
|
|
113
|
-
else console.log(msg);
|
|
311
|
+
console.log(error);
|
|
114
312
|
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
313
|
+
});
|
|
314
|
+
page.on("console", async (msg) => {
|
|
315
|
+
try {
|
|
316
|
+
const argsPromise = Promise.all(msg.args().map((x) => x.jsonValue()));
|
|
317
|
+
await lock.executeSynchronized(async () => {
|
|
318
|
+
const args = await argsPromise;
|
|
319
|
+
if (args.length > 0) console.log(...args);
|
|
320
|
+
else console.log(msg);
|
|
321
|
+
});
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error("Failed to parse message", err);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (url) await page.goto(url);
|
|
328
|
+
return {
|
|
329
|
+
context,
|
|
330
|
+
page,
|
|
331
|
+
close
|
|
332
|
+
};
|
|
333
|
+
} catch (err) {
|
|
334
|
+
await close().catch(() => {});
|
|
335
|
+
throw err;
|
|
119
336
|
}
|
|
120
|
-
if (url) await page.goto(url);
|
|
121
|
-
return {
|
|
122
|
-
context,
|
|
123
|
-
page
|
|
124
|
-
};
|
|
125
337
|
};
|
|
126
338
|
var storybookUrl = (storyId, port = 9009) => `http://localhost:${port}/iframe.html?id=${storyId}&viewMode=story`;
|
|
127
339
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playwright.mjs","names":[],"sources":["../../src/lock.ts","../../src/playwright.ts"],"sourcesContent":["//\n// Copyright 2022 DXOS.org\n//\n\nimport { trigger } from '@dxos/async';\n\n// Copied from @dxos/async.\nexport class Lock {\n private _lastPromise = Promise.resolve();\n\n async executeSynchronized<T>(fun: () => Promise<T>): Promise<T> {\n const prevPromise = this._lastPromise;\n const [getPromise, resolve] = trigger();\n this._lastPromise = getPromise();\n\n await prevPromise;\n try {\n const value = await fun();\n return value;\n } finally {\n resolve();\n }\n }\n}\n","//\n// Copyright 2023 DXOS.org\n//\n\n/* eslint-disable no-console */\n\nimport {\n type Browser,\n type BrowserContext,\n type Page,\n type PlaywrightTestConfig,\n type ReporterDescription,\n devices,\n} from '@playwright/test';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport pkgUp from 'pkg-up';\n\nimport { Lock } from './lock';\n\nconst findWorkspaceRoot = (startDir: string): string => {\n let dir = resolve(startDir);\n while (dir !== '/') {\n try {\n // Check for pnpm-workspace.yaml first (modern pnpm approach)\n const workspaceYamlPath = join(dir, 'pnpm-workspace.yaml');\n if (existsSync(workspaceYamlPath)) {\n return dir;\n }\n\n // Check for package.json with workspaces field (legacy approach)\n const pkgPath = join(dir, 'package.json');\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces) {\n return dir;\n }\n } catch {}\n const parent = dirname(dir);\n if (parent === dir) {\n break;\n }\n dir = parent;\n }\n\n throw new Error('Could not find pnpm workspace root');\n};\n\nexport const e2ePreset = (testDir: string): PlaywrightTestConfig => {\n const packageJson = pkgUp.sync({ cwd: testDir });\n const packageDir = packageJson!.split('/').slice(0, -1).join('/');\n const packageDirName = packageDir.split('/').pop();\n if (!packageDirName) {\n throw new Error('packageDirName not found');\n }\n\n const workspaceRoot = findWorkspaceRoot(packageDir);\n const testResultOuputDir = join(workspaceRoot, 'test-results/playwright/output', packageDirName);\n const reporterOutputFile = join(workspaceRoot, 'test-results/playwright/report', `${packageDirName}.json`);\n\n const browser = process.env.PLAYWRIGHT_BROWSER || (process.env.CI ? 'all' : 'chromium');\n // In the Claude Code cloud sandbox chromium needs a pinned executable, the egress proxy passed via\n // ARGS (Playwright's `proxy:` option drops its bypass list for non-default contexts), and a TLS 1.2\n // cap (see the cloud-sandbox skill). Gated so real dev/CI runs are never silently downgraded.\n const sandboxProxy = process.env.CLAUDE_CODE_REMOTE ? process.env.HTTPS_PROXY : undefined;\n const sandboxChromium = sandboxProxy\n ? {\n launchOptions: {\n executablePath: '/opt/pw-browsers/chromium',\n args: [\n '--no-sandbox',\n `--proxy-server=${sandboxProxy}`,\n '--proxy-bypass-list=127.0.0.1;localhost',\n '--ssl-version-max=tls1.2',\n ],\n },\n }\n : {};\n const projects = [\n {\n name: 'chromium',\n use: { ...devices['Desktop Chrome'], ...sandboxChromium },\n },\n {\n name: 'firefox',\n use: { ...devices['Desktop Firefox'] },\n },\n {\n name: 'webkit',\n use: { ...devices['Desktop Safari'] },\n },\n ].filter((project) => {\n return browser === 'all' || project.name === browser;\n });\n\n return {\n testDir,\n outputDir: testResultOuputDir,\n // Playwright's default is 30s, which equals the action bound below — leaving a test no budget beyond\n // a single slow action. Storybook-backed suites also pay an on-demand story compile in the first\n // test's `beforeEach`, which alone exceeded 30s. Individual configs may still raise this.\n timeout: 60_000,\n // Run tests in files in parallel.\n fullyParallel: true,\n // Fail the build on CI if you accidentally left test.only in the source code.\n forbidOnly: !!process.env.CI,\n // No retries anywhere: retrying hides flakes behind a 3x time cost, making shard timings unusable\n // for sizing the suite. A flake now fails loudly and gets skipped with a TODO instead.\n retries: 0,\n // 4 workers starved shared setup into false \"not stable\"/\"detached\" failures, so 2 is the\n // compromise. `|| 2`, not `??`: an env var set to the empty string (common in Actions) would\n // otherwise coerce to 0 workers.\n workers: Number(process.env.PLAYWRIGHT_WORKERS) || 2,\n // Reporter to use. See https://playwright.dev/docs/test-reporters.\n reporter: [\n ...(process.env.CI\n ? ([\n ['list'],\n [\n 'json',\n {\n outputFile: reporterOutputFile,\n },\n ],\n ['junit', { outputFile: reporterOutputFile.replace(/\\.json$/, '.xml') }],\n ] satisfies ReporterDescription[])\n : ([['list']] satisfies ReporterDescription[])),\n ],\n use: {\n trace: 'retain-on-failure',\n // Playwright's default is no limit, so a stuck locator would absorb the whole per-test budget and\n // report a bare `Test timeout` naming nothing.\n actionTimeout: 30_000,\n },\n projects,\n };\n};\n\nexport type SetupOptions = {\n url?: string;\n bridgeLogs?: boolean;\n viewportSize?: Parameters<Page['setViewportSize']>[0];\n};\n\nexport const setupPage = async (browser: Browser | BrowserContext, options: SetupOptions = {}) => {\n const { url, bridgeLogs, viewportSize } = options;\n\n const context = 'newContext' in browser ? await browser.newContext() : browser;\n const page = await context.newPage();\n\n if (viewportSize) {\n await page.setViewportSize(viewportSize);\n }\n\n // TODO(wittjosiah): Remove?\n if (bridgeLogs) {\n const lock = new Lock();\n\n page.on('pageerror', async (error) => {\n await lock.executeSynchronized(async () => {\n // eslint-disable-next-line no-console\n console.log(error);\n });\n });\n\n page.on('console', async (msg) => {\n try {\n const argsPromise = Promise.all(msg.args().map((x) => x.jsonValue()));\n await lock.executeSynchronized(async () => {\n const args = await argsPromise;\n\n if (args.length > 0) {\n console.log(...args);\n } else {\n console.log(msg);\n }\n });\n } catch (err) {\n console.error('Failed to parse message', err);\n }\n });\n }\n\n if (url) {\n await page.goto(url);\n }\n\n return { context, page };\n};\n\nexport const storybookUrl = (storyId: string, port = 9009) =>\n `http://localhost:${port}/iframe.html?id=${storyId}&viewMode=story`;\n\n/**\n * Playwright `webServer` for a Storybook-backed suite. Readiness is probed by `url` rather than\n * `port` because a `port` probe is a bare TCP check, which `storybook dev` satisfies by binding the\n * socket before it can serve — tests starting in that gap get ERR_CONNECTION_REFUSED.\n */\nexport const storybookWebServer = (port: number) => ({\n command: `pnpm storybook dev --ci --quiet --port=${port} --config-dir=.storybook`,\n url: `http://localhost:${port}`,\n reuseExistingServer: false,\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"playwright.mjs","names":[],"sources":["../../src/errno-shim.ts","../../src/lock.ts","../../src/playwright.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { execFileSync } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport { existsSync, mkdirSync, renameSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Shared object preloaded into Linux WebKit that restores errno around every signal handler installed with `sigaction`:\n * WebKit's thread-suspend handler returns from `sigsuspend` with errno set to EINTR on the thread it interrupted, which\n * fails an errno-checked parse in progress there (ICE candidates, SDP ports, GLib `getauxval`).\n * Freestanding, for glibc on x86_64 and aarch64, whose `struct sigaction` share this layout.\n *\n * Delete this file once Playwright ships a WebKit that restores errno itself — see `webkit-workarounds.md`.\n */\nexport const ERRNO_SHIM_SOURCE = `#define SA_SIGINFO_FLAG 4\n#define SIGNAL_COUNT 65\n#define RTLD_NEXT_HANDLE ((void *)-1L)\n\nstruct glibc_sigaction {\n void *handler;\n unsigned long mask[16];\n int flags;\n void (*restorer)(void);\n};\n\ntypedef int (*sigaction_fn)(int, const struct glibc_sigaction *, struct glibc_sigaction *);\n\nextern void *dlsym(void *handle, const char *name);\nextern int *__errno_location(void);\n\nstatic sigaction_fn next_sigaction;\n\n/* The handler the process installed for each signal, and whether it asked for siginfo. */\nstatic void *volatile handlers[SIGNAL_COUNT];\nstatic volatile int takes_siginfo[SIGNAL_COUNT];\n\n/* Resolved at load, so a signal handler never has to call dlsym; a constructor that ran earlier resolves it lazily. */\n__attribute__((constructor)) static void resolve_next_sigaction(void) {\n if (!next_sigaction) {\n next_sigaction = (sigaction_fn)dlsym(RTLD_NEXT_HANDLE, \"sigaction\");\n }\n}\n\n/* Passes all three arguments, which a one-argument handler ignores on x86_64 and aarch64. */\nstatic void trampoline(int signal, void *info, void *context) {\n int *error = __errno_location();\n int saved = *error;\n ((void (*)(int, void *, void *))handlers[signal])(signal, info, context);\n *error = saved;\n}\n\n/* Reports the process's own handler where the kernel holds the trampoline. */\nstatic void unwrap(struct glibc_sigaction *action, void *handler, int siginfo) {\n if (action && action->handler == (void *)trampoline) {\n action->handler = handler;\n action->flags = (action->flags & ~SA_SIGINFO_FLAG) | siginfo;\n }\n}\n\nint sigaction(int signal, const struct glibc_sigaction *action, struct glibc_sigaction *previous) {\n resolve_next_sigaction();\n if (signal <= 0 || signal >= SIGNAL_COUNT) {\n return next_sigaction(signal, action, previous);\n }\n\n void *old_handler = handlers[signal];\n int old_siginfo = takes_siginfo[signal];\n /* A trampoline read back through glibc's internal sigaction, as signal() returns it, keeps the recorded handler. */\n if (action && action->handler == (void *)trampoline) {\n struct glibc_sigaction restored = *action;\n restored.flags |= SA_SIGINFO_FLAG;\n int result = next_sigaction(signal, &restored, previous);\n if (result == 0) {\n unwrap(previous, old_handler, old_siginfo);\n }\n return result;\n }\n if (!action || action->handler == (void *)0 || action->handler == (void *)1) {\n int result = next_sigaction(signal, action, previous);\n if (result == 0) {\n unwrap(previous, old_handler, old_siginfo);\n }\n return result;\n }\n\n handlers[signal] = action->handler;\n takes_siginfo[signal] = action->flags & SA_SIGINFO_FLAG;\n struct glibc_sigaction wrapped = *action;\n wrapped.handler = (void *)trampoline;\n wrapped.flags |= SA_SIGINFO_FLAG;\n int result = next_sigaction(signal, &wrapped, previous);\n if (result != 0) {\n handlers[signal] = old_handler;\n takes_siginfo[signal] = old_siginfo;\n return result;\n }\n unwrap(previous, old_handler, old_siginfo);\n return result;\n}\n`;\n\n/** Whether {@link ERRNO_SHIM_SOURCE} fits this process's platform and architecture. */\nexport const errnoShimSupported = (): boolean =>\n process.platform === 'linux' && (process.arch === 'x64' || process.arch === 'arm64');\n\n/**\n * Compiles {@link ERRNO_SHIM_SOURCE} into `cacheDir` once per source, compiler invocation and architecture,\n * and returns its path.\n */\nexport const buildErrnoShim = (cacheDir: string): string => {\n const hash = createHash('sha256')\n .update(JSON.stringify([ERRNO_SHIM_SOURCE, compiler(), COMPILE_ARGS, LINK_ARGS]))\n .digest('hex')\n .slice(0, 16);\n const output = join(cacheDir, `errno-shim-${process.arch}-${hash}.so`);\n if (existsSync(output)) {\n return output;\n }\n\n mkdirSync(cacheDir, { recursive: true });\n // Renamed into place, so a concurrent Playwright worker never preloads a partly written file.\n const partial = `${output}.${process.pid}`;\n try {\n compileSharedObject(ERRNO_SHIM_SOURCE, partial);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`Linux WebKit e2e needs a C compiler (\\`cc\\`, or \\`CC\\`) to build its errno shim: ${detail}`);\n }\n renameSync(partial, output);\n return output;\n};\n\nconst COMPILE_ARGS = ['-shared', '-fPIC', '-O2', '-x', 'c', '-o'];\nconst LINK_ARGS = ['-ldl'];\n\nconst compiler = (): string => process.env.CC || 'cc';\n\nexport const compileSharedObject = (source: string, output: string): void => {\n execFileSync(compiler(), [...COMPILE_ARGS, output, '-', ...LINK_ARGS], {\n input: source,\n stdio: ['pipe', 'ignore', 'pipe'],\n });\n};\n","//\n// Copyright 2022 DXOS.org\n//\n\nimport { trigger } from '@dxos/async';\n\n// Copied from @dxos/async.\nexport class Lock {\n private _lastPromise = Promise.resolve();\n\n async executeSynchronized<T>(fun: () => Promise<T>): Promise<T> {\n const prevPromise = this._lastPromise;\n const [getPromise, resolve] = trigger();\n this._lastPromise = getPromise();\n\n await prevPromise;\n try {\n const value = await fun();\n return value;\n } finally {\n resolve();\n }\n }\n}\n","//\n// Copyright 2023 DXOS.org\n//\n\n/* eslint-disable no-console */\n\nimport {\n type Browser,\n type BrowserContext,\n type Page,\n type PlaywrightTestConfig,\n type ReporterDescription,\n devices,\n} from '@playwright/test';\nimport { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport pkgUp from 'pkg-up';\n\nimport { buildErrnoShim, errnoShimSupported } from './errno-shim.ts';\nimport { Lock } from './lock.ts';\n\n/** Values that turn a boolean `DX_E2E_*` knob off; anything else, including unset, leaves it on. */\nconst DISABLED = new Set(['0', 'false']);\n\n/** All three work around bugs in Playwright's WebKit; `webkit-workarounds.md` says how to retest and retire them. */\nconst webkitWorkarounds = (): boolean => !DISABLED.has(process.env.DX_E2E_WEBKIT_WORKAROUNDS ?? '');\n\nconst findWorkspaceRoot = (startDir: string): string => {\n let dir = resolve(startDir);\n while (dir !== '/') {\n try {\n // Check for pnpm-workspace.yaml first (modern pnpm approach)\n const workspaceYamlPath = join(dir, 'pnpm-workspace.yaml');\n if (existsSync(workspaceYamlPath)) {\n return dir;\n }\n\n // Check for package.json with workspaces field (legacy approach)\n const pkgPath = join(dir, 'package.json');\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces) {\n return dir;\n }\n } catch {}\n const parent = dirname(dir);\n if (parent === dir) {\n break;\n }\n dir = parent;\n }\n\n throw new Error('Could not find pnpm workspace root');\n};\n\nexport const e2ePreset = (testDir: string): PlaywrightTestConfig => {\n const packageJson = pkgUp.sync({ cwd: testDir });\n const packageDir = packageJson!.split('/').slice(0, -1).join('/');\n const packageDirName = packageDir.split('/').pop();\n if (!packageDirName) {\n throw new Error('packageDirName not found');\n }\n\n const workspaceRoot = findWorkspaceRoot(packageDir);\n const testResultOuputDir = join(workspaceRoot, 'test-results/playwright/output', packageDirName);\n const reporterOutputFile = join(workspaceRoot, 'test-results/playwright/report', `${packageDirName}.json`);\n\n const browser = process.env.PLAYWRIGHT_BROWSER || (process.env.CI ? 'all' : 'chromium');\n const runsWebKit = browser === 'all' || browser === 'webkit';\n // In the Claude Code cloud sandbox chromium needs a pinned executable, the egress proxy passed via\n // ARGS (Playwright's `proxy:` option drops its bypass list for non-default contexts), and a TLS 1.2\n // cap (see the cloud-sandbox skill). Gated so real dev/CI runs are never silently downgraded.\n const sandboxProxy = process.env.CLAUDE_CODE_REMOTE ? process.env.HTTPS_PROXY : undefined;\n const sandboxChromium = sandboxProxy\n ? {\n launchOptions: {\n executablePath: '/opt/pw-browsers/chromium',\n args: [\n '--no-sandbox',\n `--proxy-server=${sandboxProxy}`,\n '--proxy-bypass-list=127.0.0.1;localhost',\n '--ssl-version-max=tls1.2',\n ],\n },\n }\n : {};\n const projects = [\n {\n name: 'chromium',\n use: { ...devices['Desktop Chrome'], ...sandboxChromium },\n },\n {\n name: 'firefox',\n use: { ...devices['Desktop Firefox'] },\n },\n ...(runsWebKit\n ? [\n {\n name: 'webkit',\n use: {\n ...devices['Desktop Safari'],\n launchOptions: { env: webKitLaunchEnv(workspaceRoot) },\n },\n },\n ]\n : []),\n ].filter((project) => {\n return browser === 'all' || project.name === browser;\n });\n\n return {\n testDir,\n outputDir: testResultOuputDir,\n // Playwright's default is 30s, which equals the action bound below — leaving a test no budget beyond\n // a single slow action. Storybook-backed suites also pay an on-demand story compile in the first\n // test's `beforeEach`, which alone exceeded 30s. Individual configs may still raise this.\n timeout: 60_000,\n // Run tests in files in parallel.\n fullyParallel: true,\n // Fail the build on CI if you accidentally left test.only in the source code.\n forbidOnly: !!process.env.CI,\n // No retries anywhere: retrying hides flakes behind a 3x time cost, making shard timings unusable\n // for sizing the suite. A flake now fails loudly and gets skipped with a TODO instead.\n retries: 0,\n // 4 workers starved shared setup into false \"not stable\"/\"detached\" failures, so 2 is the\n // compromise. `|| 2`, not `??`: an env var set to the empty string (common in Actions) would\n // otherwise coerce to 0 workers.\n workers: Number(process.env.PLAYWRIGHT_WORKERS) || 2,\n // Reporter to use. See https://playwright.dev/docs/test-reporters.\n reporter: [\n ...(process.env.CI\n ? ([\n ['list'],\n [\n 'json',\n {\n outputFile: reporterOutputFile,\n },\n ],\n ['junit', { outputFile: reporterOutputFile.replace(/\\.json$/, '.xml') }],\n ] satisfies ReporterDescription[])\n : ([['list']] satisfies ReporterDescription[])),\n ],\n use: {\n trace: 'retain-on-failure',\n // Playwright's default is no limit, so a stuck locator would absorb the whole per-test budget and\n // report a bare `Test timeout` naming nothing.\n actionTimeout: 30_000,\n },\n projects,\n };\n};\n\n/** `process.env` without unset keys, the shape Playwright's launch `env` takes. */\nconst definedEnv = (): Record<string, string> => {\n const env: Record<string, string> = {};\n for (const [key, value] of Object.entries(process.env)) {\n if (value !== undefined) {\n env[key] = value;\n }\n }\n return env;\n};\n\n/**\n * Environment for a WebKit launch. Replaces the browser's whole environment rather than extending it,\n * so it carries `process.env` alongside the two crash workarounds.\n */\nconst webKitLaunchEnv = (workspaceRoot: string): Record<string, string> => {\n // x86_64 Linux WebKit: every captured boot crash faulted in the wasm in-place interpreter (IPInt) with a corrupted\n // locals base or wasm PC.\n const disableWasmIPInt = webkitWorkarounds() && process.platform === 'linux' && process.arch === 'x64';\n const errnoShim =\n webkitWorkarounds() && errnoShimSupported()\n ? buildErrnoShim(join(workspaceRoot, 'node_modules/.cache/dxos-test-utils'))\n : undefined;\n return {\n ...(disableWasmIPInt ? { JSC_useWasmIPInt: 'false' } : {}),\n ...definedEnv(),\n ...(errnoShim ? { LD_PRELOAD: [errnoShim, process.env.LD_PRELOAD].filter(Boolean).join(':') } : {}),\n };\n};\n\n/**\n * WebKit keeps OPFS under the browser's data directory, and an ephemeral context has none, so every\n * `navigator.storage` call there fails and no app backed by OPFS boots. A persistent context has one.\n * Its temporary directory is per context, which keeps the isolation a fresh context gives.\n */\nconst createContext = async (browser: Browser): Promise<{ context: BrowserContext; page?: Page }> => {\n const browserType = browser.browserType();\n if (browserType.name() !== 'webkit' || !webkitWorkarounds()) {\n return { context: await browser.newContext() };\n }\n\n const userDataDir = mkdtempSync(join(tmpdir(), 'dxos-webkit-'));\n const context = await browserType.launchPersistentContext(userDataDir, {\n env: webKitLaunchEnv(findWorkspaceRoot(process.cwd())),\n });\n context.once('close', () => rmSync(userDataDir, { recursive: true, force: true }));\n // A persistent context opens with a page; taking it leaves no blank page behind.\n return { context, page: context.pages()[0] };\n};\n\nexport type SetupOptions = {\n url?: string;\n bridgeLogs?: boolean;\n viewportSize?: Parameters<Page['setViewportSize']>[0];\n};\n\n/**\n * Opens a page, creating a context for it unless handed one to borrow.\n * `close()` disposes whatever this created: the context when it made one, otherwise just the page.\n */\nexport const setupPage = async (browser: Browser | BrowserContext, options: SetupOptions = {}) => {\n const { url, bridgeLogs, viewportSize } = options;\n\n const created = 'newContext' in browser ? await createContext(browser) : undefined;\n const context = created?.context ?? (browser as BrowserContext);\n const ownsContext = created !== undefined;\n let page: Page | undefined;\n\n // Playwright opens a trace chunk on every live context at test start, so a context left behind by a\n // closed page is re-serialized into every later trace in that worker.\n const close = async (): Promise<void> => {\n await (ownsContext ? context.close() : page?.close());\n };\n\n try {\n page = created?.page ?? (await context.newPage());\n\n if (viewportSize) {\n await page.setViewportSize(viewportSize);\n }\n\n // TODO(wittjosiah): Remove?\n if (bridgeLogs) {\n const lock = new Lock();\n\n page.on('pageerror', async (error) => {\n await lock.executeSynchronized(async () => {\n // eslint-disable-next-line no-console\n console.log(error);\n });\n });\n\n page.on('console', async (msg) => {\n try {\n const argsPromise = Promise.all(msg.args().map((x) => x.jsonValue()));\n await lock.executeSynchronized(async () => {\n const args = await argsPromise;\n\n if (args.length > 0) {\n console.log(...args);\n } else {\n console.log(msg);\n }\n });\n } catch (err) {\n console.error('Failed to parse message', err);\n }\n });\n }\n\n if (url) {\n await page.goto(url);\n }\n\n return { context, page, close };\n } catch (err) {\n // The caller never received `close`, so this is the only chance to dispose what got created. The\n // setup error is the one worth reporting, so a failure to clean up does not displace it.\n await close().catch(() => {});\n throw err;\n }\n};\n\nexport const storybookUrl = (storyId: string, port = 9009) =>\n `http://localhost:${port}/iframe.html?id=${storyId}&viewMode=story`;\n\n/**\n * Playwright `webServer` for a Storybook-backed suite. Readiness is probed by `url` rather than\n * `port` because a `port` probe is a bare TCP check, which `storybook dev` satisfies by binding the\n * socket before it can serve — tests starting in that gap get ERR_CONNECTION_REFUSED.\n */\nexport const storybookWebServer = (port: number) => ({\n command: `pnpm storybook dev --ci --quiet --port=${port} --config-dir=.storybook`,\n url: `http://localhost:${port}`,\n reuseExistingServer: false,\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives one long-lived `claude --print` process in stream-json mode: prompts go in on stdin as
|
|
3
|
+
* JSONL user messages and every event comes back on stdout as JSONL, so a test can send a turn,
|
|
4
|
+
* wait for it to finish, assert, and send the next one down the same conversation.
|
|
5
|
+
*
|
|
6
|
+
* A turn per process would work too, but each restart re-launches the MCP servers, and a server
|
|
7
|
+
* still holding the data directory while the next one boots is a race the test cannot see.
|
|
8
|
+
*/
|
|
9
|
+
export type ClaudeAgentOptions = {
|
|
10
|
+
/** Working directory for the agent; also the only directory its file tools may touch. */
|
|
11
|
+
cwd: string;
|
|
12
|
+
/** MCP servers, in the shape of an `.mcp.json` `mcpServers` map. Passed with --strict-mcp-config. */
|
|
13
|
+
mcpServers: Record<string, unknown>;
|
|
14
|
+
/** Anthropic credential. The caller reads it from DX_ANTHROPIC_API_KEY. */
|
|
15
|
+
apiKey: string;
|
|
16
|
+
/** Model alias or full name; the suite pins Sonnet. */
|
|
17
|
+
model: string;
|
|
18
|
+
/** Tools the agent may use without a prompt. Anything else is denied outright. */
|
|
19
|
+
allowedTools: string[];
|
|
20
|
+
/** Ceiling for one turn, in ms. */
|
|
21
|
+
timeout?: number;
|
|
22
|
+
};
|
|
23
|
+
/** One content block of an `assistant` message, as `claude --print --output-format stream-json` emits it. */
|
|
24
|
+
export type ContentBlock = {
|
|
25
|
+
type: 'text';
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'thinking';
|
|
29
|
+
thinking: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'tool_use';
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
input: unknown;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'tool_result';
|
|
37
|
+
tool_use_id: string;
|
|
38
|
+
content: unknown;
|
|
39
|
+
is_error?: boolean;
|
|
40
|
+
};
|
|
41
|
+
/** Token counts on an `assistant` event's message, as the Anthropic Messages API reports them. */
|
|
42
|
+
export type MessageUsage = {
|
|
43
|
+
input_tokens?: number;
|
|
44
|
+
output_tokens?: number;
|
|
45
|
+
cache_read_input_tokens?: number;
|
|
46
|
+
cache_creation_input_tokens?: number;
|
|
47
|
+
};
|
|
48
|
+
/** One line of `claude --print --output-format stream-json` output. */
|
|
49
|
+
export type StreamEvent = {
|
|
50
|
+
type: 'system';
|
|
51
|
+
subtype?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'user';
|
|
55
|
+
message: {
|
|
56
|
+
role: 'user';
|
|
57
|
+
content: ContentBlock[];
|
|
58
|
+
};
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
} | {
|
|
61
|
+
type: 'assistant';
|
|
62
|
+
message: {
|
|
63
|
+
role: 'assistant';
|
|
64
|
+
content: ContentBlock[];
|
|
65
|
+
model?: string;
|
|
66
|
+
usage?: MessageUsage;
|
|
67
|
+
};
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
} | {
|
|
70
|
+
type: 'result';
|
|
71
|
+
result?: string;
|
|
72
|
+
is_error?: boolean;
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
};
|
|
75
|
+
/** One assistant turn, reduced to what an assertion needs. */
|
|
76
|
+
export type Turn = {
|
|
77
|
+
/** The `result` event's text, or undefined when the turn produced none. */
|
|
78
|
+
result?: string;
|
|
79
|
+
/** True when the CLI reported the turn as an error (including a refusal or a turn limit). */
|
|
80
|
+
isError: boolean;
|
|
81
|
+
/** Names of every MCP tool the agent called this turn, in order. */
|
|
82
|
+
toolCalls: string[];
|
|
83
|
+
/** Every event of the turn, for diagnosing a failure without re-running the model. */
|
|
84
|
+
events: StreamEvent[];
|
|
85
|
+
/** Epoch milliseconds of the send and of the `result` event. */
|
|
86
|
+
start: number;
|
|
87
|
+
end: number;
|
|
88
|
+
};
|
|
89
|
+
export declare class ClaudeAgent {
|
|
90
|
+
#private;
|
|
91
|
+
private constructor();
|
|
92
|
+
/**
|
|
93
|
+
* Spawns the agent and returns immediately; the process is live from here until {@link close}.
|
|
94
|
+
*
|
|
95
|
+
* Nothing is sent yet — the CLI sits waiting on stdin — so a caller can scaffold whatever the
|
|
96
|
+
* first turn will act on before spending a token on it.
|
|
97
|
+
*/
|
|
98
|
+
static start({ cwd, mcpServers, apiKey, model, allowedTools, timeout, }: ClaudeAgentOptions): ClaudeAgent;
|
|
99
|
+
/**
|
|
100
|
+
* Sends one user message and resolves when that turn's `result` event arrives.
|
|
101
|
+
*
|
|
102
|
+
* One turn at a time: the conversation is a single stream, so a second call while a turn is in
|
|
103
|
+
* flight would take over the first one's callbacks and hand it the other's result. A turn that
|
|
104
|
+
* times out ends the agent rather than only rejecting, because the CLI is still working and its
|
|
105
|
+
* late `result` would otherwise land on whatever turn came next.
|
|
106
|
+
*/
|
|
107
|
+
send(prompt: string): Promise<Turn>;
|
|
108
|
+
/**
|
|
109
|
+
* Ends the agent and waits for the process to go.
|
|
110
|
+
*
|
|
111
|
+
* The whole process group, because `claude` spawns its stdio MCP servers as children: killing
|
|
112
|
+
* only the parent orphans them against whatever data directory the caller is about to delete.
|
|
113
|
+
*/
|
|
114
|
+
close(): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=claude-agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-agent.d.ts","sourceRoot":"","sources":["../../../src/claude-agent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared object preloaded into Linux WebKit that restores errno around every signal handler installed with `sigaction`:
|
|
3
|
+
* WebKit's thread-suspend handler returns from `sigsuspend` with errno set to EINTR on the thread it interrupted, which
|
|
4
|
+
* fails an errno-checked parse in progress there (ICE candidates, SDP ports, GLib `getauxval`).
|
|
5
|
+
* Freestanding, for glibc on x86_64 and aarch64, whose `struct sigaction` share this layout.
|
|
6
|
+
*
|
|
7
|
+
* Delete this file once Playwright ships a WebKit that restores errno itself — see `webkit-workarounds.md`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ERRNO_SHIM_SOURCE = "#define SA_SIGINFO_FLAG 4\n#define SIGNAL_COUNT 65\n#define RTLD_NEXT_HANDLE ((void *)-1L)\n\nstruct glibc_sigaction {\n void *handler;\n unsigned long mask[16];\n int flags;\n void (*restorer)(void);\n};\n\ntypedef int (*sigaction_fn)(int, const struct glibc_sigaction *, struct glibc_sigaction *);\n\nextern void *dlsym(void *handle, const char *name);\nextern int *__errno_location(void);\n\nstatic sigaction_fn next_sigaction;\n\n/* The handler the process installed for each signal, and whether it asked for siginfo. */\nstatic void *volatile handlers[SIGNAL_COUNT];\nstatic volatile int takes_siginfo[SIGNAL_COUNT];\n\n/* Resolved at load, so a signal handler never has to call dlsym; a constructor that ran earlier resolves it lazily. */\n__attribute__((constructor)) static void resolve_next_sigaction(void) {\n if (!next_sigaction) {\n next_sigaction = (sigaction_fn)dlsym(RTLD_NEXT_HANDLE, \"sigaction\");\n }\n}\n\n/* Passes all three arguments, which a one-argument handler ignores on x86_64 and aarch64. */\nstatic void trampoline(int signal, void *info, void *context) {\n int *error = __errno_location();\n int saved = *error;\n ((void (*)(int, void *, void *))handlers[signal])(signal, info, context);\n *error = saved;\n}\n\n/* Reports the process's own handler where the kernel holds the trampoline. */\nstatic void unwrap(struct glibc_sigaction *action, void *handler, int siginfo) {\n if (action && action->handler == (void *)trampoline) {\n action->handler = handler;\n action->flags = (action->flags & ~SA_SIGINFO_FLAG) | siginfo;\n }\n}\n\nint sigaction(int signal, const struct glibc_sigaction *action, struct glibc_sigaction *previous) {\n resolve_next_sigaction();\n if (signal <= 0 || signal >= SIGNAL_COUNT) {\n return next_sigaction(signal, action, previous);\n }\n\n void *old_handler = handlers[signal];\n int old_siginfo = takes_siginfo[signal];\n /* A trampoline read back through glibc's internal sigaction, as signal() returns it, keeps the recorded handler. */\n if (action && action->handler == (void *)trampoline) {\n struct glibc_sigaction restored = *action;\n restored.flags |= SA_SIGINFO_FLAG;\n int result = next_sigaction(signal, &restored, previous);\n if (result == 0) {\n unwrap(previous, old_handler, old_siginfo);\n }\n return result;\n }\n if (!action || action->handler == (void *)0 || action->handler == (void *)1) {\n int result = next_sigaction(signal, action, previous);\n if (result == 0) {\n unwrap(previous, old_handler, old_siginfo);\n }\n return result;\n }\n\n handlers[signal] = action->handler;\n takes_siginfo[signal] = action->flags & SA_SIGINFO_FLAG;\n struct glibc_sigaction wrapped = *action;\n wrapped.handler = (void *)trampoline;\n wrapped.flags |= SA_SIGINFO_FLAG;\n int result = next_sigaction(signal, &wrapped, previous);\n if (result != 0) {\n handlers[signal] = old_handler;\n takes_siginfo[signal] = old_siginfo;\n return result;\n }\n unwrap(previous, old_handler, old_siginfo);\n return result;\n}\n";
|
|
10
|
+
/** Whether {@link ERRNO_SHIM_SOURCE} fits this process's platform and architecture. */
|
|
11
|
+
export declare const errnoShimSupported: () => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Compiles {@link ERRNO_SHIM_SOURCE} into `cacheDir` once per source, compiler invocation and architecture,
|
|
14
|
+
* and returns its path.
|
|
15
|
+
*/
|
|
16
|
+
export declare const buildErrnoShim: (cacheDir: string) => string;
|
|
17
|
+
export declare const compileSharedObject: (source: string, output: string) => void;
|
|
18
|
+
//# sourceMappingURL=errno-shim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errno-shim.d.ts","sourceRoot":"","sources":["../../../src/errno-shim.ts"],"names":[],"mappings":"AASA;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,ogGAqF7B,CAAC;AAEF,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,QAAO,OACgD,CAAC;AAEvF;;;GAGG;AACH,eAAO,MAAM,cAAc,aAAc,MAAM,KAAG,MAqBjD,CAAC;AAOF,eAAO,MAAM,mBAAmB,WAAY,MAAM,UAAU,MAAM,KAAG,IAKpE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errno-shim.test.d.ts","sourceRoot":"","sources":["../../../src/errno-shim.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './resource';
|
|
1
|
+
export * from './resource.ts';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,eAAe,CAAC"}
|
|
@@ -5,9 +5,14 @@ export type SetupOptions = {
|
|
|
5
5
|
bridgeLogs?: boolean;
|
|
6
6
|
viewportSize?: Parameters<Page['setViewportSize']>[0];
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Opens a page, creating a context for it unless handed one to borrow.
|
|
10
|
+
* `close()` disposes whatever this created: the context when it made one, otherwise just the page.
|
|
11
|
+
*/
|
|
8
12
|
export declare const setupPage: (browser: Browser | BrowserContext, options?: SetupOptions) => Promise<{
|
|
9
13
|
context: BrowserContext;
|
|
10
14
|
page: Page;
|
|
15
|
+
close: () => Promise<void>;
|
|
11
16
|
}>;
|
|
12
17
|
export declare const storybookUrl: (storyId: string, port?: number) => string;
|
|
13
18
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playwright.d.ts","sourceRoot":"","sources":["../../../src/playwright.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,KAAK,IAAI,EACT,KAAK,oBAAoB,EAG1B,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"playwright.d.ts","sourceRoot":"","sources":["../../../src/playwright.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,KAAK,IAAI,EACT,KAAK,oBAAoB,EAG1B,MAAM,kBAAkB,CAAC;AA0C1B,eAAO,MAAM,SAAS,YAAa,MAAM,KAAG,oBAgG3C,CAAC;AAoDF,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACvD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,SAAS,YAAmB,OAAO,GAAG,cAAc,YAAW,YAAY;;;iBAU9D,OAAO,CAAC,IAAI,CAAC;EAmDtC,CAAC;AAEF,eAAO,MAAM,YAAY,YAAa,MAAM,0BACyB,CAAC;AAEtE;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,SAAU,MAAM;IAC7C,OAAO;IACP,GAAG;IACH,mBAAmB;CACnB,CAAC"}
|