workspace-adapter.test.ts 5.4 KB

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