mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
100 lines
4.2 KiB
TypeScript
100 lines
4.2 KiB
TypeScript
import { expect } from "bun:test"
|
|
import { Deferred, Effect, Fiber, Layer, Queue, Scope, Sink, Stream } from "effect"
|
|
import { ChildProcessSpawner } from "effect/unstable/process"
|
|
import { ExitCode, makeHandle, ProcessId } from "effect/unstable/process/ChildProcessSpawner"
|
|
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
|
|
import { Config } from "@opencode/core/config"
|
|
import { Environment } from "@opencode/core/environment/index"
|
|
import { Location } from "@opencode/core/location"
|
|
import { Shell } from "@opencode/core/shell"
|
|
import { Global } from "@opencode/util/global"
|
|
import { hostEnvironmentLayer } from "./fixture/environment"
|
|
import { tempGlobalLayer } from "./fixture/global"
|
|
import { tempLocationLayer } from "./fixture/location"
|
|
import { it } from "./lib/effect"
|
|
|
|
it.live("eviction makes progress past an already-removed shell", () =>
|
|
Effect.gen(function* () {
|
|
const completions = yield* Queue.unbounded<Effect.Effect<void>>()
|
|
const environment = Layer.effect(
|
|
Environment.Service,
|
|
Effect.gen(function* () {
|
|
const host = yield* Environment.Service
|
|
const scope = yield* Scope.Scope
|
|
return Environment.Service.of({
|
|
...host,
|
|
spawner: ChildProcessSpawner.make(() =>
|
|
Effect.gen(function* () {
|
|
const exited = yield* Deferred.make<ExitCode>()
|
|
const observer = yield* Deferred.make<Fiber.Fiber<unknown, unknown>>()
|
|
yield* Queue.offer(
|
|
completions,
|
|
Effect.gen(function* () {
|
|
yield* Deferred.succeed(exited, ExitCode(0))
|
|
// Shell.wait resolves before retention runs; join the whole exit handler instead.
|
|
const fiber = yield* Deferred.await(observer)
|
|
yield* Fiber.join(fiber).pipe(Effect.orDie)
|
|
}),
|
|
)
|
|
const output = Stream.succeed(Buffer.from("hello"))
|
|
return makeHandle({
|
|
pid: ProcessId(1),
|
|
exitCode: Effect.withFiber((fiber) =>
|
|
Scope.addFinalizer(scope, Fiber.interrupt(fiber)).pipe(
|
|
Effect.andThen(Deferred.succeed(observer, fiber)),
|
|
Effect.andThen(Deferred.await(exited)),
|
|
),
|
|
),
|
|
isRunning: Deferred.isDone(exited).pipe(Effect.map((done) => !done)),
|
|
kill: () => Deferred.succeed(exited, ExitCode(0)).pipe(Effect.asVoid),
|
|
stdin: Sink.drain,
|
|
stdout: output,
|
|
stderr: Stream.empty,
|
|
all: output,
|
|
getInputFd: () => Sink.drain,
|
|
getOutputFd: () => Stream.empty,
|
|
unref: Effect.succeed(Effect.void),
|
|
})
|
|
}),
|
|
),
|
|
})
|
|
}),
|
|
).pipe(Layer.provide(hostEnvironmentLayer))
|
|
|
|
yield* Effect.gen(function* () {
|
|
const shell = yield* Shell.Service
|
|
const removed = yield* shell.create({ shell: "sh", command: "removed", timeout: 0 })
|
|
const finishRemoved = yield* Queue.take(completions)
|
|
yield* shell.remove(removed.id)
|
|
expect((yield* shell.result(removed)).capture).toBeUndefined()
|
|
yield* finishRemoved
|
|
|
|
const complete = Effect.gen(function* () {
|
|
const info = yield* shell.create({ shell: "sh", command: "hello", timeout: 0 })
|
|
const finish = yield* Queue.take(completions)
|
|
yield* finish
|
|
return info
|
|
})
|
|
const oldest = yield* complete
|
|
// Exceed the 25-entry retention cap with the removed ID at the head of exitOrder.
|
|
yield* Effect.forEach(Array.from({ length: 25 }), () => complete, { discard: true })
|
|
expect(yield* shell.get(oldest.id).pipe(Effect.flip)).toBeInstanceOf(Shell.NotFoundError)
|
|
|
|
const survivor = yield* complete
|
|
expect(yield* shell.result(survivor)).toMatchObject({
|
|
info: { status: "exited", exit: 0 },
|
|
capture: { output: "hello", truncated: false },
|
|
})
|
|
}).pipe(
|
|
Effect.provide(
|
|
AppNodeBuilder.build(Shell.node, [
|
|
Location.node.replace(tempLocationLayer),
|
|
Global.node.replace(tempGlobalLayer),
|
|
Config.node.replace(Config.testLayer()),
|
|
Environment.node.replace(environment),
|
|
]),
|
|
),
|
|
)
|
|
}),
|
|
)
|