| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- import fs from "fs/promises"
- import path from "path"
- import { describe, expect } from "bun:test"
- import { Effect, Layer, Schema, Stream } from "effect"
- import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
- import { AbsolutePath } from "@opencode-ai/core/schema"
- import { Npm } from "@opencode-ai/util/npm"
- import { Document, Info } from "@opencode-ai/schema/config"
- import { Config } from "../src/config"
- import { Formatter } from "../src/formatter"
- import { Location } from "../src/location"
- import { location } from "./fixture/location"
- import { tmpdir } from "./fixture/tmpdir"
- import { testEffect } from "./lib/effect"
- const it = testEffect(Layer.empty)
- type ConfigInput = typeof Info.Encoded
- function formatterLayer(directory: string, configured?: ConfigInput["formatter"]) {
- const entries =
- configured === undefined
- ? []
- : [
- new Document({
- type: "document",
- info: Schema.decodeUnknownSync(Info)({ formatter: configured }),
- }),
- ]
- return AppNodeBuilder.build(Formatter.node, [
- [
- Config.node,
- Layer.succeed(
- Config.Service,
- Config.Service.of({
- entries: () => Effect.succeed(entries),
- changes: () => Stream.empty,
- }),
- ),
- ],
- [
- Location.node,
- Layer.succeed(
- Location.Service,
- Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
- ),
- ],
- [Npm.node, Layer.mock(Npm.Service, { which: () => Effect.succeed(undefined) })],
- ])
- }
- function withTemp<A, E, R>(body: (directory: string) => Effect.Effect<A, E, R>) {
- return Effect.acquireUseRelease(
- Effect.promise(() => tmpdir()),
- (tmp) => body(tmp.path),
- (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
- )
- }
- describe("Formatter", () => {
- it.live("does not run formatters marked as disabled in config", () =>
- withTemp((directory) =>
- Effect.gen(function* () {
- const file = path.join(directory, "test.disabled")
- expect(yield* Formatter.Service.use((formatter) => formatter.file(file))).toBe(false)
- }).pipe(
- Effect.provide(
- formatterLayer(directory, {
- disabled: {
- disabled: true,
- command: [process.execPath, "-e", "process.exit(0)", "$FILE"],
- extensions: [".disabled"],
- },
- }),
- ),
- ),
- ),
- )
- it.live("file() returns false when no formatter runs", () =>
- withTemp((directory) =>
- Effect.gen(function* () {
- const file = path.join(directory, "test.txt")
- yield* Effect.promise(() => fs.writeFile(file, "x"))
- expect(yield* Formatter.Service.use((formatter) => formatter.file(file))).toBe(false)
- }).pipe(Effect.provide(formatterLayer(directory, false))),
- ),
- )
- it.live("loads formatter state per directory", () =>
- withTemp((off) =>
- withTemp((on) =>
- Effect.gen(function* () {
- const offFile = path.join(off, "test.isolated")
- const onFile = path.join(on, "test.isolated")
- const disabled = yield* Formatter.Service.use((formatter) => formatter.file(offFile)).pipe(
- Effect.provide(formatterLayer(off, false)),
- )
- const enabled = yield* Formatter.Service.use((formatter) => formatter.file(onFile)).pipe(
- Effect.provide(
- formatterLayer(on, {
- isolated: {
- command: [process.execPath, "-e", "process.exit(0)", "$FILE"],
- extensions: [".isolated"],
- },
- }),
- ),
- )
- expect(disabled).toBe(false)
- expect(enabled).toBe(true)
- }),
- ),
- ),
- )
- it.live("stops after the first matching formatter succeeds", () =>
- withTemp((directory) =>
- Effect.gen(function* () {
- const file = path.join(directory, "test.seq")
- yield* Effect.promise(() => fs.writeFile(file, "x"))
- expect(yield* Formatter.Service.use((formatter) => formatter.file(file))).toBe(true)
- expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe("xA")
- }).pipe(
- Effect.provide(
- formatterLayer(directory, {
- first: {
- command: [
- process.execPath,
- "-e",
- "const fs = require('fs'); const file = process.argv.at(-1); fs.appendFileSync(file, 'A')",
- "$FILE",
- ],
- extensions: [".seq"],
- },
- second: {
- command: [
- process.execPath,
- "-e",
- "const fs = require('fs'); const file = process.argv.at(-1); fs.appendFileSync(file, 'B')",
- "$FILE",
- ],
- extensions: [".seq"],
- },
- }),
- ),
- ),
- ),
- )
- it.live("tries the next matching formatter when the first fails", () =>
- withTemp((directory) =>
- Effect.gen(function* () {
- const file = path.join(directory, "test.fallback")
- yield* Effect.promise(() => fs.writeFile(file, "x"))
- expect(yield* Formatter.Service.use((formatter) => formatter.file(file))).toBe(true)
- expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe("xB")
- }).pipe(
- Effect.provide(
- formatterLayer(directory, {
- first: {
- command: [process.execPath, "-e", "process.exit(1)", "$FILE"],
- extensions: [".fallback"],
- },
- second: {
- command: [
- process.execPath,
- "-e",
- "const fs = require('fs'); const file = process.argv.at(-1); fs.appendFileSync(file, 'B')",
- "$FILE",
- ],
- extensions: [".fallback"],
- },
- }),
- ),
- ),
- ),
- )
- })
|