import { afterEach, describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import path from "path"
import fs from "fs/promises"
import { SpecWriteTool } from "../../src/tool/spec"
import { Tool } from "@/tool/tool"
import { SessionID, MessageID } from "../../src/session/schema"
import { Agent } from "../../src/agent/agent"
import { Truncate } from "@/tool/truncate"
import { LSP } from "@/lsp/lsp"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Bus } from "../../src/bus"
import { Format } from "../../src/format"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
import { TestInstance, disposeAllInstances } from "../fixture/fixture"
const ctx = {
sessionID: SessionID.make("ses_test-spec-write"),
messageID: MessageID.make("msg_test-spec"),
callID: "",
agent: "build",
abort: AbortSignal.any([]),
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
}
afterEach(async () => {
await disposeAllInstances()
})
const it = testEffect(
Layer.mergeAll(
LSP.defaultLayer,
AppFileSystem.defaultLayer,
Bus.layer,
Format.defaultLayer,
CrossSpawnSpawner.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
),
)
const init = Effect.fn("SpecWriteToolTest.init")(function* () {
const info = yield* SpecWriteTool
return yield* info.init()
})
const run = Effect.fn("SpecWriteToolTest.run")(function* (
args: Tool.InferParameters<typeof SpecWriteTool>,
next: Tool.Context = ctx,
) {
const tool = yield* init()
return yield* tool.execute(args, next).pipe(Effect.timeout("10 seconds"))
})
describe("tool.spec_write", () => {
describe("new file creation", () => {
it.instance("writes spec.md to target path", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const target = path.join(test.directory, "spec", "default", "spec.md")
const result = yield* run({ filePath: target, content: "# Feature Specification: Auth\n\n## Overview\n\noverview" })
const content = yield* Effect.promise(() => fs.readFile(target, "utf-8"))
expect(content).toBe("# Feature Specification: Auth\n\n## Overview\n\noverview")
expect(result.title).toBe("Spec Artifact Written")
expect(result.metadata.filepath).toBe(target)
expect(result.output).toContain("Wrote file successfully.")
}),
)
it.instance("writes plan.md to target path", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const target = path.join(test.directory, "spec", "default", "plan.md")
const result = yield* run({ filePath: target, content: "# Implementation Plan: Auth\n\n## Summary\n\nsummary" })
const content = yield* Effect.promise(() => fs.readFile(target, "utf-8"))
expect(content).toBe("# Implementation Plan: Auth\n\n## Summary\n\nsummary")
expect(result.title).toBe("Spec Artifact Written")
}),
)
it.instance("writes tasks.md to target path", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const target = path.join(test.directory, "spec", "default", "tasks.md")
const result = yield* run({
filePath: target,
content: "# Tasks: Auth\n\n## Format\n\nformat\n\n## Path Conventions\n\npaths",
})
const content = yield* Effect.promise(() => fs.readFile(target, "utf-8"))
expect(content).toBe("# Tasks: Auth\n\n## Format\n\nformat\n\n## Path Conventions\n\npaths")
expect(result.title).toBe("Spec Artifact Written")
}),
)
it.instance("creates parent directories if needed", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const target = path.join(test.directory, "spec", "default", "spec.md")
yield* run({ filePath: target, content: "test" })
const stats = yield* Effect.promise(() => fs.stat(target))
expect(stats.isFile()).toBe(true)
}),
)
it.instance("resolves relative paths against instance directory", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const result = yield* run({ filePath: "spec/auth/spec.md", content: "relative path content" })
const target = path.join(test.directory, "spec", "auth", "spec.md")
const content = yield* Effect.promise(() => fs.readFile(target, "utf-8"))
expect(content).toBe("relative path content")
expect(result.metadata.filepath).toBe(target)
}),
)
})
describe("document validation integration", () => {
it.instance("returns validation errors for invalid spec", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const target = path.join(test.directory, "spec", "default", "spec.md")
const result = yield* run({
filePath: target,
content: "# Feature Specification: Auth\n\n## Overview\n\noverview",
})
expect(result.output).toContain("Document Section Validation")
expect(result.output).toContain("Missing required sections")
}),
)
it.instance("returns no validation errors for valid spec", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const target = path.join(test.directory, "spec", "default", "spec.md")
const content = `# Feature Specification: Auth
## Overview
overview
## User Scenarios & Testing
story
## Requirements
reqs
## Success Criteria
criteria
## Assumptions
assumptions
## Open Questions
questions
`
const result = yield* run({ filePath: target, content })
expect(result.output).not.toContain("Document Section Validation")
expect(result.output).toContain("Wrote file successfully.")
}),
)
it.instance("returns validation errors for invalid plan", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const target = path.join(test.directory, "spec", "default", "plan.md")
const result = yield* run({
filePath: target,
content: "# Implementation Plan: Auth\n\n## Summary\n\nsummary",
})
expect(result.output).toContain("Document Section Validation")
expect(result.output).toContain("Missing required sections")
}),
)
it.instance("returns validation errors for invalid tasks", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const target = path.join(test.directory, "spec", "default", "tasks.md")
const result = yield* run({
filePath: target,
content: "# Tasks: Auth\n\n## Format\n\nformat",
})
expect(result.output).toContain("Document Section Validation")
expect(result.output).toContain("Missing required sections")
}),
)
})
describe("output display", () => {
it.instance("returns relative path in output", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const target = path.join(test.directory, "spec", "default", "spec.md")
const result = yield* run({ filePath: target, content: "# Feature Specification: Auth\n\n## Overview\n\noverview" })
expect(result.metadata.filepath).toBe(target)
}),
)
})
})