authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-09 09:43:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-09 09:43:03-07:00
log8e6f4229da96076ac26e904ce9c5fa678c5def83
tree873d28dd551d37a28d7c3b24ee6403f438d43797
parent38ea6d483ebf0a8c76f7105824f79489871057e5
signature Signed by PGP key B5690EEEBB952194

[codex] Fix main CI Effect test runtimes (#3008)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 173 insertions(+), 169 deletions(-)

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+69-59
...@@ -28,6 +28,7 @@ import * as ManagedRuntime from "effect/ManagedRuntime";...@@ -28,6 +28,7 @@ import * as ManagedRuntime from "effect/ManagedRuntime";
28import * as PubSub from "effect/PubSub";28import * as PubSub from "effect/PubSub";
29import * as Scope from "effect/Scope";29import * as Scope from "effect/Scope";
30import * as Stream from "effect/Stream";30import * as Stream from "effect/Stream";
31import { it as effectIt } from "@effect/vitest";
31import { afterEach, describe, expect, it, vi } from "vite-plus/test";32import { afterEach, describe, expect, it, vi } from "vite-plus/test";
3233
33import { deriveServerPaths, ServerConfig } from "../../config.ts";34import { deriveServerPaths, ServerConfig } from "../../config.ts";
...@@ -890,70 +891,79 @@ describe("ProviderCommandReactor", () => {...@@ -890,70 +891,79 @@ describe("ProviderCommandReactor", () => {
890 });891 });
891 });892 });
892893
893 it("rejects changing models after start when the provider requires a new thread", async () => {894 effectIt.effect(
894 const harness = await createHarness({ requiresNewThreadForModelChange: true });895 "rejects changing models after start when the provider requires a new thread",
895 const now = "2026-01-01T00:00:00.000Z";896 () =>
897 Effect.gen(function* () {
898 const harness = yield* Effect.promise(() =>
899 createHarness({ requiresNewThreadForModelChange: true }),
900 );
901 const now = "2026-01-01T00:00:00.000Z";
896902
897 await Effect.runPromise(903 yield* harness.engine.dispatch({
898 harness.engine.dispatch({904 type: "thread.turn.start",
899 type: "thread.turn.start",905 commandId: CommandId.make("cmd-turn-start-restricted-1"),
900 commandId: CommandId.make("cmd-turn-start-restricted-1"),906 threadId: ThreadId.make("thread-1"),
901 threadId: ThreadId.make("thread-1"),907 message: {
902 message: {908 messageId: asMessageId("user-message-restricted-1"),
903 messageId: asMessageId("user-message-restricted-1"),909 role: "user",
904 role: "user",910 text: "first",
905 text: "first",911 attachments: [],
906 attachments: [],912 },
907 },913 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
908 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,914 runtimeMode: "approval-required",
909 runtimeMode: "approval-required",915 createdAt: now,
910 createdAt: now,916 });
911 }),
912 );
913917
914 await waitFor(() => harness.sendTurn.mock.calls.length === 1);918 yield* Effect.promise(() => waitFor(() => harness.sendTurn.mock.calls.length === 1));
915919
916 await Effect.runPromise(920 yield* harness.engine.dispatch({
917 harness.engine.dispatch({921 type: "thread.turn.start",
918 type: "thread.turn.start",922 commandId: CommandId.make("cmd-turn-start-restricted-2"),
919 commandId: CommandId.make("cmd-turn-start-restricted-2"),923 threadId: ThreadId.make("thread-1"),
920 threadId: ThreadId.make("thread-1"),924 message: {
921 message: {925 messageId: asMessageId("user-message-restricted-2"),
922 messageId: asMessageId("user-message-restricted-2"),926 role: "user",
923 role: "user",927 text: "second",
924 text: "second",928 attachments: [],
925 attachments: [],929 },
926 },930 modelSelection: {
927 modelSelection: {931 instanceId: ProviderInstanceId.make("codex"),
928 instanceId: ProviderInstanceId.make("codex"),932 model: "gpt-5.1-codex",
929 model: "gpt-5.1-codex",933 },
930 },934 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
931 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,935 runtimeMode: "approval-required",
932 runtimeMode: "approval-required",936 createdAt: now,
933 createdAt: now,937 });
934 }),
935 );
936938
937 await waitFor(async () => {939 yield* Effect.promise(() =>
938 const readModel = await harness.readModel();940 waitFor(async () => {
939 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));941 const readModel = await harness.readModel();
940 return (942 const thread = readModel.threads.find(
941 thread?.activities.some((activity) => activity.kind === "provider.turn.start.failed") ??943 (entry) => entry.id === ThreadId.make("thread-1"),
942 false944 );
943 );945 return (
944 });946 thread?.activities.some(
947 (activity) => activity.kind === "provider.turn.start.failed",
948 ) ?? false
949 );
950 }),
951 );
945952
946 expect(harness.sendTurn).toHaveBeenCalledTimes(1);953 expect(harness.sendTurn).toHaveBeenCalledTimes(1);
947 const readModel = await harness.readModel();954 const readModel = yield* Effect.promise(() => harness.readModel());
948 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));955 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
949 expect(956 expect(
950 thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"),957 thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"),
951 ).toMatchObject({958 ).toMatchObject({
952 payload: {959 payload: {
953 detail: expect.stringContaining("cannot switch models after the conversation has started"),960 detail: expect.stringContaining(
954 },961 "cannot switch models after the conversation has started",
955 });962 ),
956 });963 },
964 });
965 }),
966 );
957967
958 it("starts a first turn on the requested provider instance even when it differs from the thread model", async () => {968 it("starts a first turn on the requested provider instance even when it differs from the thread model", async () => {
959 const harness = await createHarness({969 const harness = await createHarness({
apps/server/src/provider/Layers/GrokAdapter.test.ts+13-12
...@@ -45,20 +45,21 @@ exec ${JSON.stringify(mockAgentCommand)} ${JSON.stringify(mockAgentPath)} "$@"...@@ -45,20 +45,21 @@ exec ${JSON.stringify(mockAgentCommand)} ${JSON.stringify(mockAgentPath)} "$@"
45 return wrapperPath;45 return wrapperPath;
46}46}
4747
48async function waitForFileContent(filePath: string, attempts = 40): Promise<string> {48function waitForFileContent(filePath: string, attempts = 40): Effect.Effect<string> {
49 const readAttempt = async (remainingAttempts: number): Promise<string> => {49 const readAttempt = (remainingAttempts: number): Effect.Effect<string> =>
50 if (remainingAttempts <= 0) {50 Effect.gen(function* () {
51 throw new Error(`Timed out waiting for file content at ${filePath}`);51 if (remainingAttempts <= 0) {
52 }52 return yield* Effect.die(new Error(`Timed out waiting for file content at ${filePath}`));
53 try {53 }
54 const raw = await readFile(filePath, "utf8");54 const raw = yield* Effect.tryPromise(() => readFile(filePath, "utf8")).pipe(
55 Effect.orElseSucceed(() => ""),
56 );
55 if (raw.trim().length > 0) {57 if (raw.trim().length > 0) {
56 return raw;58 return raw;
57 }59 }
58 } catch {}60 yield* Effect.sleep("25 millis");
59 await Effect.runPromise(Effect.sleep("25 millis"));61 return yield* readAttempt(remainingAttempts - 1);
60 return readAttempt(remainingAttempts - 1);62 });
61 };
62 return readAttempt(attempts);63 return readAttempt(attempts);
63}64}
6465
...@@ -169,7 +170,7 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {...@@ -169,7 +170,7 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
169170
170 yield* adapter.stopSession(threadId);171 yield* adapter.stopSession(threadId);
171172
172 const exitLog = yield* Effect.promise(() => waitForFileContent(exitLogPath));173 const exitLog = yield* waitForFileContent(exitLogPath);
173 assert.include(exitLog, "SIGTERM");174 assert.include(exitLog, "SIGTERM");
174 }),175 }),
175 );176 );
apps/server/src/provider/Layers/GrokProvider.test.ts+54-61
...@@ -1,67 +1,60 @@...@@ -1,67 +1,60 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";4import * as FileSystem from "effect/FileSystem";
4import * as Path from "effect/Path";5import * as Path from "effect/Path";
5import * as Schema from "effect/Schema";6import * as Schema from "effect/Schema";
6import { describe, expect, it } from "vite-plus/test";
7import { ChildProcessSpawner } from "effect/unstable/process";
8import { GrokSettings } from "@t3tools/contracts";7import { GrokSettings } from "@t3tools/contracts";
98
10import { buildInitialGrokProviderSnapshot, checkGrokProviderStatus } from "./GrokProvider.ts";9import { buildInitialGrokProviderSnapshot, checkGrokProviderStatus } from "./GrokProvider.ts";
1110
12const decodeGrokSettings = Schema.decodeSync(GrokSettings);11const decodeGrokSettings = Schema.decodeSync(GrokSettings);
1312
14const runNode = <A, E>(
15 effect: Effect.Effect<
16 A,
17 E,
18 FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
19 >,
20): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)));
21
22describe("buildInitialGrokProviderSnapshot", () => {13describe("buildInitialGrokProviderSnapshot", () => {
23 it("returns a disabled snapshot when settings.enabled is false", async () => {14 it.effect("returns a disabled snapshot when settings.enabled is false", () =>
24 const snapshot = await Effect.runPromise(15 Effect.gen(function* () {
25 buildInitialGrokProviderSnapshot(decodeGrokSettings({ enabled: false })),16 const snapshot = yield* buildInitialGrokProviderSnapshot(
26 );17 decodeGrokSettings({ enabled: false }),
27 expect(snapshot.enabled).toBe(false);18 );
28 expect(snapshot.status).toBe("disabled");19 expect(snapshot.enabled).toBe(false);
29 expect(snapshot.installed).toBe(false);20 expect(snapshot.status).toBe("disabled");
30 expect(snapshot.message).toContain("disabled");21 expect(snapshot.installed).toBe(false);
31 });22 expect(snapshot.message).toContain("disabled");
23 }),
24 );
3225
33 it("returns a pending snapshot by default", async () => {26 it.effect("returns a pending snapshot by default", () =>
34 const snapshot = await Effect.runPromise(27 Effect.gen(function* () {
35 buildInitialGrokProviderSnapshot(decodeGrokSettings({})),28 const snapshot = yield* buildInitialGrokProviderSnapshot(decodeGrokSettings({}));
36 );29 expect(snapshot.enabled).toBe(true);
37 expect(snapshot.enabled).toBe(true);30 expect(snapshot.installed).toBe(true);
38 expect(snapshot.installed).toBe(true);31 expect(snapshot.status).toBe("warning");
39 expect(snapshot.status).toBe("warning");32 expect(snapshot.version).toBeNull();
40 expect(snapshot.version).toBeNull();33 expect(snapshot.message).toContain("Checking Grok");
41 expect(snapshot.message).toContain("Checking Grok");34 expect(snapshot.requiresNewThreadForModelChange).toBe(true);
42 expect(snapshot.requiresNewThreadForModelChange).toBe(true);35 }),
43 });36 );
44});37});
4538
46describe("checkGrokProviderStatus", () => {39it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => {
47 it("reports the binary as missing when the binary path does not resolve", async () => {40 it.effect("reports the binary as missing when the binary path does not resolve", () =>
48 const snapshot = await runNode(41 Effect.gen(function* () {
49 checkGrokProviderStatus(42 const snapshot = yield* checkGrokProviderStatus(
50 decodeGrokSettings({43 decodeGrokSettings({
51 enabled: true,44 enabled: true,
52 binaryPath: "/definitely/not/installed/grok-binary",45 binaryPath: "/definitely/not/installed/grok-binary",
53 }),46 }),
54 ),47 );
55 );48 expect(snapshot.enabled).toBe(true);
56 expect(snapshot.enabled).toBe(true);49 expect(snapshot.installed).toBe(false);
57 expect(snapshot.installed).toBe(false);50 expect(snapshot.status).toBe("error");
58 expect(snapshot.status).toBe("error");51 expect(snapshot.message).toMatch(/not installed|not on PATH|Failed to execute/);
59 expect(snapshot.message).toMatch(/not installed|not on PATH|Failed to execute/);52 }),
60 });53 );
6154
62 it("reports an installed CLI as unhealthy when --version exits non-zero", async () => {55 it.effect("reports an installed CLI as unhealthy when --version exits non-zero", () =>
63 const snapshot = await runNode(56 Effect.gen(function* () {
64 Effect.scoped(57 const snapshot = yield* Effect.scoped(
65 Effect.gen(function* () {58 Effect.gen(function* () {
66 const fs = yield* FileSystem.FileSystem;59 const fs = yield* FileSystem.FileSystem;
67 const path = yield* Path.Path;60 const path = yield* Path.Path;
...@@ -77,18 +70,18 @@ describe("checkGrokProviderStatus", () => {...@@ -77,18 +70,18 @@ describe("checkGrokProviderStatus", () => {
77 decodeGrokSettings({ enabled: true, binaryPath: grokPath }),70 decodeGrokSettings({ enabled: true, binaryPath: grokPath }),
78 );71 );
79 }),72 }),
80 ),73 );
81 );
8274
83 expect(snapshot.enabled).toBe(true);75 expect(snapshot.enabled).toBe(true);
84 expect(snapshot.installed).toBe(true);76 expect(snapshot.installed).toBe(true);
85 expect(snapshot.status).toBe("error");77 expect(snapshot.status).toBe("error");
86 expect(snapshot.message).toContain("broken grok install");78 expect(snapshot.message).toContain("broken grok install");
87 });79 }),
80 );
8881
89 it("reports an error when ACP model discovery is unavailable", async () => {82 it.effect("reports an error when ACP model discovery is unavailable", () =>
90 const snapshot = await runNode(83 Effect.gen(function* () {
91 Effect.scoped(84 const snapshot = yield* Effect.scoped(
92 Effect.gen(function* () {85 Effect.gen(function* () {
93 const fs = yield* FileSystem.FileSystem;86 const fs = yield* FileSystem.FileSystem;
94 const path = yield* Path.Path;87 const path = yield* Path.Path;
...@@ -104,12 +97,12 @@ describe("checkGrokProviderStatus", () => {...@@ -104,12 +97,12 @@ describe("checkGrokProviderStatus", () => {
104 decodeGrokSettings({ enabled: true, binaryPath: grokPath }),97 decodeGrokSettings({ enabled: true, binaryPath: grokPath }),
105 );98 );
106 }),99 }),
107 ),100 );
108 );
109101
110 expect(snapshot.status).toBe("error");102 expect(snapshot.status).toBe("error");
111 expect(snapshot.installed).toBe(true);103 expect(snapshot.installed).toBe(true);
112 expect(snapshot.models.map((model) => model.slug)).toEqual(["grok-build"]);104 expect(snapshot.models.map((model) => model.slug)).toEqual(["grok-build"]);
113 expect(snapshot.message).toContain("ACP startup failed");105 expect(snapshot.message).toContain("ACP startup failed");
114 });106 }),
107 );
115});108});
apps/server/src/provider/acp/GrokAcpSupport.test.ts+37-37
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "@effect/vitest";
1import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
2import * as EffectAcpErrors from "effect-acp/errors";3import * as EffectAcpErrors from "effect-acp/errors";
3import { describe, expect, it } from "vite-plus/test";
44
5import {5import {
6 applyGrokAcpModelSelection,6 applyGrokAcpModelSelection,
...@@ -49,61 +49,61 @@ describe("applyGrokAcpModelSelection", () => {...@@ -49,61 +49,61 @@ describe("applyGrokAcpModelSelection", () => {
49 return { runtime, modelCalls };49 return { runtime, modelCalls };
50 };50 };
5151
52 it("calls session/set_model when the requested model differs from current", async () => {52 it.effect("calls session/set_model when the requested model differs from current", () =>
53 const { runtime, modelCalls } = makeRecordingRuntime();53 Effect.gen(function* () {
54 const result = await Effect.runPromise(54 const { runtime, modelCalls } = makeRecordingRuntime();
55 applyGrokAcpModelSelection({55 const result = yield* applyGrokAcpModelSelection({
56 runtime,56 runtime,
57 currentModelId: "grok-build",57 currentModelId: "grok-build",
58 requestedModelId: "grok-mock-alt",58 requestedModelId: "grok-mock-alt",
59 mapError: (cause) => cause.message,59 mapError: (cause) => cause.message,
60 }),60 });
61 );61 expect(modelCalls).toEqual(["grok-mock-alt"]);
62 expect(modelCalls).toEqual(["grok-mock-alt"]);62 expect(result).toBe("grok-mock-alt");
63 expect(result).toBe("grok-mock-alt");63 }),
64 });64 );
6565
66 it("skips set_model when requested matches current", async () => {66 it.effect("skips set_model when requested matches current", () =>
67 const { runtime, modelCalls } = makeRecordingRuntime();67 Effect.gen(function* () {
68 const result = await Effect.runPromise(68 const { runtime, modelCalls } = makeRecordingRuntime();
69 applyGrokAcpModelSelection({69 const result = yield* applyGrokAcpModelSelection({
70 runtime,70 runtime,
71 currentModelId: "grok-build",71 currentModelId: "grok-build",
72 requestedModelId: "grok-build",72 requestedModelId: "grok-build",
73 mapError: (cause) => cause.message,73 mapError: (cause) => cause.message,
74 }),74 });
75 );75 expect(modelCalls).toEqual([]);
76 expect(modelCalls).toEqual([]);76 expect(result).toBe("grok-build");
77 expect(result).toBe("grok-build");77 }),
78 });78 );
7979
80 it("skips set_model when no model is requested", async () => {80 it.effect("skips set_model when no model is requested", () =>
81 const { runtime, modelCalls } = makeRecordingRuntime();81 Effect.gen(function* () {
82 const result = await Effect.runPromise(82 const { runtime, modelCalls } = makeRecordingRuntime();
83 applyGrokAcpModelSelection({83 const result = yield* applyGrokAcpModelSelection({
84 runtime,84 runtime,
85 currentModelId: "grok-build",85 currentModelId: "grok-build",
86 requestedModelId: undefined,86 requestedModelId: undefined,
87 mapError: (cause) => cause.message,87 mapError: (cause) => cause.message,
88 }),88 });
89 );89 expect(modelCalls).toEqual([]);
90 expect(modelCalls).toEqual([]);90 expect(result).toBe("grok-build");
91 expect(result).toBe("grok-build");91 }),
92 });92 );
9393
94 it("propagates session/set_model failures via mapError", async () => {94 it.effect("propagates session/set_model failures via mapError", () =>
95 const failure = EffectAcpErrors.AcpRequestError.invalidParams("session id not known");95 Effect.gen(function* () {
96 const { runtime } = makeRecordingRuntime(failure);96 const failure = EffectAcpErrors.AcpRequestError.invalidParams("session id not known");
97 const error = await Effect.runPromise(97 const { runtime } = makeRecordingRuntime(failure);
98 Effect.flip(98 const error = yield* Effect.flip(
99 applyGrokAcpModelSelection({99 applyGrokAcpModelSelection({
100 runtime,100 runtime,
101 currentModelId: "grok-build",101 currentModelId: "grok-build",
102 requestedModelId: "grok-mock-alt",102 requestedModelId: "grok-mock-alt",
103 mapError: (cause) => cause.message,103 mapError: (cause) => cause.message,
104 }),104 }),
105 ),105 );
106 );106 expect(error).toBe(failure.message);
107 expect(error).toBe(failure.message);107 }),
108 });108 );
109});109});