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