import { describe, expect } from "bun:test" import { Effect, FileSystem, Option } from "effect" import { write, type Output } from "../src" import { it } from "./effect" describe("HttpApiCodegen.write", () => { it.effect("writes compiled files beneath the output directory", () => { const writes: Array<{ readonly path: string; readonly content: string }> = [] const output: Output = { operations: [], files: [{ path: "session.ts", content: "export const session = {}" }], } return Effect.gen(function* () { yield* write(output, "/generated") expect(writes).toEqual([ { path: "/generated/session.ts", content: "export const session = {}\n" }, { path: "/generated/.httpapi-codegen.json", content: '[\n "session.ts"\n]\n' }, ]) }).pipe( Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ exists: () => Effect.succeed(false), makeDirectory: () => Effect.void, writeFileString: (path, content) => { writes.push({ path, content }) return Effect.void }, }), ), ) }) it.effect("removes only stale files owned by the previous manifest", () => { const removed: Array = [] return write( { operations: [], files: [{ path: "session.ts", content: "" }], }, "/generated", ).pipe( Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ exists: (path) => Effect.succeed(path.endsWith(".httpapi-codegen.json")), makeDirectory: () => Effect.void, readFileString: () => Effect.succeed('["old.ts", "session.ts"]'), remove: (path) => { removed.push(path) return Effect.void }, writeFileString: () => Effect.void, }), ), Effect.tap(() => Effect.sync(() => expect(removed).toEqual(["/generated/old.ts"]))), ) }) it.effect("rejects unsafe and duplicate output paths before writing", () => { const writes: Array = [] return Effect.gen(function* () { const error = yield* write( { operations: [], files: [ { path: "../outside.ts", content: "" }, { path: "client.ts", content: "" }, { path: "CLIENT.ts", content: "" }, ], }, "/generated", ).pipe(Effect.flip) expect(error._tag).toBe("GenerationError") expect(writes).toEqual([]) }).pipe( Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ writeFileString: (path) => { writes.push(path) return Effect.void }, }), ), ) }) it.effect("rejects case-insensitive duplicate output paths", () => { const writes: Array = [] return Effect.gen(function* () { const error = yield* write( { operations: [], files: [ { path: "client.ts", content: "" }, { path: "CLIENT.ts", content: "" }, ], }, "/generated", ).pipe(Effect.flip) expect(error._tag).toBe("GenerationError") expect(error.reason).toBe("Duplicate output path: CLIENT.ts") expect(writes).toEqual([]) }).pipe( Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ writeFileString: (path) => { writes.push(path) return Effect.void }, }), ), ) }) it.effect("reserves the private manifest path", () => write({ operations: [], files: [{ path: ".httpapi-codegen.json", content: "" }] }, "/generated").pipe( Effect.flip, Effect.tap((error) => Effect.sync(() => expect(error.reason).toContain("Unsafe output path"))), Effect.provideService(FileSystem.FileSystem, FileSystem.makeNoop({})), ), ) it.effect("rejects existing symbolic-link output targets", () => write({ operations: [], files: [{ path: "session.ts", content: "" }] }, "/generated").pipe( Effect.flip, Effect.tap((error) => Effect.sync(() => expect(error.reason).toBe("Unsafe output path: session.ts"))), Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ exists: (path) => Effect.succeed(path.endsWith("session.ts")), makeDirectory: () => Effect.void, stat: () => Effect.succeed({ type: "SymbolicLink", mtime: Option.none(), atime: Option.none(), birthtime: Option.none(), dev: 0, ino: Option.none(), mode: 0, nlink: Option.none(), uid: Option.none(), gid: Option.none(), rdev: Option.none(), size: FileSystem.Size(0), blksize: Option.none(), blocks: Option.none(), }), }), ), ), ) })