mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 05:43:55 +00:00
- Rename packages/opencode → packages/tfcode (directory only) - Rename bin/opencode → bin/tfcode (CLI binary) - Rename .opencode → .tfcode (config directory) - Update package.json name and bin field - Update config directory path references (.tfcode) - Keep internal code references as 'opencode' for easy upstream sync - Keep @opencode-ai/* workspace package names This minimal branding approach allows clean merges from upstream opencode repository while providing tfcode branding for users.
162 lines
6.4 KiB
TypeScript
162 lines
6.4 KiB
TypeScript
import { describe, test, expect } from "bun:test"
|
|
import { NodeFileSystem } from "@effect/platform-node"
|
|
import { Effect, FileSystem, Layer } from "effect"
|
|
import { Truncate, Truncate as TruncateSvc } from "../../src/tool/truncate"
|
|
import { Identifier } from "../../src/id/id"
|
|
import { Process } from "../../src/util/process"
|
|
import { Filesystem } from "../../src/util/filesystem"
|
|
import path from "path"
|
|
import { testEffect } from "../lib/effect"
|
|
import { writeFileStringScoped } from "../lib/filesystem"
|
|
|
|
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
|
|
const ROOT = path.resolve(import.meta.dir, "..", "..")
|
|
|
|
describe("Truncate", () => {
|
|
describe("output", () => {
|
|
test("truncates large json file by bytes", async () => {
|
|
const content = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json"))
|
|
const result = await Truncate.output(content)
|
|
|
|
expect(result.truncated).toBe(true)
|
|
expect(result.content).toContain("truncated...")
|
|
if (result.truncated) expect(result.outputPath).toBeDefined()
|
|
})
|
|
|
|
test("returns content unchanged when under limits", async () => {
|
|
const content = "line1\nline2\nline3"
|
|
const result = await Truncate.output(content)
|
|
|
|
expect(result.truncated).toBe(false)
|
|
expect(result.content).toBe(content)
|
|
})
|
|
|
|
test("truncates by line count", async () => {
|
|
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
|
|
const result = await Truncate.output(lines, { maxLines: 10 })
|
|
|
|
expect(result.truncated).toBe(true)
|
|
expect(result.content).toContain("...90 lines truncated...")
|
|
})
|
|
|
|
test("truncates by byte count", async () => {
|
|
const content = "a".repeat(1000)
|
|
const result = await Truncate.output(content, { maxBytes: 100 })
|
|
|
|
expect(result.truncated).toBe(true)
|
|
expect(result.content).toContain("truncated...")
|
|
})
|
|
|
|
test("truncates from head by default", async () => {
|
|
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n")
|
|
const result = await Truncate.output(lines, { maxLines: 3 })
|
|
|
|
expect(result.truncated).toBe(true)
|
|
expect(result.content).toContain("line0")
|
|
expect(result.content).toContain("line1")
|
|
expect(result.content).toContain("line2")
|
|
expect(result.content).not.toContain("line9")
|
|
})
|
|
|
|
test("truncates from tail when direction is tail", async () => {
|
|
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n")
|
|
const result = await Truncate.output(lines, { maxLines: 3, direction: "tail" })
|
|
|
|
expect(result.truncated).toBe(true)
|
|
expect(result.content).toContain("line7")
|
|
expect(result.content).toContain("line8")
|
|
expect(result.content).toContain("line9")
|
|
expect(result.content).not.toContain("line0")
|
|
})
|
|
|
|
test("uses default MAX_LINES and MAX_BYTES", () => {
|
|
expect(Truncate.MAX_LINES).toBe(2000)
|
|
expect(Truncate.MAX_BYTES).toBe(50 * 1024)
|
|
})
|
|
|
|
test("large single-line file truncates with byte message", async () => {
|
|
const content = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json"))
|
|
const result = await Truncate.output(content)
|
|
|
|
expect(result.truncated).toBe(true)
|
|
expect(result.content).toContain("bytes truncated...")
|
|
expect(Buffer.byteLength(content, "utf-8")).toBeGreaterThan(Truncate.MAX_BYTES)
|
|
})
|
|
|
|
test("writes full output to file when truncated", async () => {
|
|
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
|
|
const result = await Truncate.output(lines, { maxLines: 10 })
|
|
|
|
expect(result.truncated).toBe(true)
|
|
expect(result.content).toContain("The tool call succeeded but the output was truncated")
|
|
expect(result.content).toContain("Grep")
|
|
if (!result.truncated) throw new Error("expected truncated")
|
|
expect(result.outputPath).toBeDefined()
|
|
expect(result.outputPath).toContain("tool_")
|
|
|
|
const written = await Filesystem.readText(result.outputPath!)
|
|
expect(written).toBe(lines)
|
|
})
|
|
|
|
test("suggests Task tool when agent has task permission", async () => {
|
|
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
|
|
const agent = { permission: [{ permission: "task", pattern: "*", action: "allow" as const }] }
|
|
const result = await Truncate.output(lines, { maxLines: 10 }, agent as any)
|
|
|
|
expect(result.truncated).toBe(true)
|
|
expect(result.content).toContain("Grep")
|
|
expect(result.content).toContain("Task tool")
|
|
})
|
|
|
|
test("omits Task tool hint when agent lacks task permission", async () => {
|
|
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
|
|
const agent = { permission: [{ permission: "task", pattern: "*", action: "deny" as const }] }
|
|
const result = await Truncate.output(lines, { maxLines: 10 }, agent as any)
|
|
|
|
expect(result.truncated).toBe(true)
|
|
expect(result.content).toContain("Grep")
|
|
expect(result.content).not.toContain("Task tool")
|
|
})
|
|
|
|
test("does not write file when not truncated", async () => {
|
|
const content = "short content"
|
|
const result = await Truncate.output(content)
|
|
|
|
expect(result.truncated).toBe(false)
|
|
if (result.truncated) throw new Error("expected not truncated")
|
|
expect("outputPath" in result).toBe(false)
|
|
})
|
|
|
|
test("loads truncate effect in a fresh process", async () => {
|
|
const out = await Process.run([process.execPath, "run", path.join(ROOT, "src", "tool", "truncate.ts")], {
|
|
cwd: ROOT,
|
|
})
|
|
|
|
expect(out.code).toBe(0)
|
|
}, 20000)
|
|
})
|
|
|
|
describe("cleanup", () => {
|
|
const DAY_MS = 24 * 60 * 60 * 1000
|
|
const it = testEffect(Layer.mergeAll(TruncateSvc.defaultLayer, NodeFileSystem.layer))
|
|
|
|
it.effect("deletes files older than 7 days and preserves recent files", () =>
|
|
Effect.gen(function* () {
|
|
const fs = yield* FileSystem.FileSystem
|
|
|
|
yield* fs.makeDirectory(Truncate.DIR, { recursive: true })
|
|
|
|
const old = path.join(Truncate.DIR, Identifier.create("tool", false, Date.now() - 10 * DAY_MS))
|
|
const recent = path.join(Truncate.DIR, Identifier.create("tool", false, Date.now() - 3 * DAY_MS))
|
|
|
|
yield* writeFileStringScoped(old, "old content")
|
|
yield* writeFileStringScoped(recent, "recent content")
|
|
yield* TruncateSvc.Service.use((s) => s.cleanup())
|
|
|
|
expect(yield* fs.exists(old)).toBe(false)
|
|
expect(yield* fs.exists(recent)).toBe(true)
|
|
}),
|
|
)
|
|
})
|
|
})
|