feat: add persistent browser server with auto-discovery
Add `glimpse serve` which starts geckodriver + Firefox and proxies WebDriver requests through a Unix socket at $XDG_RUNTIME_DIR/glimpse-<uid>.sock. All commands auto-discover the socket and reuse the running browser session (~300ms vs ~2-3s per command). The proxy intercepts session create/delete to keep Firefox alive: new session requests return the existing session ID, delete session navigates to about:blank instead of closing Firefox. - `glimpse serve` starts in foreground, logs to stderr - `glimpse serve --stop` sends shutdown via socket - `glimpse serve --status` prints JSON status - SIGTERM/SIGINT do full cleanup (Firefox + geckodriver + socket) - Second instance detected and rejected with exit code 1 - GLIMPSE_SOCKET_PATH env var for test isolation - Three new smoke tests for serve lifecycle
This commit is contained in:
115
test/smoke.js
115
test/smoke.js
@@ -10,7 +10,7 @@ import {
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { spawnSync, spawn } from "node:child_process";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const cliPath = new URL("../src/index.ts", import.meta.url).pathname;
|
||||
@@ -163,6 +163,119 @@ test(
|
||||
},
|
||||
);
|
||||
|
||||
// Serve Helper - Starts a glimpse serve process with an isolated socket,
|
||||
// waits for the "server listening" message, and returns a handle with stop().
|
||||
function startServe(extraEnv = {}) {
|
||||
const sock = join(tempDir, `serve-${Date.now()}.sock`);
|
||||
const env = { ...process.env, GLIMPSE_SOCKET_PATH: sock, ...extraEnv };
|
||||
|
||||
const child = spawn(process.execPath, ["--import", "tsx", cliPath, "serve"], {
|
||||
encoding: "utf-8",
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
|
||||
|
||||
const ready = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error(`serve did not start: ${stderr}`)), 15000);
|
||||
child.stderr.on("data", () => {
|
||||
if (stderr.includes("server listening")) {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
child.on("exit", (code) => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`serve exited early (code ${code}): ${stderr}`));
|
||||
});
|
||||
});
|
||||
|
||||
function runWithSocket(args) {
|
||||
return spawnSync(process.execPath, ["--import", "tsx", cliPath, ...args], {
|
||||
encoding: "utf-8",
|
||||
env,
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
function stop() {
|
||||
runWithSocket(["serve", "--stop"]);
|
||||
return new Promise((resolve) => {
|
||||
child.on("exit", () => resolve());
|
||||
setTimeout(() => { child.kill(); resolve(); }, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
return { ready, stop, runWithSocket, sock, child };
|
||||
}
|
||||
|
||||
test(
|
||||
"serve starts and stops cleanly",
|
||||
["serve"],
|
||||
async () => {
|
||||
const server = startServe();
|
||||
await server.ready;
|
||||
|
||||
// Status Should Report Running
|
||||
const status = server.runWithSocket(["serve", "--status"]);
|
||||
assert.equal(status.status, 0, status.stderr);
|
||||
const info = parseJson(status.stdout);
|
||||
assert.equal(info.ok, true);
|
||||
assert.equal(typeof info.sessionId, "string");
|
||||
assert.equal(info.socket, server.sock);
|
||||
|
||||
await server.stop();
|
||||
assert.equal(existsSync(server.sock), false, "socket should be cleaned up");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"serve reuses browser session across commands",
|
||||
["serve"],
|
||||
async () => {
|
||||
const server = startServe();
|
||||
await server.ready;
|
||||
|
||||
// First Command
|
||||
const r1 = server.runWithSocket([
|
||||
"reader",
|
||||
dataHtml("<h1>First</h1>"),
|
||||
"--no-reader",
|
||||
]);
|
||||
assert.equal(r1.status, 0, r1.stderr);
|
||||
assert.match(r1.stdout, /# First/);
|
||||
|
||||
// Second Command
|
||||
const r2 = server.runWithSocket([
|
||||
"reader",
|
||||
dataHtml("<h1>Second</h1>"),
|
||||
"--no-reader",
|
||||
]);
|
||||
assert.equal(r2.status, 0, r2.stderr);
|
||||
assert.match(r2.stdout, /# Second/);
|
||||
|
||||
await server.stop();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"serve rejects second instance",
|
||||
["serve", "errors"],
|
||||
async () => {
|
||||
const server = startServe();
|
||||
await server.ready;
|
||||
|
||||
// Second Instance Should Fail
|
||||
const result = server.runWithSocket(["serve"]);
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /already running/);
|
||||
|
||||
await server.stop();
|
||||
},
|
||||
);
|
||||
|
||||
test("search validates kagi token in provider", ["search", "errors"], () => {
|
||||
const env = { ...process.env };
|
||||
delete env.KAGI_TOKEN;
|
||||
|
||||
Reference in New Issue
Block a user