import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientResponse } from "effect/unstable/http"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import * as Generator from "./generate.ts"; const isGeneratorFetchError = Schema.is(Generator.GeneratorFetchError); const isGeneratorDirectoryDecodeError = Schema.is(Generator.GeneratorDirectoryDecodeError); const isGeneratorSchemaDocumentDecodeError = Schema.is( Generator.GeneratorSchemaDocumentDecodeError, ); const isGeneratorFormatExitError = Schema.is(Generator.GeneratorFormatExitError); const isGeneratorSchemaNameParseError = Schema.is(Generator.GeneratorSchemaNameParseError); const isGeneratorSchemaValueDeclarationMissingError = Schema.is( Generator.GeneratorSchemaValueDeclarationMissingError, ); const httpClient = (response: Response) => HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, response))); function processHandle(exitCode: number) { return ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(1), exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)), isRunning: Effect.succeed(false), kill: () => Effect.void, unref: Effect.succeed(Effect.void), stdin: Sink.drain, stdout: Stream.empty, stderr: Stream.empty, all: Stream.empty, getInputFd: () => Sink.drain, getOutputFd: () => Stream.empty, }); } describe("Codex schema generator errors", () => { it.effect("retains safe URL diagnostics and the HTTP cause when fetching fails", () => Effect.gen(function* () { const url = "https://generator-user:generator-password@example.test/private/schema.json?token=generator-secret#fragment"; const error = yield* Generator.fetchText(url).pipe( Effect.provideService( HttpClient.HttpClient, httpClient(new Response("unavailable", { status: 503 })), ), Effect.flip, ); assert(isGeneratorFetchError(error)); expect(error).toMatchObject({ urlInputLength: url.length, urlProtocol: "https:", urlHostname: "example.test", }); expect(error).not.toHaveProperty("url"); expect(error.stage).toBe("request"); expect(error.cause).toBeDefined(); const { cause: _, ...directDiagnostics } = error; expect(directDiagnostics).not.toHaveProperty("url"); expect(directDiagnostics.urlProtocol).toBe("https:"); expect(directDiagnostics.urlHostname).toBe("example.test"); expect(error.message).not.toMatch( /generator-user|generator-password|private|token|generator-secret|fragment/, ); }), ); it.effect("adds the GitHub directory path to listing decode failures", () => Effect.gen(function* () { const error = yield* Generator.fetchDirectoryEntries("schema/json/v2").pipe( Effect.provideService( HttpClient.HttpClient, httpClient(new Response("not-json", { status: 200 })), ), Effect.flip, ); assert(isGeneratorDirectoryDecodeError(error)); expect(error.directoryPath).toBe("schema/json/v2"); expect(error).toMatchObject({ urlProtocol: "https:", urlHostname: "api.github.com", }); expect(error.urlInputLength).toBeGreaterThan(0); expect(error).not.toHaveProperty("url"); expect(error.cause).toBeDefined(); expect(error.message).toContain("schema/json/v2"); }), ); it.effect("adds repository and download context to schema decode failures", () => Effect.gen(function* () { const repositoryPath = "codex-rs/app-server-protocol/schema/json/v2/Thread.json"; const url = "https://raw.example.test/Thread.json"; const error = yield* Generator.decodeSchemaDocument({ repositoryPath, url, raw: "not-json", }).pipe(Effect.flip); assert(isGeneratorSchemaDocumentDecodeError(error)); expect(error.repositoryPath).toBe(repositoryPath); expect(error).toMatchObject({ urlInputLength: url.length, urlProtocol: "https:", urlHostname: "raw.example.test", }); expect(error).not.toHaveProperty("url"); expect(error.cause).toBeDefined(); expect(error.message).toContain(repositoryPath); }), ); it.effect("reports formatter commands and nonzero exit codes structurally", () => { let spawned: ChildProcess.StandardCommand | undefined; const spawner = ChildProcessSpawner.make((command) => { if (ChildProcess.isStandardCommand(command)) { spawned = command; } return Effect.succeed(processHandle(17)); }); return Effect.gen(function* () { const generatedDir = "/tmp/codex-generated"; const error = yield* Generator.formatGeneratedFiles(generatedDir).pipe(Effect.flip); assert(isGeneratorFormatExitError(error)); expect(error.command).toBe("vp"); expect(error.argumentCount).toBe(3); expect(error).not.toHaveProperty("args"); expect(error.generatedDir).toBe(generatedDir); expect(error.exitCode).toBe(17); expect(error.message).toContain("17"); expect(spawned?.command).toBe("vp"); expect(spawned?.args).toEqual(["fmt", generatedDir, "--write"]); }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner)); }); it.effect("returns malformed generated schema declarations as typed failures", () => Effect.gen(function* () { const missingValueError = yield* Generator.collectSchemaEntries( "export type Session = string;", ).pipe(Effect.flip); assert(isGeneratorSchemaValueDeclarationMissingError(missingValueError)); expect(missingValueError).toMatchObject({ lineIndex: 0, typeDeclarationLength: 29, nextLinePresent: false, }); expect(missingValueError).not.toHaveProperty("typeDeclaration"); const nameParseError = yield* Generator.collectSchemaEntries( "export type @ = string;\nexport const invalid = Schema.String;", ).pipe(Effect.flip); assert(isGeneratorSchemaNameParseError(nameParseError)); expect(nameParseError).toMatchObject({ lineIndex: 0, typeDeclarationLength: 23, }); expect(nameParseError).not.toHaveProperty("typeDeclaration"); }), ); });