| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- import { describe, expect } from "bun:test"
- import { Effect, Layer, Queue } from "effect"
- import { Flag } from "@kirincode-ai/core/flag/flag"
- import { GlobalBus, type GlobalEvent } from "@/bus/global"
- import { Worktree } from "@/worktree"
- import { Server } from "../../src/server/server"
- import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/groups/experimental"
- import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace"
- import { resetDatabase } from "../fixture/db"
- import { disposeAllInstances, TestInstance } from "../fixture/fixture"
- import { testEffect } from "../lib/effect"
- const stateLayer = Layer.effectDiscard(
- Effect.gen(function* () {
- const original = {
- KIRINCODE_EXPERIMENTAL_WORKSPACES: Flag.KIRINCODE_EXPERIMENTAL_WORKSPACES,
- }
- Flag.KIRINCODE_EXPERIMENTAL_WORKSPACES = true
- yield* Effect.addFinalizer(() =>
- Effect.promise(async () => {
- Flag.KIRINCODE_EXPERIMENTAL_WORKSPACES = original.KIRINCODE_EXPERIMENTAL_WORKSPACES
- await resetDatabase()
- }),
- )
- }),
- )
- const it = testEffect(stateLayer)
- const worktreeTest = process.platform === "win32" ? it.instance.skip : it.instance
- type TestServer = ReturnType<typeof Server.Default>["app"]
- type CreatedWorktree = { directory: string }
- type ScopedWorktree = { directory: string; body: CreatedWorktree; ready: Effect.Effect<void, Error> }
- function serverScoped() {
- return Effect.sync(() => Server.Default().app)
- }
- function request(server: TestServer, input: string, init?: RequestInit) {
- return Effect.promise(() => Promise.resolve(server.request(input, init)))
- }
- function withRequestTimeout(effect: Effect.Effect<Response>, label: string, ms = 5_000) {
- return effect.pipe(
- Effect.timeoutOrElse({
- duration: `${ms} millis`,
- orElse: () => Effect.fail(new Error(`${label} timed out after ${ms}ms`)),
- }),
- )
- }
- function json<T>(response: Response) {
- return Effect.promise(() => response.json() as Promise<T>)
- }
- function readyWatcher() {
- return Effect.gen(function* () {
- const events = yield* Queue.bounded<GlobalEvent>(1)
- const on = (event: GlobalEvent) => {
- if (event.payload.type === Worktree.Event.Ready.type) Queue.offerUnsafe(events, event)
- }
- GlobalBus.on("event", on)
- yield* Effect.addFinalizer(() => Effect.sync(() => GlobalBus.off("event", on)))
- return (directory: string) =>
- Effect.gen(function* () {
- while (true) {
- const event = yield* Queue.take(events)
- if (event.directory === directory) return
- }
- }).pipe(
- Effect.timeoutOrElse({
- duration: "10 seconds",
- orElse: () => Effect.fail(new Error(`timed out waiting for worktree.ready: ${directory}`)),
- }),
- )
- })
- }
- function removeCreatedWorktree(input: {
- server: TestServer
- rootDirectory: string
- worktreeDirectory: string
- ready: Effect.Effect<void, Error>
- }) {
- return Effect.gen(function* () {
- yield* input.ready.pipe(Effect.timeout("1 second"), Effect.ignore)
- yield* Effect.promise(() => disposeAllInstances()).pipe(Effect.ignore)
- const removed = yield* request(
- input.server,
- `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(input.rootDirectory)}`,
- {
- method: "DELETE",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ directory: input.worktreeDirectory }),
- },
- )
- if (removed.status !== 200) {
- const message = yield* Effect.promise(() => removed.text())
- throw new Error(`failed to remove worktree: ${removed.status} ${message}`)
- }
- const ok = yield* json<boolean>(removed)
- if (!ok) throw new Error(`failed to remove worktree ${input.worktreeDirectory}`)
- })
- }
- function createWorktreeScoped(input: {
- server: TestServer
- directory: string
- path: string
- init: RequestInit
- timeoutLabel: string
- timeoutMs?: number
- }) {
- return Effect.acquireRelease(
- Effect.gen(function* () {
- const waitReady = yield* readyWatcher()
- const response = yield* withRequestTimeout(
- request(input.server, input.path, input.init),
- input.timeoutLabel,
- input.timeoutMs,
- )
- if (response.status !== 200) {
- const message = yield* Effect.promise(() => response.text())
- throw new Error(`${input.timeoutLabel} failed: ${response.status} ${message}`)
- }
- expect(response.status).toBe(200)
- const body = yield* json<CreatedWorktree>(response)
- return { directory: body.directory, body, ready: waitReady(body.directory) } satisfies ScopedWorktree
- }),
- (created) =>
- removeCreatedWorktree({
- server: input.server,
- rootDirectory: input.directory,
- worktreeDirectory: created.directory,
- ready: created.ready,
- }).pipe(Effect.orDie),
- ).pipe(Effect.map((created) => created.body))
- }
- function setProjectStartCommand(input: { server: TestServer; directory: string; command: string }) {
- return Effect.gen(function* () {
- const current = yield* request(input.server, `/project/current?directory=${encodeURIComponent(input.directory)}`)
- expect(current.status).toBe(200)
- const project = yield* json<{ id: string }>(current)
- const updated = yield* request(
- input.server,
- `/project/${project.id}?directory=${encodeURIComponent(input.directory)}`,
- {
- method: "PATCH",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ commands: { start: input.command } }),
- },
- )
- expect(updated.status).toBe(200)
- })
- }
- describe("worktree endpoint reproduction", () => {
- worktreeTest(
- "direct HttpApi worktree create returns without waiting for boot",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- const server = yield* serverScoped()
- const response = yield* createWorktreeScoped({
- server,
- directory: test.directory,
- path: `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`,
- init: {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({}),
- },
- timeoutLabel: "direct worktree create",
- })
- expect(response).toMatchObject({ directory: expect.any(String) })
- }),
- { git: true },
- )
- worktreeTest(
- "direct HttpApi worktree create accepts missing body",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- const server = yield* serverScoped()
- const response = yield* createWorktreeScoped({
- server,
- directory: test.directory,
- path: `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`,
- init: { method: "POST", headers: { "content-type": "application/json" } },
- timeoutLabel: "direct worktree create without body",
- })
- expect(response).toMatchObject({ directory: expect.any(String) })
- }),
- { git: true },
- )
- worktreeTest(
- "direct HttpApi worktree create accepts missing content type and body",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- const server = yield* serverScoped()
- const response = yield* createWorktreeScoped({
- server,
- directory: test.directory,
- path: `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`,
- init: { method: "POST" },
- timeoutLabel: "direct worktree create without content type or body",
- })
- expect(response).toMatchObject({ directory: expect.any(String) })
- }),
- { git: true },
- )
- worktreeTest(
- "direct HttpApi worktree create rejects explicit null payload",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- const server = yield* serverScoped()
- const response = yield* request(
- server,
- `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`,
- {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: "null",
- },
- )
- expect(response.status).toBe(400)
- }),
- { git: true },
- )
- worktreeTest(
- "workspace worktree create does not hang",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- const server = yield* serverScoped()
- const response = yield* createWorktreeScoped({
- server,
- directory: test.directory,
- path: `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`,
- init: {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ type: "worktree", branch: null }),
- },
- timeoutLabel: "workspace worktree create",
- timeoutMs: 8_000,
- })
- expect(response).toMatchObject({
- type: "worktree",
- directory: expect.any(String),
- })
- }),
- { git: true },
- )
- worktreeTest(
- "workspace worktree create returns without waiting for project start command",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- const server = yield* serverScoped()
- yield* setProjectStartCommand({
- server,
- directory: test.directory,
- command: 'bun -e "setTimeout(() => {}, 2000)"',
- })
- const started = Date.now()
- yield* createWorktreeScoped({
- server,
- directory: test.directory,
- path: `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`,
- init: {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ type: "worktree", branch: null }),
- },
- timeoutLabel: "workspace worktree create with project start command",
- timeoutMs: 6_000,
- })
- expect(Date.now() - started).toBeLessThan(1_500)
- }),
- { git: true },
- )
- })
|