fast-browser-with-jev
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { test } from "node:test";
|
||||
import { createBrowserTransport, type ChildProcessLike, type SpawnLike } from "./bridge.ts";
|
||||
import { BrowserOperationError } from "./contracts.ts";
|
||||
import type { BrowserOperation } from "./contracts.ts";
|
||||
|
||||
class TransportWorker extends EventEmitter implements ChildProcessLike {
|
||||
readonly requests: Record<string, unknown>[] = [];
|
||||
readonly stdin = {
|
||||
write: (line: string): boolean => {
|
||||
const request = JSON.parse(line) as Record<string, unknown>;
|
||||
this.requests.push(request);
|
||||
queueMicrotask(() => this.respond(request));
|
||||
return true;
|
||||
},
|
||||
end: (): void => undefined,
|
||||
once: (event: string, listener: (...args: any[]) => void): unknown => this.once(event, listener),
|
||||
};
|
||||
readonly stdout = new EventEmitter() as EventEmitter & { setEncoding?: (encoding: string) => void };
|
||||
readonly stderr = new EventEmitter();
|
||||
killed = false;
|
||||
silent = false;
|
||||
respond(request: Record<string, unknown>): void {
|
||||
if (this.silent) return;
|
||||
const operation = (request.operation as Record<string, unknown> | undefined)?.op ?? request.op;
|
||||
const tab = { tabId: "tab-1", kind: "page", selected: true, owned: true, url: "http://fixture.test/form", origin: "http://fixture.test", title: "Fixture" };
|
||||
let result: unknown;
|
||||
if (operation === "observe") result = { observationId: "obs-1", url: tab.url, title: tab.title, candidates: [] };
|
||||
else if (operation === "read") result = { tab, documentId: "doc-1", url: tab.url, title: tab.title, visibleText: "Fixture complete", visibleTextTruncated: false };
|
||||
else if (operation === "listTabs") result = [tab];
|
||||
else if (operation === "narrowOriginPolicy") result = (request.operation as Record<string, unknown>).request;
|
||||
else if (operation === "captureEvidence") result = { status: "complete", console: [{ level: "error", message: "fixture failed", count: 1, truncated: false }], failedRequests: [] };
|
||||
else if (operation === "close") result = { closed: true };
|
||||
else result = { status: "committed", mutationState: "verified", tab, documentId: "doc-2" };
|
||||
this.stdout.emit("data", JSON.stringify({ id: request.id, ok: true, result: { op: operation, result } }) + "\n");
|
||||
}
|
||||
kill(): boolean {
|
||||
this.killed = true;
|
||||
this.emit("close", null, "SIGTERM");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function spawnFor(worker: TransportWorker): SpawnLike {
|
||||
return (_command, _args, _options) => worker;
|
||||
}
|
||||
|
||||
const base = { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: ["http://fixture.test"] as const };
|
||||
const signal = () => new AbortController().signal;
|
||||
|
||||
test("typed transport reuses one worker and carries scoped context without broadening", async () => {
|
||||
const worker = new TransportWorker();
|
||||
const transport = createBrowserTransport({ ...base, spawn: spawnFor(worker) });
|
||||
try {
|
||||
const narrowed = await transport.dispatch({ operation: { op: "narrowOriginPolicy", request: { allowedOrigins: ["http://fixture.test"] } } }, signal());
|
||||
assert.deepEqual(narrowed, { op: "narrowOriginPolicy", result: { allowedOrigins: ["http://fixture.test"] } });
|
||||
const read = await transport.dispatch({ operation: { op: "read", request: {} }, context: narrowed.result }, signal());
|
||||
assert.equal(read.op, "read");
|
||||
assert.equal(read.result.visibleText, "Fixture complete");
|
||||
const tabs = await transport.dispatch({ operation: { op: "listTabs" } }, signal());
|
||||
assert.equal(tabs.result[0]?.tabId, "tab-1");
|
||||
assert.equal(worker.requests.length, 3);
|
||||
assert.deepEqual(worker.requests[0]?.operation, { op: "narrowOriginPolicy", request: { allowedOrigins: ["http://fixture.test"] } });
|
||||
assert.deepEqual(worker.requests[1]?.operation, { op: "read", request: {} });
|
||||
assert.deepEqual(worker.requests[1]?.context, { allowedOrigins: ["http://fixture.test"] });
|
||||
await assert.rejects(
|
||||
transport.dispatch({ operation: { op: "read", request: {} }, context: { allowedOrigins: "*" } }, signal()),
|
||||
(error: unknown) => error instanceof BrowserOperationError && error.details.code === "invalid_request" && error.details.mutationState === "not_attempted",
|
||||
);
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("typed transport rejects malformed and oversized operation results safely", async () => {
|
||||
const worker = new TransportWorker();
|
||||
worker.respond = (request) => {
|
||||
if ((request.operation as Record<string, unknown> | undefined)?.op === "read") {
|
||||
worker.stdout.emit("data", JSON.stringify({ id: request.id, ok: true, result: { op: "read", result: { tab: {}, documentId: "d", url: "http://fixture.test/form", title: "x", visibleText: "x", visibleTextTruncated: false } } }) + "\n");
|
||||
return;
|
||||
}
|
||||
worker.stdout.emit("data", `${JSON.stringify({ id: request.id, ok: true, result: { closed: true } })}\n`);
|
||||
};
|
||||
const transport = createBrowserTransport({ ...base, spawn: spawnFor(worker) });
|
||||
try {
|
||||
await assert.rejects(transport.dispatch({ operation: { op: "read", request: {} } }, signal()), (error: unknown) => {
|
||||
return error instanceof BrowserOperationError && error.details.code === "worker_failure";
|
||||
});
|
||||
assert.equal(worker.killed, true);
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
|
||||
const oversized = new TransportWorker();
|
||||
oversized.respond = (request) => {
|
||||
oversized.stdout.emit("data", `${"x".repeat(128 * 1024 + 1)}\n`);
|
||||
};
|
||||
const oversizedTransport = createBrowserTransport({ ...base, spawn: spawnFor(oversized) });
|
||||
try {
|
||||
await assert.rejects(oversizedTransport.dispatch({ operation: { op: "observe" } }, signal()), (error: unknown) => error instanceof BrowserOperationError && error.details.code === "worker_failure");
|
||||
assert.equal(oversized.killed, true);
|
||||
} finally {
|
||||
await oversizedTransport.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("transport preserves validated structured worker errors without raw exceptions", async () => {
|
||||
const worker = new TransportWorker();
|
||||
let failed = true;
|
||||
worker.respond = (request) => {
|
||||
if (failed) {
|
||||
failed = false;
|
||||
worker.stdout.emit("data", JSON.stringify({ id: request.id, ok: false, error: "safe worker message", details: {
|
||||
code: "stale", operation: "act", phase: "verify", message: "browser target is stale", mutationState: "not_attempted", retry: "never",
|
||||
} }) + "\n");
|
||||
return;
|
||||
}
|
||||
worker.stdout.emit("data", JSON.stringify({ id: request.id, ok: true, result: { op: "observe", result: { observationId: "obs-2", url: "http://fixture.test/form", title: "Fixture", candidates: [] } } }) + "\n");
|
||||
};
|
||||
const transport = createBrowserTransport({ ...base, spawn: spawnFor(worker) });
|
||||
try {
|
||||
await assert.rejects(transport.dispatch({ operation: { op: "act", request: { observationId: "obs", candidateId: "c", action: "fill", text: "x" } } }, signal()), (error: unknown) => {
|
||||
return error instanceof BrowserOperationError && error.details.code === "stale" && error.details.mutationState === "not_attempted" && error.details.message === "browser target is stale";
|
||||
});
|
||||
assert.equal(worker.killed, false);
|
||||
const observation = await transport.dispatch({ operation: { op: "observe" } }, signal());
|
||||
assert.equal(observation.result.observationId, "obs-2");
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("transport cancellation tears down the persistent worker without retry", async () => {
|
||||
const worker = new TransportWorker();
|
||||
worker.silent = true;
|
||||
const transport = createBrowserTransport({ ...base, spawn: spawnFor(worker), requestTimeoutMs: 500 });
|
||||
const controller = new AbortController();
|
||||
const operation: BrowserOperation = { op: "navigate", request: { url: "http://fixture.test/form" } };
|
||||
const pending = transport.dispatch({ operation }, controller.signal);
|
||||
await Promise.resolve();
|
||||
controller.abort();
|
||||
await assert.rejects(pending, (error: unknown) => error instanceof BrowserOperationError && error.details.code === "aborted" && error.details.mutationState === "unknown");
|
||||
assert.equal(worker.requests.length, 1);
|
||||
assert.equal(worker.killed, true);
|
||||
await transport.close();
|
||||
});
|
||||
|
||||
test("queued cancellation is safe and never writes a second request", async () => {
|
||||
const worker = new TransportWorker();
|
||||
worker.silent = true;
|
||||
const transport = createBrowserTransport({ ...base, spawn: spawnFor(worker), requestTimeoutMs: 500 });
|
||||
const first = transport.dispatch({ operation: { op: "observe" } }, new AbortController().signal);
|
||||
await Promise.resolve();
|
||||
const queuedController = new AbortController();
|
||||
const queued = transport.dispatch({ operation: { op: "navigate", request: { url: "http://fixture.test/form" } } }, queuedController.signal);
|
||||
queuedController.abort();
|
||||
worker.silent = false;
|
||||
worker.respond(worker.requests[0]!);
|
||||
await first;
|
||||
await assert.rejects(queued, (error: unknown) => error instanceof BrowserOperationError
|
||||
&& error.details.code === "aborted"
|
||||
&& error.details.mutationState === "not_attempted"
|
||||
&& error.details.retry === "never");
|
||||
assert.equal(worker.requests.length, 1);
|
||||
await transport.close();
|
||||
});
|
||||
|
||||
test("unknown runtime operations fail before any worker dispatch", async () => {
|
||||
const worker = new TransportWorker();
|
||||
const transport = createBrowserTransport({ ...base, spawn: spawnFor(worker) });
|
||||
try {
|
||||
await assert.rejects(transport.dispatch({ operation: { op: "notAnOperation" } as never }, signal()), (error: unknown) => error instanceof BrowserOperationError
|
||||
&& error.details.code === "invalid_request"
|
||||
&& error.details.mutationState === "not_attempted");
|
||||
assert.equal(worker.requests.length, 0);
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("parent cancellation tears down the worker even without a call signal abort", async () => {
|
||||
const worker = new TransportWorker();
|
||||
worker.silent = true;
|
||||
const parent = new AbortController();
|
||||
const transport = createBrowserTransport({ ...base, spawn: spawnFor(worker), requestTimeoutMs: 500 }, parent.signal);
|
||||
const pending = transport.dispatch({ operation: { op: "observe" } }, new AbortController().signal);
|
||||
await Promise.resolve();
|
||||
parent.abort();
|
||||
await assert.rejects(pending, (error: unknown) => error instanceof BrowserOperationError && error.details.code === "aborted");
|
||||
assert.equal(worker.killed, true);
|
||||
await transport.close();
|
||||
});
|
||||
Reference in New Issue
Block a user