Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions apps/server/src/provider/CodexInstallation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import * as Exit from "effect/Exit";
import * as Queue from "effect/Queue";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import { HttpClient, HttpClientResponse } from "effect/http";
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http";
import * as TestClock from "effect/testing/TestClock";
import { ChildProcess, ChildProcessSpawner } from "effect/process";
import * as CodexInstallation from "./CodexInstallation.ts";

Expand All @@ -31,7 +32,8 @@ const makeHarness = Effect.fn("test.makeCodexInstallation")(function* (
input: {
options?: Partial<CodexInstallation.CodexInstallationOptions>;
manifestCurrent?: Effect.Effect<ModelManifest.ModelManifestData>;
body?: Stream.Stream<Uint8Array>;
body?: Stream.Stream<Uint8Array, HttpClientError.HttpClientError>;
downloadStatus?: number;
baseDir?: string;
local?: { version: string; appServerFails?: boolean; versionFails?: boolean };
platform?: NodeJS.Platform;
Expand Down Expand Up @@ -106,7 +108,10 @@ const makeHarness = Effect.fn("test.makeCodexInstallation")(function* (
Effect.sync(() => {
downloads++;
return Object.defineProperty(
HttpClientResponse.fromWeb(request, new Response(null)),
HttpClientResponse.fromWeb(
request,
new Response(null, { status: input.downloadStatus ?? 200 }),
),
"stream",
{
value: input.body ?? Stream.succeed(archive),
Expand Down Expand Up @@ -366,6 +371,81 @@ it.effect("rejects corrupted downloads without publishing a runtime", () =>
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect.each(["interrupted body", "HTTP 404"] as const)(
"reports a download failure for %s without blaming disk access",
(failure) =>
Effect.gen(function* () {
const request = HttpClientRequest.get(asset.url);
const response = HttpClientResponse.fromWeb(request, new Response(null));
const h = yield* makeHarness(
failure === "HTTP 404"
? { downloadStatus: 404 }
: {
body: Stream.concat(
Stream.succeed(archive.subarray(0, 10)),
Stream.fail(
new HttpClientError.HttpClientError({
reason: new HttpClientError.DecodeError({
request,
response,
cause: new TypeError("terminated"),
}),
}),
),
),
},
);
yield* h.installation.start;
expect(yield* terminalState(h.installation)).toMatchObject({
phase: "failed",
message:
failure === "HTTP 404"
? "The Codex download server returned HTTP 404. Try again later."
: "The Codex download was interrupted. Check your internet connection, then retry installation.",
});
expect(h.downloads()).toBe(1);
expect(yield* h.fs.readDirectory(h.installation.managedDirectory)).toEqual([]);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect(
"preserves the filesystem failure message when installation cannot create its directory",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-install-test-" });
const baseDir = `${directory}/not-a-directory`;
yield* fs.writeFileString(baseDir, "existing file");
const h = yield* makeHarness({ baseDir });
yield* h.installation.start;
expect(yield* terminalState(h.installation)).toMatchObject({
phase: "failed",
message: "Could not install Codex. Check disk space and directory access, then try again.",
});
expect(h.downloads()).toBe(0);
expect(yield* fs.readFileString(baseDir)).toBe("existing file");
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("reports a download timeout and cleans the staging directory", () =>
Effect.gen(function* () {
const started = yield* Deferred.make<void>();
const stall = Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never));
const h = yield* makeHarness({
body: Stream.concat(Stream.succeed(archive.subarray(0, 10)), Stream.fromEffect(stall)),
});
yield* h.installation.start;
yield* Deferred.await(started);
yield* TestClock.adjust("45 minutes");
expect(yield* terminalState(h.installation)).toMatchObject({
phase: "failed",
message: "The Codex download timed out. Retry installation.",
});
expect(h.downloads()).toBe(1);
expect(yield* h.fs.readDirectory(h.installation.managedDirectory)).toEqual([]);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("cancels an in-flight download and cleans the staging directory", () =>
Effect.gen(function* () {
const downloading = yield* Deferred.make<void>();
Expand Down
22 changes: 22 additions & 0 deletions apps/server/src/provider/CodexInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,14 @@ export const makeCodexInstallation = Effect.fn("makeCodexInstallation")(function
),
Stream.run(fs.sink(archivePath, { flag: "wx", mode: 0o600 })),
Effect.timeout("45 minutes"),
Effect.catchTags({
TimeoutError: (cause) =>
installationError(
"download",
"The Codex download timed out. Retry installation.",
cause,
),
}),
);
if (downloadedBytes !== release.archiveBytes || hash.digest("hex") !== release.sha256)
return yield* installationError(
Expand Down Expand Up @@ -605,6 +613,20 @@ export const makeCodexInstallation = Effect.fn("makeCodexInstallation")(function
yield* activate();
},
Effect.scoped,
Effect.catchTags({
HttpClientError: (cause) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const httpStatus =
cause.reason._tag === "StatusCodeError" ? cause.reason.response.status : undefined;
return new CodexInstallationError({
operation: "download",
detail:
httpStatus === undefined
? "The Codex download was interrupted. Check your internet connection, then retry installation."
: `The Codex download server returned HTTP ${httpStatus}. Try again later.`,
cause,
});
},
}),
Effect.mapError(
wrapFailure(
"install",
Expand Down
Loading