#!/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 \[options\]/); assert.match(result.stdout, /reader /); 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 \[options\]/); assert.match(result.stdout, /--wait-js=/); assert.equal(result.stderr, ""); }); test("reader extracts page content as markdown", ["reader"], () => { const result = runCli([ "reader", dataHtml( 'Hello

Main

Some text

Link', ), "--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("Hello

Main

World

"), "--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("Hello

Old

"), "--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("Hello"), "--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("Hello"), `--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("

First

"), "--no-reader", ]); assert.equal(r1.status, 0, r1.stderr); assert.match(r1.stdout, /# First/); // Second Command const r2 = server.runWithSocket([ "reader", dataHtml("

Second

"), "--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("Hello"), "--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("Hello"), "--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("Hello

Main

"), "--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("Hello"), "--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("Hello"), "--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("Hello"), "--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("Hello

Main

"), "--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("Hello"), "--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("Hello"), "--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("Hello"), "--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); });