workspace-adapter.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import { afterAll, afterEach, describe, expect } from "bun:test"
  2. import { Effect, Layer, Option } from "effect"
  3. import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
  4. import { AppFileSystem } from "@opencode-ai/core/filesystem"
  5. import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
  6. import { Flag } from "@opencode-ai/core/flag/flag"
  7. import path from "path"
  8. import { pathToFileURL } from "url"
  9. import { Account } from "../../src/account/account"
  10. import { Auth } from "../../src/auth"
  11. import { Bus } from "../../src/bus"
  12. import { Config } from "../../src/config/config"
  13. import { Env } from "../../src/env"
  14. import { RuntimeFlags } from "../../src/effect/runtime-flags"
  15. import { Workspace } from "../../src/control-plane/workspace"
  16. import { Plugin } from "../../src/plugin/index"
  17. import { InstanceBootstrap } from "../../src/project/bootstrap-service"
  18. import { Instance } from "../../src/project/instance"
  19. import { InstanceStore } from "../../src/project/instance-store"
  20. import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
  21. import { testEffect } from "../lib/effect"
  22. import { NpmTest } from "../fake/npm"
  23. const emptyAccount = Layer.mock(Account.Service)({
  24. active: () => Effect.succeed(Option.none()),
  25. activeOrg: () => Effect.succeed(Option.none()),
  26. })
  27. const emptyAuth = Layer.mock(Auth.Service)({
  28. all: () => Effect.succeed({}),
  29. })
  30. const configLayer = Config.layer.pipe(
  31. Layer.provide(EffectFlock.defaultLayer),
  32. Layer.provide(AppFileSystem.defaultLayer),
  33. Layer.provide(Env.defaultLayer),
  34. Layer.provide(emptyAuth),
  35. Layer.provide(emptyAccount),
  36. Layer.provide(NpmTest.noop),
  37. )
  38. const pluginLayer = Plugin.layer.pipe(
  39. Layer.provide(Bus.layer),
  40. Layer.provide(configLayer),
  41. Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
  42. )
  43. const noopBootstrapLayer = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
  44. const workspaceLayer = Workspace.defaultLayer.pipe(
  45. Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrapLayer))),
  46. )
  47. const it = testEffect(Layer.mergeAll(pluginLayer, workspaceLayer, CrossSpawnSpawner.defaultLayer))
  48. const experimental = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
  49. Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
  50. afterEach(async () => {
  51. await disposeAllInstances()
  52. })
  53. afterAll(() => {
  54. Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = experimental
  55. })
  56. describe("plugin.workspace", () => {
  57. it.live("plugin can install a workspace adapter", () =>
  58. provideTmpdirInstance((dir) =>
  59. Effect.gen(function* () {
  60. const type = `plug-${Math.random().toString(36).slice(2)}`
  61. const file = path.join(dir, "plugin.ts")
  62. const mark = path.join(dir, "created.json")
  63. const space = path.join(dir, "space")
  64. yield* Effect.promise(() =>
  65. Bun.write(
  66. file,
  67. [
  68. "export default async ({ experimental_workspace }) => {",
  69. ` experimental_workspace.register(${JSON.stringify(type)}, {`,
  70. ' name: "plug",',
  71. ' description: "plugin workspace adapter",',
  72. " configure(input) {",
  73. ` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`,
  74. " },",
  75. " async create(input) {",
  76. ` await Bun.write(${JSON.stringify(mark)}, JSON.stringify(input))`,
  77. " },",
  78. " async remove() {},",
  79. " target(input) {",
  80. ' return { type: "local", directory: input.directory }',
  81. " },",
  82. " })",
  83. " return {}",
  84. "}",
  85. "",
  86. ].join("\n"),
  87. ),
  88. )
  89. yield* Effect.promise(() =>
  90. Bun.write(
  91. path.join(dir, "opencode.json"),
  92. JSON.stringify(
  93. {
  94. $schema: "https://opencode.ai/config.json",
  95. plugin: [pathToFileURL(file).href],
  96. },
  97. null,
  98. 2,
  99. ),
  100. ),
  101. )
  102. const plugin = yield* Plugin.Service
  103. yield* plugin.init()
  104. const workspace = yield* Workspace.Service
  105. const info = yield* workspace.create({
  106. type,
  107. branch: null,
  108. extra: { key: "value" },
  109. projectID: Instance.project.id,
  110. })
  111. expect(info.type).toBe(type)
  112. expect(info.name).toBe("plug")
  113. expect(info.branch).toBe("plug/main")
  114. expect(info.directory).toBe(space)
  115. expect(info.extra).toEqual({ key: "value" })
  116. expect(JSON.parse(yield* Effect.promise(() => Bun.file(mark).text()))).toMatchObject({
  117. type,
  118. name: "plug",
  119. branch: "plug/main",
  120. directory: space,
  121. extra: { key: "value" },
  122. })
  123. }),
  124. ),
  125. )
  126. })