import { NodeFileSystem, NodeHttpServer } from "@effect/platform-node"
import { ServerProcess } from "@opencode/server/process"
import { afterAll, describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { HttpServer, HttpServerError, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { createServer } from "node:http"
import { mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { WebUi } from "../src/services/web-ui"
import { it } from "../../core/test/lib/effect"
const root = await mkdtemp(path.join(tmpdir(), "opencode-web-ui-"))
afterAll(() => rm(root, { recursive: true, force: true }))
describe("web UI", () => {
it.live("serves the web shell and assets before server authentication", () =>
Effect.gen(function* () {
const transform = yield* WebUi.handler({
assets: {
"index.html": "
connect",
"_assets/app.js": "console.log('connect')",
"_assets/app.css": "body { color: black; }",
"icons/icon.svg": "",
"font.woff2": new Uint8Array([0, 1, 2, 255]),
"sw.js": "service worker",
},
})
const server = yield* ServerProcess.start(
{ hostname: "127.0.0.1", port: 0, password: "secret", database: { path: ":memory:" } },
undefined,
transform,
)
const origin = HttpServer.formatAddress(server.address)
yield* Effect.forEach(
[
"/",
"/settings",
"/workspace/example",
"/_assets/app.js",
"/_assets/app.css",
"/icons/icon.svg",
"/font.woff2",
"/sw.js",
],
(pathname) =>
Effect.gen(function* () {
yield* Effect.forEach(["GET", "HEAD"], (method) =>
Effect.gen(function* () {
const response = yield* Effect.promise(() => fetch(new URL(pathname, origin), { method }))
expect(response.status).toBe(200)
expect(response.headers.get("www-authenticate")).toBeNull()
yield* Effect.promise(() => response.arrayBuffer())
}),
)
}),
)
yield* Effect.forEach(["/api", "/api/info", "/api/event", "/api/missing", "/openapi.json"], (pathname) =>
Effect.gen(function* () {
const response = yield* Effect.promise(() => fetch(new URL(pathname, origin)))
expect(response.status).toBe(401)
expect(response.headers.get("www-authenticate")).toBe('Basic realm="Secure Area"')
yield* Effect.promise(() => response.arrayBuffer())
}),
)
const response = yield* Effect.promise(() =>
fetch(new URL("/api/info", origin), { headers: { authorization: `Basic ${btoa("opencode:secret")}` } }),
)
expect(response.status).toBe(200)
expect(yield* Effect.promise(() => response.json())).toHaveProperty("pid")
const pairing = yield* Effect.promise(() =>
fetch(new URL("/api/pair", origin), {
method: "POST",
headers: { authorization: `Basic ${btoa("opencode:secret")}` },
}).then((response) => response.json() as Promise<{ code: string }>),
)
const redirect = yield* Effect.promise(() =>
fetch(new URL(`/auth/connect/${pairing.code}`, origin), {
redirect: "manual",
headers: { accept: "text/html" },
}),
)
expect(redirect.status).toBe(302)
const cookie = (redirect.headers.get("set-cookie") ?? "").split(";")[0]
const authorized = yield* Effect.promise(() => fetch(new URL("/api/info", origin), { headers: { cookie } }))
expect(authorized.status).toBe(200)
yield* Effect.promise(() => authorized.arrayBuffer())
}).pipe(Effect.provide(NodeFileSystem.layer)),
)
test("falls back from API routes to assets and the SPA index", async () => {
const index = path.join(root, "index.html")
const asset = path.join(root, "app.js")
await writeFile(index, "embedded")
await writeFile(asset, "console.log('embedded')")
const assets = {
"index.html": await Bun.file(index).text(),
"_assets/app.js": await Bun.file(asset).text(),
"sw.js": "service worker",
"registerSW.js": "registration",
"font.woff2": new Uint8Array([0, 1, 2, 255]),
}
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const transform = yield* WebUi.handler({ assets })
const http = yield* NodeHttpServer.make(createServer, { host: "127.0.0.1", port: 0 })
yield* http.serve(
transform(
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const pathname = new URL(request.url, "http://localhost").pathname
if (pathname === "/api/info")
return HttpServerResponse.jsonUnsafe({
version: "test",
pid: 1,
urls: [origin],
paths: { tmp: "/tmp/opencode" },
})
return yield* Effect.fail(
new HttpServerError.HttpServerError({
reason: new HttpServerError.RouteNotFound({ request }),
}),
)
}),
),
)
const origin = HttpServer.formatAddress(http.address)
const status = yield* Effect.promise(() => fetch(`${origin}/api/info`))
expect(yield* Effect.promise(() => status.json())).toEqual({
version: "test",
pid: 1,
urls: [origin],
paths: { tmp: "/tmp/opencode" },
})
const missing = yield* Effect.promise(() => fetch(`${origin}/api/missing`))
expect(missing.status).toBe(404)
expect(yield* Effect.promise(() => missing.text())).toBe("")
yield* Effect.forEach(["/_assets/old.js", "/_assets/old.css", "/_assets/missing"], (pathname) =>
Effect.gen(function* () {
const missing = yield* Effect.promise(() => fetch(`${origin}${pathname}`))
expect(missing.status).toBe(404)
expect(missing.headers.get("cache-control")).toBe("no-store")
expect(yield* Effect.promise(() => missing.text())).toBe("")
}),
)
const script = yield* Effect.promise(() => fetch(`${origin}/_assets/app.js`))
expect(yield* Effect.promise(() => script.text())).toBe("console.log('embedded')")
expect(script.headers.get("content-type")).toContain("javascript")
expect(script.headers.get("cache-control")).toBe("public, max-age=31536000, immutable")
const worker = yield* Effect.promise(() => fetch(`${origin}/sw.js`))
expect(worker.headers.get("cache-control")).toBe("no-cache")
const registration = yield* Effect.promise(() => fetch(`${origin}/registerSW.js`))
expect(registration.headers.get("cache-control")).toBe("no-cache")
const font = yield* Effect.promise(() => fetch(`${origin}/font.woff2`))
expect(font.headers.get("content-type")).toBe("font/woff2")
expect(new Uint8Array(yield* Effect.promise(() => font.arrayBuffer()))).toEqual(
new Uint8Array([0, 1, 2, 255]),
)
const fallback = yield* Effect.promise(() => fetch(`${origin}/workspace/example`))
expect(yield* Effect.promise(() => fallback.text())).toContain("embedded")
expect(fallback.headers.get("content-security-policy")).toContain("default-src 'self'")
expect(fallback.headers.get("content-security-policy")).toContain("connect-src * data: blob:")
const dotted = yield* Effect.promise(() => fetch(`${origin}/workspace/example.js`))
expect(dotted.status).toBe(200)
expect(yield* Effect.promise(() => dotted.text())).toContain("embedded")
const legacy = yield* Effect.promise(() => fetch(`${origin}/assets/missing.js`))
expect(legacy.status).toBe(200)
expect(yield* Effect.promise(() => legacy.text())).toContain("embedded")
}),
).pipe(Effect.provide(NodeFileSystem.layer)),
)
})
})