From 010c198e22a48099584d21141bc8204ff81fdfc9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 9 Jun 2026 09:39:09 -0700 Subject: [PATCH] fix CI Effect test runtimes Co-authored-by: codex --- .../Layers/ProviderCommandReactor.test.ts | 128 ++++++++++-------- .../src/provider/Layers/GrokAdapter.test.ts | 25 ++-- .../src/provider/Layers/GrokProvider.test.ts | 115 ++++++++-------- .../src/provider/acp/GrokAcpSupport.test.ts | 74 +++++----- 4 files changed, 173 insertions(+), 169 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index 66e566c83db..0d5cfe2feba 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -28,6 +28,7 @@ import * as ManagedRuntime from "effect/ManagedRuntime"; import * as PubSub from "effect/PubSub"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; +import { it as effectIt } from "@effect/vitest"; import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { deriveServerPaths, ServerConfig } from "../../config.ts"; @@ -890,70 +891,79 @@ describe("ProviderCommandReactor", () => { }); }); - it("rejects changing models after start when the provider requires a new thread", async () => { - const harness = await createHarness({ requiresNewThreadForModelChange: true }); - const now = "2026-01-01T00:00:00.000Z"; + effectIt.effect( + "rejects changing models after start when the provider requires a new thread", + () => + Effect.gen(function* () { + const harness = yield* Effect.promise(() => + createHarness({ requiresNewThreadForModelChange: true }), + ); + const now = "2026-01-01T00:00:00.000Z"; - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-restricted-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-restricted-1"), - role: "user", - text: "first", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); + yield* harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-restricted-1"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-restricted-1"), + role: "user", + text: "first", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); + yield* Effect.promise(() => waitFor(() => harness.sendTurn.mock.calls.length === 1)); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-restricted-2"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-restricted-2"), - role: "user", - text: "second", - attachments: [], - }, - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5.1-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); + yield* harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-restricted-2"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-restricted-2"), + role: "user", + text: "second", + attachments: [], + }, + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.1-codex", + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }); - await waitFor(async () => { - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - return ( - thread?.activities.some((activity) => activity.kind === "provider.turn.start.failed") ?? - false - ); - }); + yield* Effect.promise(() => + waitFor(async () => { + const readModel = await harness.readModel(); + const thread = readModel.threads.find( + (entry) => entry.id === ThreadId.make("thread-1"), + ); + return ( + thread?.activities.some( + (activity) => activity.kind === "provider.turn.start.failed", + ) ?? false + ); + }), + ); - expect(harness.sendTurn).toHaveBeenCalledTimes(1); - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect( - thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"), - ).toMatchObject({ - payload: { - detail: expect.stringContaining("cannot switch models after the conversation has started"), - }, - }); - }); + expect(harness.sendTurn).toHaveBeenCalledTimes(1); + const readModel = yield* Effect.promise(() => harness.readModel()); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect( + thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"), + ).toMatchObject({ + payload: { + detail: expect.stringContaining( + "cannot switch models after the conversation has started", + ), + }, + }); + }), + ); it("starts a first turn on the requested provider instance even when it differs from the thread model", async () => { const harness = await createHarness({ diff --git a/apps/server/src/provider/Layers/GrokAdapter.test.ts b/apps/server/src/provider/Layers/GrokAdapter.test.ts index b6e45b5026a..bfd5ae25755 100644 --- a/apps/server/src/provider/Layers/GrokAdapter.test.ts +++ b/apps/server/src/provider/Layers/GrokAdapter.test.ts @@ -45,20 +45,21 @@ exec ${JSON.stringify(mockAgentCommand)} ${JSON.stringify(mockAgentPath)} "$@" return wrapperPath; } -async function waitForFileContent(filePath: string, attempts = 40): Promise { - const readAttempt = async (remainingAttempts: number): Promise => { - if (remainingAttempts <= 0) { - throw new Error(`Timed out waiting for file content at ${filePath}`); - } - try { - const raw = await readFile(filePath, "utf8"); +function waitForFileContent(filePath: string, attempts = 40): Effect.Effect { + const readAttempt = (remainingAttempts: number): Effect.Effect => + Effect.gen(function* () { + if (remainingAttempts <= 0) { + return yield* Effect.die(new Error(`Timed out waiting for file content at ${filePath}`)); + } + const raw = yield* Effect.tryPromise(() => readFile(filePath, "utf8")).pipe( + Effect.orElseSucceed(() => ""), + ); if (raw.trim().length > 0) { return raw; } - } catch {} - await Effect.runPromise(Effect.sleep("25 millis")); - return readAttempt(remainingAttempts - 1); - }; + yield* Effect.sleep("25 millis"); + return yield* readAttempt(remainingAttempts - 1); + }); return readAttempt(attempts); } @@ -169,7 +170,7 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => { yield* adapter.stopSession(threadId); - const exitLog = yield* Effect.promise(() => waitForFileContent(exitLogPath)); + const exitLog = yield* waitForFileContent(exitLogPath); assert.include(exitLog, "SIGTERM"); }), ); diff --git a/apps/server/src/provider/Layers/GrokProvider.test.ts b/apps/server/src/provider/Layers/GrokProvider.test.ts index d5453ef60cb..75d0982565e 100644 --- a/apps/server/src/provider/Layers/GrokProvider.test.ts +++ b/apps/server/src/provider/Layers/GrokProvider.test.ts @@ -1,67 +1,60 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; +import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import { describe, expect, it } from "vite-plus/test"; -import { ChildProcessSpawner } from "effect/unstable/process"; import { GrokSettings } from "@t3tools/contracts"; import { buildInitialGrokProviderSnapshot, checkGrokProviderStatus } from "./GrokProvider.ts"; const decodeGrokSettings = Schema.decodeSync(GrokSettings); -const runNode = ( - effect: Effect.Effect< - A, - E, - FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner - >, -): Promise => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))); - describe("buildInitialGrokProviderSnapshot", () => { - it("returns a disabled snapshot when settings.enabled is false", async () => { - const snapshot = await Effect.runPromise( - buildInitialGrokProviderSnapshot(decodeGrokSettings({ enabled: false })), - ); - expect(snapshot.enabled).toBe(false); - expect(snapshot.status).toBe("disabled"); - expect(snapshot.installed).toBe(false); - expect(snapshot.message).toContain("disabled"); - }); + it.effect("returns a disabled snapshot when settings.enabled is false", () => + Effect.gen(function* () { + const snapshot = yield* buildInitialGrokProviderSnapshot( + decodeGrokSettings({ enabled: false }), + ); + expect(snapshot.enabled).toBe(false); + expect(snapshot.status).toBe("disabled"); + expect(snapshot.installed).toBe(false); + expect(snapshot.message).toContain("disabled"); + }), + ); - it("returns a pending snapshot by default", async () => { - const snapshot = await Effect.runPromise( - buildInitialGrokProviderSnapshot(decodeGrokSettings({})), - ); - expect(snapshot.enabled).toBe(true); - expect(snapshot.installed).toBe(true); - expect(snapshot.status).toBe("warning"); - expect(snapshot.version).toBeNull(); - expect(snapshot.message).toContain("Checking Grok"); - expect(snapshot.requiresNewThreadForModelChange).toBe(true); - }); + it.effect("returns a pending snapshot by default", () => + Effect.gen(function* () { + const snapshot = yield* buildInitialGrokProviderSnapshot(decodeGrokSettings({})); + expect(snapshot.enabled).toBe(true); + expect(snapshot.installed).toBe(true); + expect(snapshot.status).toBe("warning"); + expect(snapshot.version).toBeNull(); + expect(snapshot.message).toContain("Checking Grok"); + expect(snapshot.requiresNewThreadForModelChange).toBe(true); + }), + ); }); -describe("checkGrokProviderStatus", () => { - it("reports the binary as missing when the binary path does not resolve", async () => { - const snapshot = await runNode( - checkGrokProviderStatus( +it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => { + it.effect("reports the binary as missing when the binary path does not resolve", () => + Effect.gen(function* () { + const snapshot = yield* checkGrokProviderStatus( decodeGrokSettings({ enabled: true, binaryPath: "/definitely/not/installed/grok-binary", }), - ), - ); - expect(snapshot.enabled).toBe(true); - expect(snapshot.installed).toBe(false); - expect(snapshot.status).toBe("error"); - expect(snapshot.message).toMatch(/not installed|not on PATH|Failed to execute/); - }); + ); + expect(snapshot.enabled).toBe(true); + expect(snapshot.installed).toBe(false); + expect(snapshot.status).toBe("error"); + expect(snapshot.message).toMatch(/not installed|not on PATH|Failed to execute/); + }), + ); - it("reports an installed CLI as unhealthy when --version exits non-zero", async () => { - const snapshot = await runNode( - Effect.scoped( + it.effect("reports an installed CLI as unhealthy when --version exits non-zero", () => + Effect.gen(function* () { + const snapshot = yield* Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -77,18 +70,18 @@ describe("checkGrokProviderStatus", () => { decodeGrokSettings({ enabled: true, binaryPath: grokPath }), ); }), - ), - ); + ); - expect(snapshot.enabled).toBe(true); - expect(snapshot.installed).toBe(true); - expect(snapshot.status).toBe("error"); - expect(snapshot.message).toContain("broken grok install"); - }); + expect(snapshot.enabled).toBe(true); + expect(snapshot.installed).toBe(true); + expect(snapshot.status).toBe("error"); + expect(snapshot.message).toContain("broken grok install"); + }), + ); - it("reports an error when ACP model discovery is unavailable", async () => { - const snapshot = await runNode( - Effect.scoped( + it.effect("reports an error when ACP model discovery is unavailable", () => + Effect.gen(function* () { + const snapshot = yield* Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -104,12 +97,12 @@ describe("checkGrokProviderStatus", () => { decodeGrokSettings({ enabled: true, binaryPath: grokPath }), ); }), - ), - ); + ); - expect(snapshot.status).toBe("error"); - expect(snapshot.installed).toBe(true); - expect(snapshot.models.map((model) => model.slug)).toEqual(["grok-build"]); - expect(snapshot.message).toContain("ACP startup failed"); - }); + expect(snapshot.status).toBe("error"); + expect(snapshot.installed).toBe(true); + expect(snapshot.models.map((model) => model.slug)).toEqual(["grok-build"]); + expect(snapshot.message).toContain("ACP startup failed"); + }), + ); }); diff --git a/apps/server/src/provider/acp/GrokAcpSupport.test.ts b/apps/server/src/provider/acp/GrokAcpSupport.test.ts index 8102b6469c1..02d60976b24 100644 --- a/apps/server/src/provider/acp/GrokAcpSupport.test.ts +++ b/apps/server/src/provider/acp/GrokAcpSupport.test.ts @@ -1,6 +1,6 @@ +import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as EffectAcpErrors from "effect-acp/errors"; -import { describe, expect, it } from "vite-plus/test"; import { applyGrokAcpModelSelection, @@ -49,61 +49,61 @@ describe("applyGrokAcpModelSelection", () => { return { runtime, modelCalls }; }; - it("calls session/set_model when the requested model differs from current", async () => { - const { runtime, modelCalls } = makeRecordingRuntime(); - const result = await Effect.runPromise( - applyGrokAcpModelSelection({ + it.effect("calls session/set_model when the requested model differs from current", () => + Effect.gen(function* () { + const { runtime, modelCalls } = makeRecordingRuntime(); + const result = yield* applyGrokAcpModelSelection({ runtime, currentModelId: "grok-build", requestedModelId: "grok-mock-alt", mapError: (cause) => cause.message, - }), - ); - expect(modelCalls).toEqual(["grok-mock-alt"]); - expect(result).toBe("grok-mock-alt"); - }); + }); + expect(modelCalls).toEqual(["grok-mock-alt"]); + expect(result).toBe("grok-mock-alt"); + }), + ); - it("skips set_model when requested matches current", async () => { - const { runtime, modelCalls } = makeRecordingRuntime(); - const result = await Effect.runPromise( - applyGrokAcpModelSelection({ + it.effect("skips set_model when requested matches current", () => + Effect.gen(function* () { + const { runtime, modelCalls } = makeRecordingRuntime(); + const result = yield* applyGrokAcpModelSelection({ runtime, currentModelId: "grok-build", requestedModelId: "grok-build", mapError: (cause) => cause.message, - }), - ); - expect(modelCalls).toEqual([]); - expect(result).toBe("grok-build"); - }); + }); + expect(modelCalls).toEqual([]); + expect(result).toBe("grok-build"); + }), + ); - it("skips set_model when no model is requested", async () => { - const { runtime, modelCalls } = makeRecordingRuntime(); - const result = await Effect.runPromise( - applyGrokAcpModelSelection({ + it.effect("skips set_model when no model is requested", () => + Effect.gen(function* () { + const { runtime, modelCalls } = makeRecordingRuntime(); + const result = yield* applyGrokAcpModelSelection({ runtime, currentModelId: "grok-build", requestedModelId: undefined, mapError: (cause) => cause.message, - }), - ); - expect(modelCalls).toEqual([]); - expect(result).toBe("grok-build"); - }); + }); + expect(modelCalls).toEqual([]); + expect(result).toBe("grok-build"); + }), + ); - it("propagates session/set_model failures via mapError", async () => { - const failure = EffectAcpErrors.AcpRequestError.invalidParams("session id not known"); - const { runtime } = makeRecordingRuntime(failure); - const error = await Effect.runPromise( - Effect.flip( + it.effect("propagates session/set_model failures via mapError", () => + Effect.gen(function* () { + const failure = EffectAcpErrors.AcpRequestError.invalidParams("session id not known"); + const { runtime } = makeRecordingRuntime(failure); + const error = yield* Effect.flip( applyGrokAcpModelSelection({ runtime, currentModelId: "grok-build", requestedModelId: "grok-mock-alt", mapError: (cause) => cause.message, }), - ), - ); - expect(error).toBe(failure.message); - }); + ); + expect(error).toBe(failure.message); + }), + ); });