| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- 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 { 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 Config.Info.Encoded
- function formatterLayer(directory: string, configured?: ConfigInput["formatter"]) {
- const entries =
- configured === undefined
- ? []
- : [
- new Config.Document({
- type: "document",
- info: Schema.decodeUnknownSync(Config.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("status() returns empty list when no formatters are configured", () =>
- withTemp((directory) =>
- Formatter.Service.use((formatter) => formatter.status()).pipe(Effect.provide(formatterLayer(directory))),
- ),
- )
- it.live("status() returns built-in formatters when formatter is true", () =>
- withTemp((directory) =>
- Formatter.Service.use((formatter) =>
- Effect.gen(function* () {
- const statuses = yield* formatter.status()
- const gofmt = statuses.find((item) => item.name === "gofmt")
- expect(gofmt).toBeDefined()
- expect(gofmt?.extensions).toContain(".go")
- }),
- ).pipe(Effect.provide(formatterLayer(directory, true))),
- ),
- )
- it.live("status() keeps built-in formatters when config object is provided", () =>
- withTemp((directory) =>
- Formatter.Service.use((formatter) =>
- Effect.gen(function* () {
- const statuses = yield* formatter.status()
- expect(statuses.find((item) => item.name === "gofmt")?.extensions).toContain(".go")
- expect(statuses.find((item) => item.name === "mix")).toBeDefined()
- }),
- ).pipe(Effect.provide(formatterLayer(directory, { gofmt: {} }))),
- ),
- )
- it.live("status() excludes formatters marked as disabled in config", () =>
- withTemp((directory) =>
- Formatter.Service.use((formatter) =>
- Effect.gen(function* () {
- const statuses = yield* formatter.status()
- expect(statuses.find((item) => item.name === "gofmt")).toBeUndefined()
- expect(statuses.find((item) => item.name === "mix")).toBeDefined()
- }),
- ).pipe(Effect.provide(formatterLayer(directory, { gofmt: { disabled: true } }))),
- ),
- )
- it.live("service initializes without error", () =>
- withTemp((directory) =>
- Formatter.Service.use((formatter) => formatter.init()).pipe(Effect.provide(formatterLayer(directory))),
- ),
- )
- 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("status() initializes formatter state per directory", () =>
- Effect.acquireUseRelease(
- Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
- ([off, on]) =>
- Effect.gen(function* () {
- const disabled = yield* Formatter.Service.use((formatter) => formatter.status()).pipe(
- Effect.provide(formatterLayer(off.path, false)),
- )
- const enabled = yield* Formatter.Service.use((formatter) => formatter.status()).pipe(
- Effect.provide(formatterLayer(on.path, true)),
- )
- expect(disabled).toEqual([])
- expect(enabled.find((item) => item.name === "gofmt")).toBeDefined()
- }),
- (directories) =>
- Effect.promise(() => Promise.all(directories.map((tmp) => tmp[Symbol.asyncDispose]())).then(() => undefined)),
- ),
- )
- 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"],
- },
- }),
- ),
- ),
- ),
- )
- })
|