mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-29 05:53:24 +08:00
159 lines
7.2 KiB
TypeScript
159 lines
7.2 KiB
TypeScript
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": "<html><body>connect</body></html>",
|
|
"_assets/app.js": "console.log('connect')",
|
|
"_assets/app.css": "body { color: black; }",
|
|
"icons/icon.svg": "<svg></svg>",
|
|
"font.woff2": new Uint8Array([0, 1, 2, 255]),
|
|
"sw.js": "service worker",
|
|
},
|
|
})
|
|
const server = yield* ServerProcess.start<never, never>(
|
|
{ hostname: "127.0.0.1", port: 0, password: "secret", database: { path: ":memory:" } },
|
|
undefined,
|
|
transform,
|
|
)
|
|
const origin = HttpServer.formatAddress(server.address)
|
|
yield* Effect.forEach(
|
|
[
|
|
"/",
|
|
"/connect?data=%7B%7D",
|
|
"/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/status", "/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/status", origin), { headers: { authorization: `Basic ${btoa("opencode:secret")}` } }),
|
|
)
|
|
expect(response.status).toBe(200)
|
|
expect(yield* Effect.promise(() => response.json())).toHaveProperty("pid")
|
|
}).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, "<html><body>embedded</body></html>")
|
|
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/status")
|
|
return HttpServerResponse.jsonUnsafe({ version: "test", pid: 1, urls: [origin] })
|
|
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/status`))
|
|
expect(yield* Effect.promise(() => status.json())).toEqual({ version: "test", pid: 1, urls: [origin] })
|
|
|
|
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)),
|
|
)
|
|
})
|
|
})
|