|
|
@@ -0,0 +1,68 @@
|
|
|
+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 })
|
|
|
+ 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.text())).toBe("")
|
|
|
+
|
|
|
+ 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)),
|
|
|
+ )
|
|
|
+ })
|
|
|
+})
|