()
| 190 | }); |
| 191 | |
| 192 | const main = async () => { |
| 193 | if (!existsSync(appExe)) fail(`app binary not found at ${appExe}`); |
| 194 | if (!existsSync(executorBin)) fail(`bundled executor not found at ${executorBin}`); |
| 195 | |
| 196 | const home = mkdtempSync(join(tmpdir(), "executor-repro-")); |
| 197 | const dataDir = join(home, ".executor"); |
| 198 | let app, foreign; |
| 199 | |
| 200 | try { |
| 201 | // 1. Boot: no daemon present, so the app spawns its OWN managed sidecar. |
| 202 | log("launching app (cold) — expect it to spawn its own sidecar"); |
| 203 | app = await launchApp(home); |
| 204 | await waitFor("console (Settings)", () => bodyHas(app.cdp, "Settings"), 120_000); |
| 205 | // Packaged mode spawns the bundled `executor daemon run` as its managed |
| 206 | // sidecar, so the manifest is kind "cli-daemon" — the app's own daemon is |
| 207 | // distinguished from a foreign one by owner.client ("desktop" vs "cli"). |
| 208 | const own = readManifest(dataDir); |
| 209 | if (!own || own.owner?.client !== "desktop") |
| 210 | fail( |
| 211 | `expected the app's own managed daemon (owner.client=desktop) after cold boot, got ${JSON.stringify(own)}`, |
| 212 | ); |
| 213 | log(`app booted on its own managed sidecar (pid ${own.pid}, owner ${own.owner.client})`); |
| 214 | |
| 215 | // 2. The app's sidecar dies out from under it -> crash screen. |
| 216 | log(`killing the app's own sidecar (pid ${own.pid})`); |
| 217 | process.kill(own.pid, "SIGKILL"); |
| 218 | await waitFor("crash screen", () => bodyHas(app.cdp, "stopped unexpectedly"), 30_000); |
| 219 | log("crash screen shown"); |
| 220 | |
| 221 | // 3. A CLI daemon takes over ownership of the data dir. |
| 222 | const port = await freePort(); |
| 223 | log(`starting a foreign CLI daemon on :${port} (takes over ${dataDir})`); |
| 224 | foreign = await startForeignDaemon(home, dataDir, port); |
| 225 | if (!foreign.ready) fail(`foreign daemon never became ready:\n${foreign.err}`); |
| 226 | const foreignManifest = readManifest(dataDir); |
| 227 | if ( |
| 228 | !foreignManifest || |
| 229 | foreignManifest.owner?.client !== "cli" || |
| 230 | foreignManifest.pid === own.pid |
| 231 | ) |
| 232 | fail( |
| 233 | `expected a foreign cli daemon (owner.client=cli, new pid), got ${JSON.stringify(foreignManifest)}`, |
| 234 | ); |
| 235 | const foreignPid = foreignManifest.pid; |
| 236 | log( |
| 237 | `foreign cli daemon owns the scope (pid ${foreignPid}, owner ${foreignManifest.owner.client})`, |
| 238 | ); |
| 239 | |
| 240 | // 4. User hits "Restart server". THIS is the wedge: pre-fix it re-spawns and |
| 241 | // dies on the scope lock; post-fix it adopts the running cli-daemon. |
| 242 | log('clicking "Restart server" on the crash screen'); |
| 243 | await app.cdp.eval(`document.querySelector("#restart")?.click(); true`); |
| 244 | |
| 245 | const recovered = await waitFor( |
| 246 | "console to come back after restart", |
| 247 | () => bodyHas(app.cdp, "Settings"), |
| 248 | 90_000, |
| 249 | ).then( |
no test coverage detected