stably 4.12.21 → 4.12.22-rc.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.
@@ -230,8 +230,17 @@ to restart the browser session.`);
230
230
  resolve();
231
231
  });
232
232
  child.on("close", (code) => {
233
- if (!signalled)
234
- reject(new Error(`Daemon process exited with code ${code}`));
233
+ if (!signalled) {
234
+ // Read the .err log when the daemon crashes before outputting ### Error
235
+ // (e.g., SyntaxError, missing module), the real error is only in this file.
236
+ let message = `Daemon process exited with code ${code}`;
237
+ try {
238
+ const errLogContent = import_fs.default.readFileSync(errLog, "utf-8").trim();
239
+ if (errLogContent)
240
+ message += "\n" + errLogContent;
241
+ } catch {}
242
+ reject(new Error(message));
243
+ }
235
244
  });
236
245
  });
237
246
  process.off("SIGINT", sigintHandler);
@@ -43,27 +43,9 @@ var import_socketConnection = require("../client/socketConnection");
43
43
  var import_commands = require("./commands");
44
44
  var import_command = require("./command");
45
45
  const daemonDebug = (0, import_utilsBundle.debug)("pw:daemon");
46
- async function socketExists(socketPath) {
47
- try {
48
- const stat = await import_promises.default.stat(socketPath);
49
- if (stat?.isSocket())
50
- return true;
51
- } catch (e) {
52
- }
53
- return false;
54
- }
55
46
  async function startMcpDaemonServer(config, contextFactory) {
56
47
  const sessionConfig = config.sessionConfig;
57
48
  const { socketPath, version } = sessionConfig;
58
- if (import_os.default.platform() !== "win32" && await socketExists(socketPath)) {
59
- daemonDebug(`Socket already exists, removing: ${socketPath}`);
60
- try {
61
- await import_promises.default.unlink(socketPath);
62
- } catch (error) {
63
- daemonDebug(`Failed to remove existing socket: ${error}`);
64
- throw error;
65
- }
66
- }
67
49
  const cwd = import_url.default.pathToFileURL(process.cwd()).href;
68
50
  const clientInfo = {
69
51
  name: "playwright-cli",
@@ -194,6 +176,9 @@ async function startMcpDaemonServer(config, contextFactory) {
194
176
  const shutdown = (exitCode) => {
195
177
  daemonDebug(`shutting down daemon with exit code ${exitCode}`);
196
178
  server.close();
179
+ if (import_os.default.platform() !== "win32") {
180
+ try { require("fs").unlinkSync(socketPath); } catch {}
181
+ }
197
182
  (0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
198
183
  };
199
184
  const server = import_net.default.createServer((socket) => {
@@ -230,9 +215,42 @@ async function startMcpDaemonServer(config, contextFactory) {
230
215
  };
231
216
  });
232
217
  return new Promise((resolve, reject) => {
218
+ let retried = false;
233
219
  server.on("error", (error) => {
234
- daemonDebug(`server error: ${error.message}`);
235
- reject(error);
220
+ if (error.code !== "EADDRINUSE" || retried) {
221
+ daemonDebug(`server error: ${error.message}`);
222
+ return reject(error);
223
+ }
224
+ daemonDebug("Socket already in use, checking if daemon is alive...");
225
+ const client = import_net.default.connect(socketPath);
226
+ client.setTimeout(5000);
227
+ client.on("timeout", () => {
228
+ client.destroy();
229
+ retried = true;
230
+ reject(new Error(
231
+ `Daemon at ${socketPath} is not responding (connect timed out). ` +
232
+ `Remove the socket manually or kill the process.`
233
+ ));
234
+ });
235
+ client.on("connect", () => {
236
+ client.destroy();
237
+ reject(new Error(
238
+ `Another daemon is already running on ${socketPath}. ` +
239
+ `Close it with 'stably-browser close' first.`
240
+ ));
241
+ });
242
+ client.on("error", () => {
243
+ if (retried) return;
244
+ daemonDebug("Stale socket detected, removing and retrying...");
245
+ if (import_os.default.platform() !== "win32") {
246
+ try { require("fs").unlinkSync(socketPath); } catch {}
247
+ }
248
+ retried = true;
249
+ server.listen(socketPath, () => {
250
+ daemonDebug(`daemon server listening on ${socketPath}`);
251
+ resolve(socketPath);
252
+ });
253
+ });
236
254
  });
237
255
  server.listen(socketPath, () => {
238
256
  daemonDebug(`daemon server listening on ${socketPath}`);
@@ -43,6 +43,20 @@ function decorateCLICommand(command, version) {
43
43
  options.chromiumSandbox = options.chromiumSandbox === true ? void 0 : false;
44
44
  (0, import_watchdog.setupExitWatchdog)();
45
45
  const config = await resolveCLIConfig(options.daemonSession);
46
+ // Docker's /dev/shm defaults to 64MB. Chrome uses /dev/shm for renderer shared
47
+ // memory (frame compositing, screenshot buffers). Complex pages can push Chrome
48
+ // past 64MB, silently crashing the renderer and killing the CDP connection.
49
+ // --disable-dev-shm-usage tells Chrome to use /tmp instead.
50
+ //
51
+ // Injected AFTER resolveCLIConfig() so it operates on the final resolved config,
52
+ // not the pipeline inputs — doesn't interfere with user-set PLAYWRIGHT_MCP_CONFIG
53
+ // or any other config source (defaults, env vars, --config= files, CLI flags).
54
+ // Harmless on non-Docker: both /dev/shm and /tmp are tmpfs.
55
+ // Harmless for CDP connections: CdpContextFactory ignores launchOptions.args.
56
+ if (!config.browser.launchOptions.args)
57
+ config.browser.launchOptions.args = [];
58
+ if (!config.browser.launchOptions.args.includes("--disable-dev-shm-usage"))
59
+ config.browser.launchOptions.args.push("--disable-dev-shm-usage");
46
60
  const browserContextFactory = (0, import_browserContextFactory.contextFactory)(config);
47
61
  const extensionContextFactory = new import_extensionContextFactory.ExtensionContextFactory(config.browser.launchOptions.channel || "chrome", config.browser.userDataDir, config.browser.launchOptions.executablePath);
48
62
  const cf = config.extension ? extensionContextFactory : browserContextFactory;
@@ -147,7 +147,17 @@ class CdpContextFactory extends BaseContextFactory {
147
147
  });
148
148
  }
149
149
  async _doCreateContext(browser) {
150
- return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
150
+ if (this.config.browser.isolated)
151
+ return await browser.newContext();
152
+ // Non-isolated: reuse the default context so pages survive across sessions.
153
+ // After a browser crash/disconnect, _browserPromise is cleared (see BaseContextFactory)
154
+ // and _obtainBrowser reconnects via connectOverCDP. The fresh connection should have
155
+ // a default context, but if contexts()[0] is undefined (e.g., browser was restarted
156
+ // without a default context), fall back to creating a new one so the daemon doesn't crash.
157
+ const context = browser.contexts()[0];
158
+ if (context)
159
+ return context;
160
+ return await browser.newContext();
151
161
  }
152
162
  }
153
163
  class RemoteContextFactory extends BaseContextFactory {
@@ -106,9 +106,12 @@ async function resolveCLIConfig(cliOptions) {
106
106
  }
107
107
  async function validateConfig(config) {
108
108
  if (config.browser.browserName === "chromium" && config.browser.launchOptions.chromiumSandbox === void 0) {
109
- if (process.platform === "linux")
110
- config.browser.launchOptions.chromiumSandbox = config.browser.launchOptions.channel !== "chromium";
111
- else
109
+ if (process.platform === "linux") {
110
+ // On Linux, enable sandbox for non-bundled channels (chrome, msedge, etc.) UNLESS
111
+ // running as root. Chrome refuses to sandbox when running as root (common in CI/Docker).
112
+ const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
113
+ config.browser.launchOptions.chromiumSandbox = isRoot ? false : config.browser.launchOptions.channel !== "chromium";
114
+ } else
112
115
  config.browser.launchOptions.chromiumSandbox = true;
113
116
  }
114
117
  if (config.saveVideo && !checkFfmpeg()) {
@@ -0,0 +1,11 @@
1
+ --- lib/mcp/browser/config.js
2
+ +++ lib/mcp/browser/config.js
3
+ @@ -305,7 +305,7 @@
4
+ function outputDir(config, clientInfo) {
5
+ if (config.outputDir)
6
+ return import_path.default.resolve(config.outputDir);
7
+ const rootPath = (0, import_server2.firstRootPath)(clientInfo);
8
+ if (rootPath)
9
+ - return import_path.default.resolve(rootPath, config.skillMode ? ".playwright-cli" : ".playwright-mcp");
10
+ + return import_path.default.resolve(rootPath, config.skillMode ? ".stably-browser" : ".playwright-mcp");
11
+ const tmpDir = process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir();
@@ -118,6 +118,40 @@ class SessionLog {
118
118
  lines.push("");
119
119
  this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
120
120
  }
121
+ logUserAction(action, tab, code, isUpdate) {
122
+ code = code.trim();
123
+ // Send recorder event for user action tracking
124
+ const actionForLog = { ...action };
125
+ delete actionForLog.ariaSnapshot;
126
+ delete actionForLog.selector;
127
+ actionForLog.isUpdate = !!isUpdate;
128
+ sendRecorderEvent({
129
+ type: "user-action",
130
+ action: actionForLog,
131
+ code,
132
+ url: tab.page.url(),
133
+ timestamp: Date.now()
134
+ });
135
+ // Also log to session file
136
+ const lines = [""];
137
+ lines.push(
138
+ `### User action: ${action.name}`,
139
+ "- Args",
140
+ "```json",
141
+ JSON.stringify(actionForLog, null, 2),
142
+ "```"
143
+ );
144
+ if (code) {
145
+ lines.push(
146
+ "- Code",
147
+ "```js",
148
+ code,
149
+ "```"
150
+ );
151
+ }
152
+ lines.push("");
153
+ this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
154
+ }
121
155
  }
