refactor(client): simplify local service lifecycle

This commit is contained in:
Dax Raad 2026-07-15 13:16:49 -04:00
commit 75bc611ef1
51 changed files with 635 additions and 534 deletions

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { Service } from "@opencode-ai/client/effect"
import { Service } from "@opencode-ai/client/effect/service"
import path from "node:path"
import { Standalone } from "../../src/services/standalone"

View file

@ -19,8 +19,6 @@ test("resolution groups Effect-native lifecycle operations only for the managed
healthy: true,
version: InstallationVersion,
pid: process.pid,
instanceID: id,
status: { type: "ready" },
})
},
})
@ -45,9 +43,9 @@ test("resolution groups Effect-native lifecycle operations only for the managed
expect(resolved.endpoint.url).toBe(server.url.toString())
expect(resolved.service).toBeDefined()
if (!resolved.service) throw new Error("Expected managed service capabilities")
expect(Effect.isEffect(resolved.service.reconnect(() => {}))).toBe(true)
expect(Effect.isEffect(resolved.service.reconnect())).toBe(true)
expect(Effect.isEffect(resolved.service.restart())).toBe(true)
expect(await runPromise(resolved.service.reconnect(() => {}))).toEqual(resolved.endpoint)
expect(await runPromise(resolved.service.reconnect())).toEqual(resolved.endpoint)
const explicit = await runPromise(ServerConnection.resolve({ server: server.url.toString() }))
expect(explicit.endpoint.url).toBe(server.url.toString())

View file

@ -1,5 +1,5 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Service } from "@opencode-ai/client/effect"
import { Service } from "@opencode-ai/client/effect/service"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql"
@ -146,11 +146,10 @@ test("concurrent service processes elect one server", async () => {
await fetch(new URL("/api/health", info.url), {
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
}).then((response) => response.json()),
).toMatchObject({
).toEqual({
healthy: true,
version: info.version,
pid: info.pid,
instanceID: info.id,
status: { type: "ready" },
})
const blockedTemp = registration + "." + info.id + ".tmp"
await fs.mkdir(blockedTemp)
@ -193,7 +192,7 @@ test("concurrent service processes elect one server", async () => {
).toEqual({ timeSuspended: null })
expect(await waitForExecutionStart(database, sessionID)).toBe(1)
await Effect.runPromise(
Service.stop({ file: registration }, { targetVersion: "next" }).pipe(Effect.provide(NodeFileSystem.layer)),
Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)),
)
await winner?.exited
} finally {
@ -227,19 +226,6 @@ test("a failed service stays registered and owns the lock until stopped", async
try {
const info = await waitForInfo(registration)
const status = await Effect.runPromise(
Service.status({ file: registration }).pipe(
Effect.filterOrFail((status) => status.type === "failed"),
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(200)))),
Effect.provide(NodeFileSystem.layer),
),
)
expect(status).toEqual({
type: "failed",
version: info.version,
message: "The background service could not start.",
action: "Run `opencode service restart` after checking the service logs.",
})
expect(owner.exitCode).toBe(null)
const contender = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })