opencode/packages/desktop/test/browser-errors.test.ts

81 lines
4.3 KiB
TypeScript

import { expect, test } from "bun:test"
import { rm } from "node:fs/promises"
import path from "node:path"
import { Browser } from "@opencode/plugin-browser/rpc"
import { browserFailure, protocolError } from "../src/main/browser/errors"
import { createBrowserFiles } from "../src/main/browser/files"
import { analyzeCpu, analyzeTrace, parseHeap } from "../src/main/browser/analysis"
const tabID = Browser.TabID.make(`tab_${crypto.randomUUID()}`)
test("navigation failures explain the server network and never recommend disabling TLS", () => {
const action: Browser.Action = { type: "navigate", tabID, url: "https://example.com" }
const refused = browserFailure(action, new Error("net::ERR_CONNECTION_REFUSED"))
expect(refused.code).toBe("navigation_failed")
expect(refused.message).toContain("localhost means that server")
expect(refused.message).toContain("hostname/port")
const tls = browserFailure(action, new Error("net::ERR_CERT_AUTHORITY_INVALID"))
expect(tls.message).toContain("do not bypass certificate checks")
const aborted = browserFailure(action, new Error("net::ERR_ABORTED"))
expect(aborted.message).toContain("browser.files.list({tabID})")
})
test("native protocol errors keep the cause and give a valid recovery operation", () => {
expect(protocolError("DOM.resolveNode", new Error("Could not find node with given id")).message).toContain(
"browser.snapshot({tabID})",
)
expect(protocolError("Runtime.evaluate", new Error("Cannot find context with specified id")).message).toContain(
"browser.frames({tabID})",
)
const unsupported = protocolError("Target.getBrowserContexts", new Error("Not allowed"))
expect(unsupported.message).toContain("does not support or allow")
expect(unsupported.message).toContain("do not retry unchanged")
expect(unsupported.cause).toBeInstanceOf(Error)
const failure = browserFailure({ type: "screenshot", tabID }, new Error("UnknownVizError"))
expect(failure.message).toContain("browser.tabs.focus({tabID})")
expect(failure.message).toContain("report the capture failure")
})
test("long page errors retain the operation context without classifying script text as a navigation error", () => {
const failure = browserFailure(
{ type: "evaluate", tabID, script: "throw Error()" },
new Error("ERR_CONNECTION_REFUSED " + "x".repeat(10_000)),
)
expect(failure.code).toBe("operation_failed")
expect(failure.message.startsWith("browser.evaluate failed.")).toBe(true)
expect(failure.message.length).toBeLessThanOrEqual(2_048)
})
test("files distinguish pending, failed, unknown and missing desktop copies", async () => {
const files = createBrowserFiles(() => ["https://example.com"])
await files.ready
try {
const pending = files.add("download.txt", "text/plain")
expect(() => files.get(pending.id)).toThrow("do not start a duplicate download")
pending.state = "failed"
expect(() => files.get(pending.id)).toThrow("Inspect browser.console")
expect(() => files.get(Browser.FileID.make(`file_${crypto.randomUUID()}`))).toThrow(
"not a server path or request ID",
)
const id = await files.save("capture.json", "application/json", new TextEncoder().encode("{}"))
await rm(files.get(id).path)
await expect(files.transfer(id)).rejects.toThrow("on the desktop")
await expect(
files.save("large.bin", "application/octet-stream", new Uint8Array(Browser.MAX_FILE_BYTES + 1)),
).rejects.toThrow("Do not retry an identical capture")
// The page sees the temp basename as File.name: spaces and non-ASCII stay, only characters no
// filesystem accepts change, and a bare dot or separator cannot leave the per-file directory.
expect(path.basename(files.add("Quarter 1 日本語.csv", "text/csv").path)).toBe("Quarter 1 日本語.csv")
expect(path.basename(files.add("a/b:c?.txt", "text/plain").path)).toBe("a_b_c_.txt")
expect(path.basename(files.add("..", "text/plain").path)).toBe("file")
expect(path.dirname(files.add(".", "text/plain").path).startsWith(files.directory)).toBe(true)
} finally {
await files.dispose()
}
})
test("wrong capture formats identify the matching capture tool", () => {
expect(() => analyzeTrace({ nodes: [] })).toThrow("browser.trace.stop")
expect(() => analyzeCpu({ traceEvents: [] })).toThrow("browser.cpu.stop")
expect(() => parseHeap({ traceEvents: [] })).toThrow("browser.heap.snapshot")
})