122
156
  // Annotate the CommonJS export names for ESM import in node:
123
157
  0 && (module.exports = {
@@ -101,6 +101,12 @@ After all batches complete, provide:
101
101
  - List of any failed/skipped tests with reasons
102
102
  - Suggestions for resolving issues
103
103
 
104
+ ## Orchestrator Available
105
+
106
+ If `create-planner` and `create-worker` subagents are available (check your available subagent types), defer to the orchestrator for parallel execution instead of using this batch approach. The orchestrator groups tests by page and runs workers in parallel, which is faster than sequential batching. This skill applies only when the orchestrator subagents are not available.
107
+
108
+ Note: the batching guidance in this skill (browser-close between tests, batch sizes of ~6) still applies *within* each orchestrator worker — workers process their page group's tests sequentially with the same browser-reset pattern.
109
+
104
110
  ## Do NOT
105
111
 
106
112
  - Start creating tests without informing user of scope
@@ -231,6 +231,40 @@ stably-browser run-test --help
231
231
  - Overrides `browser`/`context`/`page` fixtures to connect via CDP (no new browser launched)
232
232
  - Preserves browser state after the run (pages remain open for inspection)
233
233
 
234
+ ## Test Creation Workflow
235
+
236
+ Use `stably-browser` as a live exploration tool during test creation, not just as a final verifier.
237
+
238
+ Preferred pattern when feasible:
239
+
240
+ 1. Open the page and explore it live.
241
+ ```bash
242
+ stably-browser open https://example.com/target
243
+ stably-browser click e15
244
+ stably-browser fill e22 "query"
245
+ ```
246
+
247
+ 2. Read the snapshot returned by each action.
248
+ - Treat the browser snapshot as ground truth for visible DOM structure, roles, labels, text, and control behavior.
249
+ - Use code/source reads for routes, feature flags, auth/setup, and hidden preconditions.
250
+
251
+ 3. Reuse emitted Playwright code.
252
+ - `stably-browser` actions emit concrete Playwright code/locator suggestions.
253
+ - Prefer copying that evidence into the test instead of inventing selectors from scratch.
254
+
255
+ 4. Run `stably-browser run-test` to verify.
256
+ - Use `run-test` after you understand the page, or earlier when it is needed to bootstrap project-aware auth/setup.
257
+ - After `run-test`, prefer inspecting the persisted browser/session before blindly rerunning from the error text alone.
258
+
259
+ 5. If auth/setup requires a project-aware bootstrap:
260
+ - Direct `open` may not reproduce Playwright project auth/setup on its own.
261
+ - In those repos, use `stably-browser run-test --project=<project>` or a seed test to establish the correct state, then inspect the persisted browser/session.
262
+
263
+ Anti-patterns:
264
+ - Guessing selectors from source code when the live browser already shows the actual DOM
265
+ - Treating `run-test` as the only discovery tool
266
+ - Blindly looping `run-test` → read error → edit without inspecting the browser state in between
267
+
234
268
  ### Running Multiple Tests — Sequential vs Parallel
235
269
 
236
270
  When verifying that multiple independent tests pass, each test needs its own clean browser state. Two approaches:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stably",
3
- "version": "4.12.21",
3
+ "version": "4.12.22-rc.0",
4
4
  "packageManager": "pnpm@10.24.0",
5
5
  "description": "AI-powered E2E Playwright testing CLI. Stably can understand your codebase, edit/run tests, and handle complex test scenarios for you.",
6
6
  "main": "dist/index.mjs",