1import * as NodeServices from "@effect/platform-node/NodeServices";
2import * as ModelManifest from "./ModelManifest.ts";
3import { expect, it } from "@effect/vitest";
4import * as HostProcess from "@t3tools/shared/HostProcess";
5import * as Crypto from "effect/Crypto";
6import * as Deferred from "effect/Deferred";
7import * as Effect from "effect/Effect";
8import * as Hex from "effect/encoding/Hex";
9import * as FileSystem from "effect/FileSystem";
10import * as Option from "effect/Option";
11import * as Exit from "effect/Exit";
12import * as Scope from "effect/Scope";
13import * as Stream from "effect/Stream";
14import { HttpClient, HttpClientResponse } from "effect/http";
15import * as CodexInstallation from "./CodexInstallation.ts";
16
17const archive = Buffer.from(
18 "H4sIAAAAAAAC/+3W0W6DIBQGYB/FcD0sKNSkD7J7aom6tmAQtzXL3n3QZM3qdWVt+n8XoCckJp78wLY3q8bu9Ge2HBbUUp7nYD4H8s9zrNdSsCxnWQLT6JULn8ye09K9h/u2/c0/jSM9xqGzo0+bf3Gdf15WQiD/Kdy61/CA+z8dlO9Wrv2387+c5Z9VZY38p7BY0+Gh8t/sVauLt9Ga9Pnnop7ln1e4/6fxRQ7qZCf/qt3YW0M2OX/JyfvljbCCy3XBSaiGH9VqH4tKuaZbC6qG4aDpTrmP3sQV2nh3Gmxvzqsud0vyjaABAAAAAAAAAAAAAAAk8gOq19rvACgAAA==",
19 "base64",
20);
21const asset = {
22 version: "0.156.1",
23 target: "aarch64-apple-darwin",
24 url: "https://github.com/openai/codex/releases/download/test/package.tar.gz",
25 archiveBytes: archive.length,
26};
27const makeHarness = Effect.fn("test.makeCodexInstallation")(function* (
28 input: {
29 options?: Partial<CodexInstallation.CodexInstallationOptions>;
30 manifestCurrent?: Effect.Effect<ModelManifest.ModelManifestData>;
31 body?: Stream.Stream<Uint8Array>;
32 baseDir?: string;
33 local?: { version: string; appServerFails?: boolean; versionFails?: boolean };
34 } = {},
35) {
36 const fs = yield* FileSystem.FileSystem;
37 const baseDir =
38 input.baseDir ?? (yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-install-test-" }));
39 const localDirectory = `${baseDir}/local`;
40 const localBinaryPath = `${localDirectory}/codex`;
41 const probeLog = `${baseDir}/local-probes.txt`;
42 if (input.local) {
43 yield* fs.makeDirectory(localDirectory, { recursive: true });
44 yield* fs.writeFileString(
45 localBinaryPath,
46 `#!/bin/sh\nprintf '%s\\n' "$*" >> '${probeLog}'\ncase "$1" in\n--version) ${input.local.versionFails ? "exit 1" : `printf '%s\\n' 'codex-cli ${input.local.version}'`};;\napp-server) exit ${input.local.appServerFails ? "1" : "0"};;\nesac\n`,
47 { mode: 0o755 },
48 );
49 }
50 let downloads = 0;
51 const crypto = yield* Crypto.Crypto;
52 const sha256 = Hex.encode(yield* crypto.digest("SHA-256", archive).pipe(Effect.orDie));
53 const installation = yield* CodexInstallation.makeCodexInstallation({
54 baseDir,
55 releaseAsset: { ...asset, sha256 },
56 validate: () => Effect.void,
57 ...input.options,
58 }).pipe(
59 Effect.provideService(ModelManifest.ModelManifest, {
60 current: input.manifestCurrent ?? Effect.succeed(ModelManifest.BUNDLED_MODEL_MANIFEST),
61 refresh: Effect.succeed(ModelManifest.BUNDLED_MODEL_MANIFEST),
62 forceRefresh: Effect.succeed(ModelManifest.BUNDLED_MODEL_MANIFEST),
63 refreshInBackground: Effect.void,
64 }),
65 Effect.provideService(HostProcess.Platform, "darwin"),
66 Effect.provideService(HostProcess.Architecture, "arm64"),
67 Effect.provideService(HostProcess.Environment, { PATH: input.local ? localDirectory : "" }),
68 Effect.provideService(
69 HttpClient.HttpClient,
70 HttpClient.make((request) =>
71 Effect.sync(() => {
72 downloads++;
73 return Object.defineProperty(
74 HttpClientResponse.fromWeb(request, new Response(null)),
75 "stream",
76 {
77 value: input.body ?? Stream.succeed(archive),
78 },
79 );
80 }),
81 ),
82 ),
83 );
84 return { installation, fs, baseDir, localBinaryPath, probeLog, downloads: () => downloads };
85});
86const terminalState = (installation: CodexInstallation.CodexInstallation["Service"]) =>
87 installation.changes.pipe(
88 Stream.filter((state) => ["succeeded", "failed", "cancelled"].includes(state.phase)),
89 Stream.runHead,
90 Effect.map(Option.getOrThrow),
91 );
92
93it.effect.each(["0.156.0", "0.156.1", "0.156.2", "0.157.0"])(
94 "reuses installed Codex %s without downloading or taking ownership of it",
95 (version) =>
96 Effect.gen(function* () {
97 const h = yield* makeHarness({ local: { version } });
98 expect(yield* h.installation.start).toMatchObject({
99 source: "local",
100 phase: "succeeded",
101 installedVersion: version,
102 executablePath: h.localBinaryPath,
103 canRemove: false,
104 });
105 expect(yield* h.installation.resolve()).toMatchObject({
106 source: "local",
107 executablePath: h.localBinaryPath,
108 managedVersionDirectory: null,
109 version,
110 });
111 yield* h.installation.acquire().pipe(Effect.scoped);
112 expect(h.downloads()).toBe(0);
113 expect(yield* h.fs.exists(h.installation.managedDirectory)).toBe(false);
114 expect((yield* h.fs.readFileString(h.probeLog)).trim().split("\n")).toEqual([
115 "--version",
116 "app-server --help",
117 ]);
118 yield* h.installation.remove();
119 expect(yield* h.fs.exists(h.localBinaryPath)).toBe(true);
120 expect((yield* h.installation.state).source).toBe("local");
121 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
122);
123it.effect.each([
124 { version: "0.128.9" },
125 { version: "0.145.0" },
126 { version: "0.155.1" },
127 { version: "0.155.9" },
128 { version: "0.156.1-alpha.1" },
129 { version: "unknown" },
130 { version: "0.156.1", appServerFails: true },
131 { version: "0.156.1", versionFails: true },
132])("downloads the pinned release when the local CLI is unsupported or broken: %j", (local) =>
133 Effect.gen(function* () {
134 const h = yield* makeHarness({ local });
135 expect((yield* h.installation.state).installedVersion).toBeNull();
136 yield* h.installation.start;
137 const installed = yield* terminalState(h.installation);
138 expect(installed.phase).toBe("succeeded");
139 const executable = yield* h.installation.resolve();
140 expect(executable.source).toBe("managed");
141 expect(installed.executablePath).toBe(executable.executablePath);
142 expect(h.downloads()).toBe(1);
143 expect(yield* h.fs.exists(h.localBinaryPath)).toBe(true);
144 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
145);
146it.effect("falls back to a managed download when the reused local executable disappears", () =>
147 Effect.gen(function* () {
148 const h = yield* makeHarness({ local: { version: "0.156.1" } });
149 expect((yield* h.installation.resolve()).source).toBe("local");
150 yield* h.fs.remove(h.localBinaryPath);
151 yield* h.installation.start;
152 expect((yield* terminalState(h.installation)).phase).toBe("succeeded");
153 expect((yield* h.installation.resolve()).source).toBe("managed");
154 expect(h.downloads()).toBe(1);
155 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
156);
157
158it.effect("rechecks compatibility after the local executable is replaced", () =>
159 Effect.gen(function* () {
160 const h = yield* makeHarness({ local: { version: "0.156.1" } });
161 expect((yield* h.installation.resolve()).source).toBe("local");
162 yield* h.fs.remove(h.localBinaryPath);
163 yield* h.fs.writeFileString(h.localBinaryPath, "#!/bin/sh\nprintf 'codex-cli 0.128.9\\n'\n", {
164 mode: 0o755,
165 });
166 yield* h.installation.start;
167 expect((yield* terminalState(h.installation)).phase).toBe("succeeded");
168 expect((yield* h.installation.resolve()).source).toBe("managed");
169 expect(h.downloads()).toBe(1);
170 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
171);
172
173it.effect("rechecks a cached local executable when the shared manifest policy changes", () =>
174 Effect.gen(function* () {
175 let manifest = ModelManifest.BUNDLED_MODEL_MANIFEST;
176 const h = yield* makeHarness({
177 local: { version: "0.156.0" },
178 manifestCurrent: Effect.sync(() => manifest),
179 });
180 expect((yield* h.installation.resolve()).source).toBe("local");
181 manifest = {
182 ...manifest,
183 compatibility: [
184 {
185 driver: "codex",
186 t3CodeRange: ">=0.0.42",
187 ranges: [
188 { range: ">=0.156.1", status: "supported" },
189 { range: "<0.156.1", status: "broken" },
190 ],
191 },
192 ],
193 };
194 yield* h.installation.start;
195 expect((yield* terminalState(h.installation)).phase).toBe("succeeded");
196 expect((yield* h.installation.resolve()).source).toBe("managed");
197 expect(h.downloads()).toBe(1);
198 expect((yield* h.fs.readFileString(h.probeLog)).trim().split("\n")).toEqual([
199 "--version",
200 "app-server --help",
201 ]);
202 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
203);
204
205it.effect("uses bundled Codex compatibility when the remote manifest omits its policy", () =>
206 Effect.gen(function* () {
207 const h = yield* makeHarness({
208 local: { version: "0.156.0" },
209 manifestCurrent: Effect.succeed({
210 ...ModelManifest.BUNDLED_MODEL_MANIFEST,
211 compatibility: [],
212 }),
213 });
214 expect((yield* h.installation.start).source).toBe("local");
215 expect(h.downloads()).toBe(0);
216 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
217);
218
219it.effect("preserves the invoked name of version-manager launcher symlinks", () =>
220 Effect.gen(function* () {
221 const h = yield* makeHarness({ local: { version: "0.156.1" } });
222 const launcher = `${h.baseDir}/launcher`;
223 yield* h.fs.writeFileString(
224 launcher,
225 '#!/bin/sh\ncase "$0" in */codex) ;; *) exit 1;; esac\ncase "$1" in --version) printf "codex-cli 0.156.1\\n";; app-server) exit 0;; esac\n',
226 { mode: 0o755 },
227 );
228 yield* h.fs.remove(h.localBinaryPath);
229 yield* h.fs.symlink(launcher, h.localBinaryPath);
230 expect(yield* h.installation.start).toMatchObject({ source: "local", phase: "succeeded" });
231 expect((yield* h.installation.resolve()).executablePath).toBe(h.localBinaryPath);
232 expect(h.downloads()).toBe(0);
233 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
234);
235
236it.effect(
237 "installs the complete verified package in tools/codex/version and survives a restart",
238 () =>
239 Effect.gen(function* () {
240 const { installation, fs, baseDir } = yield* makeHarness();
241 yield* installation.start;
242 expect((yield* terminalState(installation)).phase).toBe("succeeded");
243 const executable = yield* installation.resolve();
244 expect(executable.executablePath).toBe(`${baseDir}/tools/codex/0.156.1/bin/codex`);
245 expect(
246 yield* fs.readFileString(`${executable.managedVersionDirectory}/bin/codex-code-mode-host`),
247 ).toBe("host");
248 expect(yield* fs.readFileString(`${executable.managedVersionDirectory}/codex-path/rg`)).toBe(
249 "rg",
250 );
251 const restarted = yield* makeHarness({ baseDir });
252 expect((yield* restarted.installation.resolve()).version).toBe("0.156.1");
253 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
254);
255
256it.effect("requires an update before running an older activated managed installation", () =>
257 Effect.gen(function* () {
258 const first = yield* makeHarness();
259 yield* first.installation.start;
260 yield* terminalState(first.installation);
261 const executable = yield* first.installation.resolve();
262 const oldDirectory = `${first.installation.managedDirectory}/0.155.1`;
263 yield* first.fs.rename(executable.managedVersionDirectory!, oldDirectory);
264 for (const name of ["codex-package.json", ".install-complete.json"]) {
265 const file = `${oldDirectory}/${name}`;
266 yield* first.fs.writeFileString(
267 file,
268 (yield* first.fs.readFileString(file)).replaceAll("0.156.1", "0.155.1"),
269 );
270 }
271 yield* first.fs.writeFileString(
272 `${first.installation.managedDirectory}/active.json`,
273 '{"version":"0.155.1"}',
274 );
275 const restarted = yield* makeHarness({ baseDir: first.baseDir });
276 expect((yield* Effect.flip(restarted.installation.resolve())).detail).toContain(
277 "outside the supported range",
278 );
279 yield* restarted.installation.start;
280 expect((yield* terminalState(restarted.installation)).phase).toBe("succeeded");
281 expect((yield* restarted.installation.resolve()).version).toBe("0.156.1");
282 expect(restarted.downloads()).toBe(1);
283 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
284);
285
286it.effect("rejects corrupted downloads without publishing a runtime", () =>
287 Effect.gen(function* () {
288 const { installation, fs } = yield* makeHarness({
289 options: { releaseAsset: { ...asset, sha256: "0".repeat(64) } },
290 });
291 yield* installation.start;
292 const state = yield* terminalState(installation);
293 expect(state.phase).toBe("failed");
294 expect(state.message).toContain("SHA-256");
295 expect(yield* fs.exists(`${installation.managedDirectory}/active.json`)).toBe(false);
296 expect(yield* fs.exists(`${installation.managedDirectory}/0.156.1`)).toBe(false);
297 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
298);
299
300it.effect("cancels an in-flight download and cleans the staging directory", () =>
301 Effect.gen(function* () {
302 const downloading = yield* Deferred.make<void>();
303 const body = Stream.fromEffect(
304 Deferred.succeed(downloading, undefined).pipe(Effect.andThen(Effect.never)),
305 );
306 const { installation, fs } = yield* makeHarness({ body });
307 const started = yield* installation.start;
308 yield* Deferred.await(downloading);
309 expect((yield* installation.cancel(started.operationId!)).phase).toBe("cancelled");
310 expect(
311 (yield* fs.readDirectory(installation.managedDirectory)).filter((name) =>
312 name.startsWith(".install-"),
313 ),
314 ).toEqual([]);
315 expect(yield* fs.exists(`${installation.managedDirectory}/active.json`)).toBe(false);
316 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
317);
318
319it.effect("protects active process leases and removes the runtime after release", () =>
320 Effect.gen(function* () {
321 const { installation, fs } = yield* makeHarness();
322 yield* installation.start;
323 yield* terminalState(installation);
324 const leaseScope = yield* Scope.make();
325 yield* installation.acquire().pipe(Effect.provideService(Scope.Scope, leaseScope));
326 expect((yield* Effect.flip(installation.remove())).detail).toContain("Stop Codex sessions");
327 yield* Scope.close(leaseScope, Exit.void);
328 yield* installation.remove();
329 expect(yield* fs.exists(installation.managedDirectory)).toBe(false);
330 expect((yield* installation.state).installedVersion).toBeNull();
331 expect((yield* installation.state).executablePath).toBeNull();
332 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
333);
334
335it.effect("keeps an activated runtime when a later update fails verification", () =>
336 Effect.gen(function* () {
337 const first = yield* makeHarness();
338 yield* first.installation.start;
339 yield* terminalState(first.installation);
340 const update = yield* makeHarness({
341 baseDir: first.baseDir,
342 options: { releaseAsset: { ...asset, version: "0.156.2", sha256: "0".repeat(64) } },
343 });
344 expect((yield* update.installation.state).executablePath).toBe(
345 (yield* first.installation.resolve()).executablePath,
346 );
347 yield* update.installation.start;
348 expect((yield* terminalState(update.installation)).phase).toBe("failed");
349 expect((yield* update.installation.resolve()).version).toBe("0.156.1");
350 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
351);
352
353it("publishes complete packages for all supported platforms", () => {
354 for (const platform of ["darwin", "linux", "win32"] as const)
355 for (const arch of ["x64", "arm64"])
356 expect(CodexInstallation.resolveCodexReleaseAsset(platform, arch)?.url).toContain(
357 "codex-package-",
358 );
359 expect(CodexInstallation.resolveCodexReleaseAsset("linux", "riscv64")).toBeNull();
360});