config.test.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import fs from "node:fs/promises"
  2. import path from "node:path"
  3. import { expect } from "bun:test"
  4. import { Config } from "@opencode-ai/schema/config"
  5. import { Effect, Schema } from "effect"
  6. import { HttpServer } from "effect/unstable/http"
  7. import { tmpdir } from "../../core/test/fixture/tmpdir"
  8. import { it } from "../../core/test/lib/effect"
  9. import { ServerProcess } from "../src/process"
  10. import { AbsolutePath } from "@opencode-ai/schema/schema"
  11. it.live("returns ordered config entries for the requested directory", () =>
  12. Effect.acquireUseRelease(
  13. Effect.promise(() => tmpdir("opencode-config-endpoint-")),
  14. (tmp) =>
  15. Effect.gen(function* () {
  16. const global = path.join(tmp.path, "global")
  17. const project = path.join(tmp.path, "project")
  18. const config = path.join(project, "opencode.json")
  19. yield* Effect.promise(() =>
  20. Promise.all([fs.mkdir(global, { recursive: true }), fs.mkdir(project, { recursive: true })]),
  21. )
  22. yield* Effect.promise(() =>
  23. fs.writeFile(
  24. config,
  25. JSON.stringify({
  26. permissions: [
  27. { action: "shell", resource: "*", effect: "ask" },
  28. { action: "shell", resource: "git status", effect: "allow" },
  29. ],
  30. mcp: { servers: { docs: { type: "remote", url: "https://example.com/mcp" } } },
  31. }),
  32. ),
  33. )
  34. const server = yield* ServerProcess.start<never, never>({
  35. hostname: "127.0.0.1",
  36. port: 0,
  37. password: "secret",
  38. app: { version: "test-version" },
  39. database: { path: ":memory:" },
  40. config: { directory: global },
  41. fs: { filewatcher: false },
  42. })
  43. const url = new URL("/api/config", HttpServer.formatAddress(server.address))
  44. url.searchParams.set("location[directory]", project)
  45. const response = yield* Effect.promise(() =>
  46. fetch(url, { headers: { authorization: `Basic ${btoa("opencode:secret")}` } }),
  47. )
  48. const body: unknown = yield* Effect.promise(() => response.json())
  49. const entries = Schema.decodeUnknownSync(Schema.Array(Config.Entry))(body)
  50. expect(response.status).toBe(200)
  51. expect(Array.isArray(entries)).toBe(true)
  52. const document = entries.find(
  53. (entry): entry is Config.Document => entry.type === "document" && entry.path === config,
  54. )
  55. expect(document?.info.permissions).toEqual([
  56. { action: "shell", resource: "*", effect: "ask" },
  57. { action: "shell", resource: "git status", effect: "allow" },
  58. ])
  59. expect(document?.path).toBe(AbsolutePath.make(config))
  60. if (!Array.isArray(body)) throw new Error("Expected a config entry array")
  61. const raw = body.find((entry) => isRecord(entry) && entry["type"] === "document" && entry["path"] === config)
  62. if (!isRecord(raw) || !isRecord(raw["info"])) throw new Error("Expected a config document")
  63. expect(raw["info"]).not.toHaveProperty("default_agent")
  64. expect(raw["info"]).not.toHaveProperty("model")
  65. const mcp = raw["info"]["mcp"]
  66. if (!isRecord(mcp) || !isRecord(mcp["servers"]) || !isRecord(mcp["servers"]["docs"]))
  67. throw new Error("Expected an MCP server config")
  68. expect(mcp["servers"]["docs"]).not.toHaveProperty("headers")
  69. expect(mcp["servers"]["docs"]).not.toHaveProperty("oauth")
  70. }),
  71. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  72. ),
  73. )
  74. function isRecord(value: unknown): value is Record<string, unknown> {
  75. return typeof value === "object" && value !== null && !Array.isArray(value)
  76. }