Files
2026-08-11 14:58:23 +08:00

83 lines
3.7 KiB
TypeScript

import { NodeFileSystem, NodeHttpServer } from "@effect/platform-node"
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"
const root = await mkdtemp(path.join(tmpdir(), "opencode-web-ui-"))
afterAll(() => rm(root, { recursive: true, force: true }))
describe("web UI", () => {
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, "<html><body>embedded</body></html>")
await writeFile(asset, "console.log('embedded')")
const assets = {
"index.html": await Bun.file(index).text(),
"app.js": await Bun.file(asset).text(),
"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/health") return HttpServerResponse.jsonUnsafe({ healthy: true })
if (pathname === "/api/missing")
return HttpServerResponse.jsonUnsafe({ code: "missing" }, { status: 404 })
return yield* Effect.fail(
new HttpServerError.HttpServerError({
reason: new HttpServerError.RouteNotFound({ request }),
}),
)
}),
),
)
const origin = HttpServer.formatAddress(http.address)
const health = yield* Effect.promise(() => fetch(`${origin}/api/health`))
expect(yield* Effect.promise(() => health.json())).toEqual({ healthy: true })
const missing = yield* Effect.promise(() => fetch(`${origin}/api/missing`))
expect(missing.status).toBe(404)
expect(yield* Effect.promise(() => missing.json())).toEqual({ code: "missing" })
const script = yield* Effect.promise(() => fetch(`${origin}/app.js`))
expect(yield* Effect.promise(() => script.text())).toBe("console.log('embedded')")
const font = yield* Effect.promise(() => fetch(`${origin}/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:")
}),
).pipe(Effect.provide(NodeFileSystem.layer)),
)
})
test("adds server credentials to browser URL userinfo", () => {
const target = new URL(
WebUi.url({
url: "http://localhost:4096",
auth: { type: "basic", username: "opencode", password: "secret" },
}),
)
expect(target.username).toBe("opencode")
expect(target.password).toBe("secret")
expect(target.searchParams.has("auth_token")).toBe(false)
})
})