diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index 71ab041def471..2281365ea23e8 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -6,7 +6,12 @@ import * as NodePath from "node:path"; import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; -import { DEFAULT_SERVER_SETTINGS, ProviderInstanceId } from "@t3tools/contracts"; +import { + DEFAULT_SERVER_SETTINGS, + ProviderInstanceId, + ProviderSessionId, + ThreadId, +} from "@t3tools/contracts"; import type { OpenCodeSettings } from "../settings.ts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Deferred from "effect/Deferred"; @@ -575,4 +580,39 @@ it.layer(layerUpdate)("OpenCodeDriver updates", (it) => { assert.isNull(v2.update); }).pipe(Effect.scoped), ); + + it.effect( + "opens sessions through OpenCode 2 after the binary is upgraded in place from 1.x", + () => + Effect.gen(function* () { + versionOutput.current = "1.18.18\n"; + const instance = yield* create({}, noHttp); + // Let the startup status check claim the refresh permit, then wait behind it so it + // cannot probe after the upgrade below. + yield* Effect.yieldNow; + yield* instance.snapshot.refresh; + serverStarts.length = 0; + const openSession = instance.orchestrationAdapter + .openSession({ + threadId: ThreadId.make("thread-upgraded"), + providerSessionId: ProviderSessionId.make("session-upgraded"), + modelSelection: { instanceId: instance.instanceId, model: "opencode/big-pickle" }, + runtimePolicy: { runtimeMode: "full-access", interactionMode: "default", cwd: null }, + }) + .pipe(Effect.scoped, Effect.flip); + + yield* openSession; + assert.deepStrictEqual(serverStarts, ["connect"], "1.x opens through the 1.x adapter"); + + versionOutput.current = "opencode v2.2.4\n"; + serverStarts.length = 0; + yield* openSession; + assert.notInclude( + serverStarts, + "connect", + "after upgrading to 2.x, the next session must not open through the 1.x adapter", + ); + assert.include(serverStarts, "start", "the OpenCode 2 adapter starts its server"); + }).pipe(Effect.scoped), + ); }); diff --git a/packages/provider-opencode/src/server/versionProbe.test.ts b/packages/provider-opencode/src/server/versionProbe.test.ts index d698bbcb8785b..c6a9536f774f3 100644 --- a/packages/provider-opencode/src/server/versionProbe.test.ts +++ b/packages/provider-opencode/src/server/versionProbe.test.ts @@ -160,9 +160,9 @@ describe("OpenCode version probe", () => { }), ); - it.effect("remembers a probed runtime but never a failure", () => + it.effect("remembers a probed 2.x runtime but re-probes 1.x and failures", () => Effect.gen(function* () { - const outputs = ["", "opencode v2.0.18\n", "1.18.32\n"]; + const outputs = ["", "opencode v2.0.18\n", "1.18.32\n", "1.18.32\n"]; let calls = 0; const runtime = { runOpenCodeCommand: () => { @@ -190,8 +190,9 @@ describe("OpenCode version probe", () => { assert.strictEqual((yield* probe.get).generation, "v2"); assert.strictEqual(calls, 2); assert.strictEqual((yield* probe.refresh).generation, "v1"); - assert.strictEqual((yield* probe.get).generation, "v1"); assert.strictEqual(calls, 3); + assert.strictEqual((yield* probe.get).generation, "v1"); + assert.strictEqual(calls, 4); }), ); }); diff --git a/packages/provider-opencode/src/server/versionProbe.ts b/packages/provider-opencode/src/server/versionProbe.ts index 5f0f871d5213f..ab40142de845a 100644 --- a/packages/provider-opencode/src/server/versionProbe.ts +++ b/packages/provider-opencode/src/server/versionProbe.ts @@ -172,11 +172,14 @@ export const probeOpenCodeRuntime = ( ? probeOpenCodeServer(settings.serverUrl, settings.serverPassword) : probeOpenCodeBinary(settings.binaryPath, environment); +// Only 2.x is kept: 1.x can be upgraded in place while T3 runs, and 2.x converts the shared +// database, so going back is unsupported. export const makeOpenCodeRuntimeProbe = (probe: Effect.Effect) => Effect.map( Cache.makeWith(() => probe, { capacity: 1, - timeToLive: (exit) => (Exit.isSuccess(exit) ? Duration.infinity : Duration.zero), + timeToLive: (exit) => + Exit.isSuccess(exit) && exit.value.generation === "v2" ? Duration.infinity : Duration.zero, }), (cache) => ({ get: Cache.get(cache, undefined),