Files
glimpse/test/smoke.js
Evan Reichard e3d7c28820 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
2026-05-02 20:32:23 -04:00

502 lines
14 KiB
JavaScript
Executable File

#!/usr/bin/env node
// @ts-nocheck
import {
mkdtempSync,
rmSync,
existsSync,
mkdirSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync, spawn } from "node:child_process";
import assert from "node:assert/strict";
const cliPath = new URL("../src/index.ts", import.meta.url).pathname;
const tempDir = mkdtempSync(join(tmpdir(), "glimpse-smoke-"));
const filters = process.argv.slice(2).filter((arg) => arg !== "--list");
const shouldList = process.argv.includes("--list");
const tests = [];
function test(name, tags, fn) {
tests.push({ name, tags, fn });
}
function dataHtml(html) {
return `data:text/html,${html}`;
}
function runCli(args, options = {}) {
return spawnSync(process.execPath, ["--import", "tsx", cliPath, ...args], {
encoding: "utf-8",
env: options.env ?? process.env,
timeout: 30000,
});
}
function parseJson(text) {
try {
return JSON.parse(text);
} catch (err) {
throw new Error(`Failed to parse JSON: ${err.message}\n${text}`);
}
}
function expectSuccess(args, options = {}) {
const result = runCli(args, options);
assert.equal(result.status, 0, result.stderr || result.stdout);
return parseJson(result.stdout);
}
function expectFailure(args, options = {}) {
const result = runCli(args, options);
assert.notEqual(result.status, 0, result.stdout || result.stderr);
return parseJson(result.stderr);
}
function matchesFilters(entry) {
if (filters.length === 0) {
return true;
}
return filters.some((filter) => {
const normalized = filter.toLowerCase();
return (
entry.name.toLowerCase().includes(normalized) ||
entry.tags.includes(normalized)
);
});
}
test("no args prints help", ["help", "cli"], () => {
const result = runCli([]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /Usage: glimpse <command> <url> \[options\]/);
assert.match(result.stdout, /reader <url>/);
assert.equal(result.stderr, "");
});
test("help flag prints help", ["help", "cli"], () => {
const result = runCli(["--help"]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /Usage: glimpse <command> <url> \[options\]/);
assert.match(result.stdout, /--wait-js=<code>/);
assert.equal(result.stderr, "");
});
test("reader extracts page content as markdown", ["reader"], () => {
const result = runCli([
"reader",
dataHtml(
'<title>Hello</title><h1>Main</h1><p>Some text</p><a href="https://example.com">Link</a>',
),
"--no-reader",
]);
assert.equal(result.status, 0, result.stderr || result.stdout);
const output = result.stdout.trim();
assert.match(output, /# Main/);
assert.match(output, /Some text/);
assert.match(output, /\[Link\]\(https:\/\/example\.com\/??\)/);
});
test("reader returns json format with method field", ["reader"], () => {
const output = expectSuccess([
"reader",
dataHtml("<title>Hello</title><h1>Main</h1><p>World</p>"),
"--no-reader",
"--format=json",
]);
assert.equal(output.title, "Hello");
assert.equal(output.method, "raw");
assert.equal(typeof output.markdown, "string");
assert.match(output.markdown, /# Main/);
assert.match(output.text, /Main/);
});
test(
"reader runs top-level javascript before extraction",
["reader", "js"],
() => {
const result = runCli([
"reader",
dataHtml("<title>Hello</title><h1>Old</h1>"),
"--no-reader",
"--js=document.querySelector('h1').textContent = 'New'",
]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /# New/);
},
);
test("exec returns javascript result", ["exec", "js"], () => {
const result = runCli([
"exec",
dataHtml("<title>Hello</title>"),
"--js=return document.title",
]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(result.stdout.trim(), "Hello");
});
test(
"screenshot returns standard success envelope and writes file",
["screenshot"],
() => {
const outputPath = join(tempDir, "page.png");
const output = expectSuccess([
"screenshot",
dataHtml("<title>Hello</title>"),
`--output=${outputPath}`,
]);
assert.equal(output.ok, true);
assert.equal(output.result.path, outputPath);
assert.equal(typeof output.elapsedMs, "number");
assert.equal(existsSync(outputPath), true);
},
);
// 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;
const result = runCli(["search", "--config=/nonexistent/path", "example query"], { env });
const output = parseJson(result.stderr);
assert.notEqual(result.status, 0, result.stdout || result.stderr);
assert.equal(output.ok, false);
assert.equal(output.error.code, "KAGI_TOKEN_REQUIRED");
assert.match(output.error.message, /Kagi search requires/);
assert.match(output.error.message, /config token/);
assert.equal(typeof output.elapsedMs, "number");
});
test(
"invalid config returns structured error before browser startup",
["config", "errors"],
() => {
const configPath = join(tempDir, "bad-config.json");
writeFileSync(configPath, "not json");
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
`--config=${configPath}`,
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "CONFIG_READ_FAILED");
assert.match(output.error.message, /Failed to read config file/);
},
);
test(
"invalid config schema returns structured error",
["config", "errors"],
() => {
const configPath = join(tempDir, "bad-schema.json");
writeFileSync(configPath, JSON.stringify({ search: { provider: 42 } }));
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
`--config=${configPath}`,
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "INVALID_CONFIG");
assert.match(output.error.message, /search\.provider must be a string/);
},
);
test("empty home config is accepted", ["config"], () => {
const configHome = join(tempDir, "config-home");
const configDir = join(configHome, "glimpse");
mkdirSync(configDir, { recursive: true });
writeFileSync(join(configDir, "config.json"), "{}");
const result = runCli(
["reader", dataHtml("<title>Hello</title><h1>Main</h1>"), "--no-reader"],
{ env: { ...process.env, XDG_CONFIG_HOME: configHome } },
);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /# Main/);
});
test("unknown command returns structured error", ["errors", "cli"], () => {
const output = expectFailure(["nope", dataHtml("<title>Hello</title>"), "--no-reader"]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "UNKNOWN_COMMAND");
assert.match(output.error.message, /Unknown command: nope/);
assert.equal(typeof output.elapsedMs, "number");
});
test(
"invalid timeout returns invalid option before browser startup",
["errors", "timeout"],
() => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
"--timeout=abc",
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "INVALID_OPTION");
assert.match(output.error.message, /--timeout must be a positive number/);
assert.equal(typeof output.elapsedMs, "number");
},
);
test("invalid wait-until returns invalid option", ["errors", "wait"], () => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
"--wait-until=loaded",
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "INVALID_OPTION");
assert.match(output.error.message, /Unsupported --wait-until value: loaded/);
});
test("wait-js succeeds when condition is true", ["wait"], () => {
const result = runCli([
"reader",
dataHtml("<title>Hello</title><h1>Main</h1>"),
"--no-reader",
'--wait-js=return document.title === "Hello"',
]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /# Main/);
});
test("wait-js timeout returns wait timeout", ["wait", "errors"], () => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
"--wait-js=return false",
"--timeout=0.001",
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "WAIT_TIMEOUT");
assert.match(output.error.message, /waiting for --wait-js/);
assert.equal(typeof output.elapsedMs, "number");
assert.match(output.url, /^data:text\/html,/);
});
test(
"wait-js exception returns script failed",
["wait", "errors", "js"],
() => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
'--wait-js=throw new Error("boom")',
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "SCRIPT_FAILED");
assert.match(output.error.message, /--wait-js failed/);
assert.match(output.error.message, /boom/);
},
);
test(
"top-level javascript exception returns script failed",
["errors", "js"],
() => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
'--js=throw new Error("boom")',
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "SCRIPT_FAILED");
assert.match(output.error.message, /Prelude script failed/);
assert.match(output.error.message, /boom/);
},
);
function listTests() {
const tags = [...new Set(tests.flatMap((entry) => entry.tags))].sort();
console.log(`Tags: ${tags.join(", ")}`);
for (const entry of tests) {
console.log(`${entry.name} [${entry.tags.join(", ")}]`);
}
}
async function main() {
if (shouldList) {
listTests();
return;
}
const selectedTests = tests.filter(matchesFilters);
if (selectedTests.length === 0) {
console.error(`No tests matched: ${filters.join(", ")}`);
process.exit(1);
}
let failed = 0;
// Run Tests
for (const { name, fn } of selectedTests) {
try {
await fn();
console.log(`ok - ${name}`);
} catch (err) {
failed += 1;
console.error(`not ok - ${name}`);
console.error(err.stack || err.message);
}
}
// Clean Temporary Files
rmSync(tempDir, { recursive: true, force: true });
if (failed > 0) {
console.error(`${failed} test(s) failed`);
process.exit(1);
}
console.log(`${selectedTests.length} test(s) passed`);
}
main().catch((err) => {
rmSync(tempDir, { recursive: true, force: true });
console.error(err.stack || err.message);
process.exit(1);
});