mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-04 08:03:14 +00:00
refactor: apply minimal tfcode branding
- 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.
This commit is contained in:
14
packages/tfcode/test/util/data-url.test.ts
Normal file
14
packages/tfcode/test/util/data-url.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { decodeDataUrl } from "../../src/util/data-url"
|
||||
|
||||
describe("decodeDataUrl", () => {
|
||||
test("decodes base64 data URLs", () => {
|
||||
const body = '{\n "ok": true\n}\n'
|
||||
const url = `data:text/plain;base64,${Buffer.from(body).toString("base64")}`
|
||||
expect(decodeDataUrl(url)).toBe(body)
|
||||
})
|
||||
|
||||
test("decodes plain data URLs", () => {
|
||||
expect(decodeDataUrl("data:text/plain,hello%20world")).toBe("hello world")
|
||||
})
|
||||
})
|
||||
61
packages/tfcode/test/util/effect-zod.test.ts
Normal file
61
packages/tfcode/test/util/effect-zod.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { zod } from "../../src/util/effect-zod"
|
||||
|
||||
describe("util.effect-zod", () => {
|
||||
test("converts class schemas for route dto shapes", () => {
|
||||
class Method extends Schema.Class<Method>("ProviderAuthMethod")({
|
||||
type: Schema.Union([Schema.Literal("oauth"), Schema.Literal("api")]),
|
||||
label: Schema.String,
|
||||
}) {}
|
||||
|
||||
const out = zod(Method)
|
||||
|
||||
expect(out.meta()?.ref).toBe("ProviderAuthMethod")
|
||||
expect(
|
||||
out.parse({
|
||||
type: "oauth",
|
||||
label: "OAuth",
|
||||
}),
|
||||
).toEqual({
|
||||
type: "oauth",
|
||||
label: "OAuth",
|
||||
})
|
||||
})
|
||||
|
||||
test("converts structs with optional fields, arrays, and records", () => {
|
||||
const out = zod(
|
||||
Schema.Struct({
|
||||
foo: Schema.optional(Schema.String),
|
||||
bar: Schema.Array(Schema.Number),
|
||||
baz: Schema.Record(Schema.String, Schema.Boolean),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(
|
||||
out.parse({
|
||||
bar: [1, 2],
|
||||
baz: { ok: true },
|
||||
}),
|
||||
).toEqual({
|
||||
bar: [1, 2],
|
||||
baz: { ok: true },
|
||||
})
|
||||
expect(
|
||||
out.parse({
|
||||
foo: "hi",
|
||||
bar: [1],
|
||||
baz: { ok: false },
|
||||
}),
|
||||
).toEqual({
|
||||
foo: "hi",
|
||||
bar: [1],
|
||||
baz: { ok: false },
|
||||
})
|
||||
})
|
||||
|
||||
test("throws for unsupported tuple schemas", () => {
|
||||
expect(() => zod(Schema.Tuple([Schema.String, Schema.Number]))).toThrow("unsupported effect schema")
|
||||
})
|
||||
})
|
||||
558
packages/tfcode/test/util/filesystem.test.ts
Normal file
558
packages/tfcode/test/util/filesystem.test.ts
Normal file
@@ -0,0 +1,558 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
describe("filesystem", () => {
|
||||
describe("exists()", () => {
|
||||
test("returns true for existing file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
await fs.writeFile(filepath, "content", "utf-8")
|
||||
|
||||
expect(await Filesystem.exists(filepath)).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.txt")
|
||||
|
||||
expect(await Filesystem.exists(filepath)).toBe(false)
|
||||
})
|
||||
|
||||
test("returns true for existing directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const dirpath = path.join(tmp.path, "subdir")
|
||||
await fs.mkdir(dirpath)
|
||||
|
||||
expect(await Filesystem.exists(dirpath)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isDir()", () => {
|
||||
test("returns true for directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const dirpath = path.join(tmp.path, "testdir")
|
||||
await fs.mkdir(dirpath)
|
||||
|
||||
expect(await Filesystem.isDir(dirpath)).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
await fs.writeFile(filepath, "content", "utf-8")
|
||||
|
||||
expect(await Filesystem.isDir(filepath)).toBe(false)
|
||||
})
|
||||
|
||||
test("returns false for non-existent path", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist")
|
||||
|
||||
expect(await Filesystem.isDir(filepath)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("size()", () => {
|
||||
test("returns file size", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
const content = "Hello, World!"
|
||||
await fs.writeFile(filepath, content, "utf-8")
|
||||
|
||||
expect(await Filesystem.size(filepath)).toBe(content.length)
|
||||
})
|
||||
|
||||
test("returns 0 for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.txt")
|
||||
|
||||
expect(await Filesystem.size(filepath)).toBe(0)
|
||||
})
|
||||
|
||||
test("returns directory size", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const dirpath = path.join(tmp.path, "testdir")
|
||||
await fs.mkdir(dirpath)
|
||||
|
||||
// Directories have size on some systems
|
||||
const size = await Filesystem.size(dirpath)
|
||||
expect(typeof size).toBe("number")
|
||||
})
|
||||
})
|
||||
|
||||
describe("readText()", () => {
|
||||
test("reads file content", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
const content = "Hello, World!"
|
||||
await fs.writeFile(filepath, content, "utf-8")
|
||||
|
||||
expect(await Filesystem.readText(filepath)).toBe(content)
|
||||
})
|
||||
|
||||
test("throws for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.txt")
|
||||
|
||||
await expect(Filesystem.readText(filepath)).rejects.toThrow()
|
||||
})
|
||||
|
||||
test("reads UTF-8 content correctly", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "unicode.txt")
|
||||
const content = "Hello 世界 🌍"
|
||||
await fs.writeFile(filepath, content, "utf-8")
|
||||
|
||||
expect(await Filesystem.readText(filepath)).toBe(content)
|
||||
})
|
||||
})
|
||||
|
||||
describe("readJson()", () => {
|
||||
test("reads and parses JSON", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.json")
|
||||
const data = { key: "value", nested: { array: [1, 2, 3] } }
|
||||
await fs.writeFile(filepath, JSON.stringify(data), "utf-8")
|
||||
|
||||
const result: typeof data = await Filesystem.readJson(filepath)
|
||||
expect(result).toEqual(data)
|
||||
})
|
||||
|
||||
test("throws for invalid JSON", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "invalid.json")
|
||||
await fs.writeFile(filepath, "{ invalid json", "utf-8")
|
||||
|
||||
await expect(Filesystem.readJson(filepath)).rejects.toThrow()
|
||||
})
|
||||
|
||||
test("throws for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.json")
|
||||
|
||||
await expect(Filesystem.readJson(filepath)).rejects.toThrow()
|
||||
})
|
||||
|
||||
test("returns typed data", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "typed.json")
|
||||
interface Config {
|
||||
name: string
|
||||
version: number
|
||||
}
|
||||
const data: Config = { name: "test", version: 1 }
|
||||
await fs.writeFile(filepath, JSON.stringify(data), "utf-8")
|
||||
|
||||
const result = await Filesystem.readJson<Config>(filepath)
|
||||
expect(result.name).toBe("test")
|
||||
expect(result.version).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("readBytes()", () => {
|
||||
test("reads file as buffer", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
const content = "Hello, World!"
|
||||
await fs.writeFile(filepath, content, "utf-8")
|
||||
|
||||
const buffer = await Filesystem.readBytes(filepath)
|
||||
expect(buffer).toBeInstanceOf(Buffer)
|
||||
expect(buffer.toString("utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("throws for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.bin")
|
||||
|
||||
await expect(Filesystem.readBytes(filepath)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("write()", () => {
|
||||
test("writes text content", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
const content = "Hello, World!"
|
||||
|
||||
await Filesystem.write(filepath, content)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("writes buffer content", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.bin")
|
||||
const content = Buffer.from([0x00, 0x01, 0x02, 0x03])
|
||||
|
||||
await Filesystem.write(filepath, content)
|
||||
|
||||
const read = await fs.readFile(filepath)
|
||||
expect(read).toEqual(content)
|
||||
})
|
||||
|
||||
test("writes with permissions", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "protected.txt")
|
||||
const content = "secret"
|
||||
|
||||
await Filesystem.write(filepath, content, 0o600)
|
||||
|
||||
const stats = await fs.stat(filepath)
|
||||
// Check permissions on Unix
|
||||
if (process.platform !== "win32") {
|
||||
expect(stats.mode & 0o777).toBe(0o600)
|
||||
}
|
||||
})
|
||||
|
||||
test("creates parent directories", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "nested", "deep", "file.txt")
|
||||
const content = "nested content"
|
||||
|
||||
await Filesystem.write(filepath, content)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
})
|
||||
|
||||
describe("writeJson()", () => {
|
||||
test("writes JSON data", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "data.json")
|
||||
const data = { key: "value", number: 42 }
|
||||
|
||||
await Filesystem.writeJson(filepath, data)
|
||||
|
||||
const content = await fs.readFile(filepath, "utf-8")
|
||||
expect(JSON.parse(content)).toEqual(data)
|
||||
})
|
||||
|
||||
test("writes formatted JSON", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "pretty.json")
|
||||
const data = { key: "value" }
|
||||
|
||||
await Filesystem.writeJson(filepath, data)
|
||||
|
||||
const content = await fs.readFile(filepath, "utf-8")
|
||||
expect(content).toContain("\n")
|
||||
expect(content).toContain(" ")
|
||||
})
|
||||
|
||||
test("writes with permissions", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "config.json")
|
||||
const data = { secret: "data" }
|
||||
|
||||
await Filesystem.writeJson(filepath, data, 0o600)
|
||||
|
||||
const stats = await fs.stat(filepath)
|
||||
if (process.platform !== "win32") {
|
||||
expect(stats.mode & 0o777).toBe(0o600)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("mimeType()", () => {
|
||||
test("returns correct MIME type for JSON", () => {
|
||||
expect(Filesystem.mimeType("test.json")).toContain("application/json")
|
||||
})
|
||||
|
||||
test("returns correct MIME type for JavaScript", () => {
|
||||
expect(Filesystem.mimeType("test.js")).toContain("javascript")
|
||||
})
|
||||
|
||||
test("returns MIME type for TypeScript (or video/mp2t due to extension conflict)", () => {
|
||||
const mime = Filesystem.mimeType("test.ts")
|
||||
// .ts is ambiguous: TypeScript vs MPEG-2 TS video
|
||||
expect(mime === "video/mp2t" || mime === "application/typescript" || mime === "text/typescript").toBe(true)
|
||||
})
|
||||
|
||||
test("returns correct MIME type for images", () => {
|
||||
expect(Filesystem.mimeType("test.png")).toContain("image/png")
|
||||
expect(Filesystem.mimeType("test.jpg")).toContain("image/jpeg")
|
||||
})
|
||||
|
||||
test("returns default for unknown extension", () => {
|
||||
expect(Filesystem.mimeType("test.unknown")).toBe("application/octet-stream")
|
||||
})
|
||||
|
||||
test("handles files without extension", () => {
|
||||
expect(Filesystem.mimeType("Makefile")).toBe("application/octet-stream")
|
||||
})
|
||||
})
|
||||
|
||||
describe("windowsPath()", () => {
|
||||
test("converts Git Bash paths", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(Filesystem.windowsPath("/c/Users/test")).toBe("C:/Users/test")
|
||||
expect(Filesystem.windowsPath("/d/dev/project")).toBe("D:/dev/project")
|
||||
} else {
|
||||
expect(Filesystem.windowsPath("/c/Users/test")).toBe("/c/Users/test")
|
||||
}
|
||||
})
|
||||
|
||||
test("converts Cygwin paths", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(Filesystem.windowsPath("/cygdrive/c/Users/test")).toBe("C:/Users/test")
|
||||
expect(Filesystem.windowsPath("/cygdrive/x/dev/project")).toBe("X:/dev/project")
|
||||
} else {
|
||||
expect(Filesystem.windowsPath("/cygdrive/c/Users/test")).toBe("/cygdrive/c/Users/test")
|
||||
}
|
||||
})
|
||||
|
||||
test("converts WSL paths", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(Filesystem.windowsPath("/mnt/c/Users/test")).toBe("C:/Users/test")
|
||||
expect(Filesystem.windowsPath("/mnt/z/dev/project")).toBe("Z:/dev/project")
|
||||
} else {
|
||||
expect(Filesystem.windowsPath("/mnt/c/Users/test")).toBe("/mnt/c/Users/test")
|
||||
}
|
||||
})
|
||||
|
||||
test("ignores normal Windows paths", () => {
|
||||
expect(Filesystem.windowsPath("C:/Users/test")).toBe("C:/Users/test")
|
||||
expect(Filesystem.windowsPath("D:\\dev\\project")).toBe("D:\\dev\\project")
|
||||
})
|
||||
})
|
||||
|
||||
describe("writeStream()", () => {
|
||||
test("writes from Web ReadableStream", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "streamed.txt")
|
||||
const content = "Hello from stream!"
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(content))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("writes from Node.js Readable stream", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "node-streamed.txt")
|
||||
const content = "Hello from Node stream!"
|
||||
const { Readable } = await import("stream")
|
||||
const stream = Readable.from([content])
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("writes binary data from Web ReadableStream", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "binary.dat")
|
||||
const binaryData = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff])
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(binaryData)
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
const read = await fs.readFile(filepath)
|
||||
expect(Buffer.from(read)).toEqual(Buffer.from(binaryData))
|
||||
})
|
||||
|
||||
test("writes large content in chunks", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "large.txt")
|
||||
const chunks = ["chunk1", "chunk2", "chunk3", "chunk4", "chunk5"]
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
for (const chunk of chunks) {
|
||||
controller.enqueue(new TextEncoder().encode(chunk))
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(chunks.join(""))
|
||||
})
|
||||
|
||||
test("creates parent directories", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "nested", "deep", "streamed.txt")
|
||||
const content = "nested stream content"
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(content))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("writes with permissions", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "protected-stream.txt")
|
||||
const content = "secret stream content"
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(content))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream, 0o600)
|
||||
|
||||
const stats = await fs.stat(filepath)
|
||||
if (process.platform !== "win32") {
|
||||
expect(stats.mode & 0o777).toBe(0o600)
|
||||
}
|
||||
})
|
||||
|
||||
test("writes executable with permissions", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "script.sh")
|
||||
const content = "#!/bin/bash\necho hello"
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(content))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream, 0o755)
|
||||
|
||||
const stats = await fs.stat(filepath)
|
||||
if (process.platform !== "win32") {
|
||||
expect(stats.mode & 0o777).toBe(0o755)
|
||||
}
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolve()", () => {
|
||||
test("resolves slash-prefixed drive paths on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const forward = tmp.path.replaceAll("\\", "/")
|
||||
expect(Filesystem.resolve(`/${forward}`)).toBe(Filesystem.normalizePath(tmp.path))
|
||||
})
|
||||
|
||||
test("resolves slash-prefixed drive roots on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toUpperCase()
|
||||
expect(Filesystem.resolve(`/${drive}:`)).toBe(Filesystem.resolve(`${drive}:/`))
|
||||
})
|
||||
|
||||
test("resolves Git Bash and MSYS2 paths on Windows", async () => {
|
||||
// Git Bash and MSYS2 both use /<drive>/... paths on Windows.
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
const rest = tmp.path.slice(2).replaceAll("\\", "/")
|
||||
expect(Filesystem.resolve(`/${drive}${rest}`)).toBe(Filesystem.normalizePath(tmp.path))
|
||||
})
|
||||
|
||||
test("resolves Git Bash and MSYS2 drive roots on Windows", async () => {
|
||||
// Git Bash and MSYS2 both use /<drive> paths on Windows.
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
expect(Filesystem.resolve(`/${drive}`)).toBe(Filesystem.resolve(`${drive.toUpperCase()}:/`))
|
||||
})
|
||||
|
||||
test("resolves Cygwin paths on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
const rest = tmp.path.slice(2).replaceAll("\\", "/")
|
||||
expect(Filesystem.resolve(`/cygdrive/${drive}${rest}`)).toBe(Filesystem.normalizePath(tmp.path))
|
||||
})
|
||||
|
||||
test("resolves Cygwin drive roots on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
expect(Filesystem.resolve(`/cygdrive/${drive}`)).toBe(Filesystem.resolve(`${drive.toUpperCase()}:/`))
|
||||
})
|
||||
|
||||
test("resolves WSL mount paths on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
const rest = tmp.path.slice(2).replaceAll("\\", "/")
|
||||
expect(Filesystem.resolve(`/mnt/${drive}${rest}`)).toBe(Filesystem.normalizePath(tmp.path))
|
||||
})
|
||||
|
||||
test("resolves WSL mount roots on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
expect(Filesystem.resolve(`/mnt/${drive}`)).toBe(Filesystem.resolve(`${drive.toUpperCase()}:/`))
|
||||
})
|
||||
|
||||
test("resolves symlinked directory to canonical path", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const target = path.join(tmp.path, "real")
|
||||
await fs.mkdir(target)
|
||||
const link = path.join(tmp.path, "link")
|
||||
await fs.symlink(target, link)
|
||||
expect(Filesystem.resolve(link)).toBe(Filesystem.resolve(target))
|
||||
})
|
||||
|
||||
test("returns unresolved path when target does not exist", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const missing = path.join(tmp.path, "does-not-exist-" + Date.now())
|
||||
const result = Filesystem.resolve(missing)
|
||||
expect(result).toBe(Filesystem.normalizePath(path.resolve(missing)))
|
||||
})
|
||||
|
||||
test("throws ELOOP on symlink cycle", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const a = path.join(tmp.path, "a")
|
||||
const b = path.join(tmp.path, "b")
|
||||
await fs.symlink(b, a)
|
||||
await fs.symlink(a, b)
|
||||
expect(() => Filesystem.resolve(a)).toThrow()
|
||||
})
|
||||
|
||||
// Windows: chmod(0o000) is a no-op, so EACCES cannot be triggered
|
||||
test("throws EACCES on permission-denied symlink target", async () => {
|
||||
if (process.platform === "win32") return
|
||||
if (process.getuid?.() === 0) return // skip when running as root
|
||||
await using tmp = await tmpdir()
|
||||
const dir = path.join(tmp.path, "restricted")
|
||||
await fs.mkdir(dir)
|
||||
const link = path.join(tmp.path, "link")
|
||||
await fs.symlink(dir, link)
|
||||
await fs.chmod(dir, 0o000)
|
||||
try {
|
||||
expect(() => Filesystem.resolve(path.join(link, "child"))).toThrow()
|
||||
} finally {
|
||||
await fs.chmod(dir, 0o755)
|
||||
}
|
||||
})
|
||||
|
||||
// Windows: traversing through a file throws ENOENT (not ENOTDIR),
|
||||
// which resolve() catches as a fallback instead of rethrowing
|
||||
test("rethrows non-ENOENT errors", async () => {
|
||||
if (process.platform === "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const file = path.join(tmp.path, "not-a-directory")
|
||||
await fs.writeFile(file, "x")
|
||||
expect(() => Filesystem.resolve(path.join(file, "child"))).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
59
packages/tfcode/test/util/format.test.ts
Normal file
59
packages/tfcode/test/util/format.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { formatDuration } from "../../src/util/format"
|
||||
|
||||
describe("util.format", () => {
|
||||
describe("formatDuration", () => {
|
||||
test("returns empty string for zero or negative values", () => {
|
||||
expect(formatDuration(0)).toBe("")
|
||||
expect(formatDuration(-1)).toBe("")
|
||||
expect(formatDuration(-100)).toBe("")
|
||||
})
|
||||
|
||||
test("formats seconds under a minute", () => {
|
||||
expect(formatDuration(1)).toBe("1s")
|
||||
expect(formatDuration(30)).toBe("30s")
|
||||
expect(formatDuration(59)).toBe("59s")
|
||||
})
|
||||
|
||||
test("formats minutes under an hour", () => {
|
||||
expect(formatDuration(60)).toBe("1m")
|
||||
expect(formatDuration(61)).toBe("1m 1s")
|
||||
expect(formatDuration(90)).toBe("1m 30s")
|
||||
expect(formatDuration(120)).toBe("2m")
|
||||
expect(formatDuration(330)).toBe("5m 30s")
|
||||
expect(formatDuration(3599)).toBe("59m 59s")
|
||||
})
|
||||
|
||||
test("formats hours under a day", () => {
|
||||
expect(formatDuration(3600)).toBe("1h")
|
||||
expect(formatDuration(3660)).toBe("1h 1m")
|
||||
expect(formatDuration(7200)).toBe("2h")
|
||||
expect(formatDuration(8100)).toBe("2h 15m")
|
||||
expect(formatDuration(86399)).toBe("23h 59m")
|
||||
})
|
||||
|
||||
test("formats days under a week", () => {
|
||||
expect(formatDuration(86400)).toBe("~1 day")
|
||||
expect(formatDuration(172800)).toBe("~2 days")
|
||||
expect(formatDuration(259200)).toBe("~3 days")
|
||||
expect(formatDuration(604799)).toBe("~6 days")
|
||||
})
|
||||
|
||||
test("formats weeks", () => {
|
||||
expect(formatDuration(604800)).toBe("~1 week")
|
||||
expect(formatDuration(1209600)).toBe("~2 weeks")
|
||||
expect(formatDuration(1609200)).toBe("~2 weeks")
|
||||
})
|
||||
|
||||
test("handles boundary values correctly", () => {
|
||||
expect(formatDuration(59)).toBe("59s")
|
||||
expect(formatDuration(60)).toBe("1m")
|
||||
expect(formatDuration(3599)).toBe("59m 59s")
|
||||
expect(formatDuration(3600)).toBe("1h")
|
||||
expect(formatDuration(86399)).toBe("23h 59m")
|
||||
expect(formatDuration(86400)).toBe("~1 day")
|
||||
expect(formatDuration(604799)).toBe("~6 days")
|
||||
expect(formatDuration(604800)).toBe("~1 week")
|
||||
})
|
||||
})
|
||||
})
|
||||
164
packages/tfcode/test/util/glob.test.ts
Normal file
164
packages/tfcode/test/util/glob.test.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Glob } from "../../src/util/glob"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
describe("Glob", () => {
|
||||
describe("scan()", () => {
|
||||
test("finds files matching pattern", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "c.md"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results.sort()).toEqual(["a.txt", "b.txt"])
|
||||
})
|
||||
|
||||
test("returns absolute paths when absolute option is true", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*.txt", { cwd: tmp.path, absolute: true })
|
||||
|
||||
expect(results[0]).toBe(path.join(tmp.path, "file.txt"))
|
||||
})
|
||||
|
||||
test("excludes directories by default", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual(["file.txt"])
|
||||
})
|
||||
|
||||
test("excludes directories when include is 'file'", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, include: "file" })
|
||||
|
||||
expect(results).toEqual(["file.txt"])
|
||||
})
|
||||
|
||||
test("includes directories when include is 'all'", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, include: "all" })
|
||||
|
||||
expect(results.sort()).toEqual(["file.txt", "subdir"])
|
||||
})
|
||||
|
||||
test("handles nested patterns", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "nested"), { recursive: true })
|
||||
await fs.writeFile(path.join(tmp.path, "nested", "deep.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("**/*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual([path.join("nested", "deep.txt")])
|
||||
})
|
||||
|
||||
test("returns empty array for no matches", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
const results = await Glob.scan("*.nonexistent", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
|
||||
test("does not follow symlinks by default", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "realdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8")
|
||||
await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir"))
|
||||
|
||||
const results = await Glob.scan("**/*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual([path.join("realdir", "file.txt")])
|
||||
})
|
||||
|
||||
test("follows symlinks when symlink option is true", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "realdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8")
|
||||
await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir"))
|
||||
|
||||
const results = await Glob.scan("**/*.txt", { cwd: tmp.path, symlink: true })
|
||||
|
||||
expect(results.sort()).toEqual([path.join("linkdir", "file.txt"), path.join("realdir", "file.txt")])
|
||||
})
|
||||
|
||||
test("includes dotfiles when dot option is true", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, dot: true })
|
||||
|
||||
expect(results.sort()).toEqual([".hidden", "visible"])
|
||||
})
|
||||
|
||||
test("excludes dotfiles when dot option is false", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, dot: false })
|
||||
|
||||
expect(results).toEqual(["visible"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("scanSync()", () => {
|
||||
test("finds files matching pattern synchronously", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8")
|
||||
|
||||
const results = Glob.scanSync("*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results.sort()).toEqual(["a.txt", "b.txt"])
|
||||
})
|
||||
|
||||
test("respects options", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = Glob.scanSync("*", { cwd: tmp.path, include: "all" })
|
||||
|
||||
expect(results.sort()).toEqual(["file.txt", "subdir"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("match()", () => {
|
||||
test("matches simple patterns", () => {
|
||||
expect(Glob.match("*.txt", "file.txt")).toBe(true)
|
||||
expect(Glob.match("*.txt", "file.js")).toBe(false)
|
||||
})
|
||||
|
||||
test("matches directory patterns", () => {
|
||||
expect(Glob.match("**/*.js", "src/index.js")).toBe(true)
|
||||
expect(Glob.match("**/*.js", "src/index.ts")).toBe(false)
|
||||
})
|
||||
|
||||
test("matches dot files", () => {
|
||||
expect(Glob.match(".*", ".gitignore")).toBe(true)
|
||||
expect(Glob.match("**/*.md", ".github/README.md")).toBe(true)
|
||||
})
|
||||
|
||||
test("matches brace expansion", () => {
|
||||
expect(Glob.match("*.{js,ts}", "file.js")).toBe(true)
|
||||
expect(Glob.match("*.{js,ts}", "file.ts")).toBe(true)
|
||||
expect(Glob.match("*.{js,ts}", "file.py")).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
36
packages/tfcode/test/util/iife.test.ts
Normal file
36
packages/tfcode/test/util/iife.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { iife } from "../../src/util/iife"
|
||||
|
||||
describe("util.iife", () => {
|
||||
test("should execute function immediately and return result", () => {
|
||||
let called = false
|
||||
const result = iife(() => {
|
||||
called = true
|
||||
return 42
|
||||
})
|
||||
|
||||
expect(called).toBe(true)
|
||||
expect(result).toBe(42)
|
||||
})
|
||||
|
||||
test("should work with async functions", async () => {
|
||||
let called = false
|
||||
const result = await iife(async () => {
|
||||
called = true
|
||||
return "async result"
|
||||
})
|
||||
|
||||
expect(called).toBe(true)
|
||||
expect(result).toBe("async result")
|
||||
})
|
||||
|
||||
test("should handle functions with no return value", () => {
|
||||
let called = false
|
||||
const result = iife(() => {
|
||||
called = true
|
||||
})
|
||||
|
||||
expect(called).toBe(true)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
50
packages/tfcode/test/util/lazy.test.ts
Normal file
50
packages/tfcode/test/util/lazy.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { lazy } from "../../src/util/lazy"
|
||||
|
||||
describe("util.lazy", () => {
|
||||
test("should call function only once", () => {
|
||||
let callCount = 0
|
||||
const getValue = () => {
|
||||
callCount++
|
||||
return "expensive value"
|
||||
}
|
||||
|
||||
const lazyValue = lazy(getValue)
|
||||
|
||||
expect(callCount).toBe(0)
|
||||
|
||||
const result1 = lazyValue()
|
||||
expect(result1).toBe("expensive value")
|
||||
expect(callCount).toBe(1)
|
||||
|
||||
const result2 = lazyValue()
|
||||
expect(result2).toBe("expensive value")
|
||||
expect(callCount).toBe(1)
|
||||
})
|
||||
|
||||
test("should preserve the same reference", () => {
|
||||
const obj = { value: 42 }
|
||||
const lazyObj = lazy(() => obj)
|
||||
|
||||
const result1 = lazyObj()
|
||||
const result2 = lazyObj()
|
||||
|
||||
expect(result1).toBe(obj)
|
||||
expect(result2).toBe(obj)
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
|
||||
test("should work with different return types", () => {
|
||||
const lazyString = lazy(() => "string")
|
||||
const lazyNumber = lazy(() => 123)
|
||||
const lazyBoolean = lazy(() => true)
|
||||
const lazyNull = lazy(() => null)
|
||||
const lazyUndefined = lazy(() => undefined)
|
||||
|
||||
expect(lazyString()).toBe("string")
|
||||
expect(lazyNumber()).toBe(123)
|
||||
expect(lazyBoolean()).toBe(true)
|
||||
expect(lazyNull()).toBe(null)
|
||||
expect(lazyUndefined()).toBe(undefined)
|
||||
})
|
||||
})
|
||||
72
packages/tfcode/test/util/lock.test.ts
Normal file
72
packages/tfcode/test/util/lock.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Lock } from "../../src/util/lock"
|
||||
|
||||
function tick() {
|
||||
return new Promise<void>((r) => queueMicrotask(r))
|
||||
}
|
||||
|
||||
async function flush(n = 5) {
|
||||
for (let i = 0; i < n; i++) await tick()
|
||||
}
|
||||
|
||||
describe("util.lock", () => {
|
||||
test("writer exclusivity: blocks reads and other writes while held", async () => {
|
||||
const key = "lock:" + Math.random().toString(36).slice(2)
|
||||
|
||||
const state = {
|
||||
writer2: false,
|
||||
reader: false,
|
||||
writers: 0,
|
||||
}
|
||||
|
||||
// Acquire writer1
|
||||
using writer1 = await Lock.write(key)
|
||||
state.writers++
|
||||
expect(state.writers).toBe(1)
|
||||
|
||||
// Start writer2 candidate (should block)
|
||||
const writer2Task = (async () => {
|
||||
const w = await Lock.write(key)
|
||||
state.writers++
|
||||
expect(state.writers).toBe(1)
|
||||
state.writer2 = true
|
||||
// Hold for a tick so reader cannot slip in
|
||||
await tick()
|
||||
return w
|
||||
})()
|
||||
|
||||
// Start reader candidate (should block)
|
||||
const readerTask = (async () => {
|
||||
const r = await Lock.read(key)
|
||||
state.reader = true
|
||||
return r
|
||||
})()
|
||||
|
||||
// Flush microtasks and assert neither acquired
|
||||
await flush()
|
||||
expect(state.writer2).toBe(false)
|
||||
expect(state.reader).toBe(false)
|
||||
|
||||
// Release writer1
|
||||
writer1[Symbol.dispose]()
|
||||
state.writers--
|
||||
|
||||
// writer2 should acquire next
|
||||
const writer2 = await writer2Task
|
||||
expect(state.writer2).toBe(true)
|
||||
|
||||
// Reader still blocked while writer2 held
|
||||
await flush()
|
||||
expect(state.reader).toBe(false)
|
||||
|
||||
// Release writer2
|
||||
writer2[Symbol.dispose]()
|
||||
state.writers--
|
||||
|
||||
// Reader should now acquire
|
||||
const reader = await readerTask
|
||||
expect(state.reader).toBe(true)
|
||||
|
||||
reader[Symbol.dispose]()
|
||||
})
|
||||
})
|
||||
59
packages/tfcode/test/util/module.test.ts
Normal file
59
packages/tfcode/test/util/module.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Module } from "@opencode-ai/util/module"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
describe("util.module", () => {
|
||||
test("resolves package subpaths from the provided dir", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const root = path.join(tmp.path, "proj")
|
||||
const file = path.join(root, "node_modules/typescript/lib/tsserver.js")
|
||||
await Filesystem.write(file, "export {}\n")
|
||||
await Filesystem.writeJson(path.join(root, "node_modules/typescript/package.json"), { name: "typescript" })
|
||||
|
||||
expect(Module.resolve("typescript/lib/tsserver.js", root)).toBe(file)
|
||||
})
|
||||
|
||||
test("resolves packages through ancestor node_modules", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const root = path.join(tmp.path, "proj")
|
||||
const cwd = path.join(root, "apps/web")
|
||||
const file = path.join(root, "node_modules/eslint/lib/api.js")
|
||||
await Filesystem.write(file, "export {}\n")
|
||||
await Filesystem.writeJson(path.join(root, "node_modules/eslint/package.json"), {
|
||||
name: "eslint",
|
||||
main: "lib/api.js",
|
||||
})
|
||||
await Filesystem.write(path.join(cwd, ".keep"), "")
|
||||
|
||||
expect(Module.resolve("eslint", cwd)).toBe(file)
|
||||
})
|
||||
|
||||
test("resolves relative to the provided dir", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const a = path.join(tmp.path, "a")
|
||||
const b = path.join(tmp.path, "b")
|
||||
const left = path.join(a, "node_modules/biome/index.js")
|
||||
const right = path.join(b, "node_modules/biome/index.js")
|
||||
await Filesystem.write(left, "export {}\n")
|
||||
await Filesystem.write(right, "export {}\n")
|
||||
await Filesystem.writeJson(path.join(a, "node_modules/biome/package.json"), {
|
||||
name: "biome",
|
||||
main: "index.js",
|
||||
})
|
||||
await Filesystem.writeJson(path.join(b, "node_modules/biome/package.json"), {
|
||||
name: "biome",
|
||||
main: "index.js",
|
||||
})
|
||||
|
||||
expect(Module.resolve("biome", a)).toBe(left)
|
||||
expect(Module.resolve("biome", b)).toBe(right)
|
||||
expect(Module.resolve("biome", a)).not.toBe(Module.resolve("biome", b))
|
||||
})
|
||||
|
||||
test("returns undefined when resolution fails", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
expect(Module.resolve("missing-package", tmp.path)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
128
packages/tfcode/test/util/process.test.ts
Normal file
128
packages/tfcode/test/util/process.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Process } from "../../src/util/process"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
function node(script: string) {
|
||||
return [process.execPath, "-e", script]
|
||||
}
|
||||
|
||||
describe("util.process", () => {
|
||||
test("captures stdout and stderr", async () => {
|
||||
const out = await Process.run(node('process.stdout.write("out");process.stderr.write("err")'))
|
||||
expect(out.code).toBe(0)
|
||||
expect(out.stdout.toString()).toBe("out")
|
||||
expect(out.stderr.toString()).toBe("err")
|
||||
})
|
||||
|
||||
test("returns code when nothrow is enabled", async () => {
|
||||
const out = await Process.run(node("process.exit(7)"), { nothrow: true })
|
||||
expect(out.code).toBe(7)
|
||||
})
|
||||
|
||||
test("throws RunFailedError on non-zero exit", async () => {
|
||||
const err = await Process.run(node('process.stderr.write("bad");process.exit(3)')).catch((error) => error)
|
||||
expect(err).toBeInstanceOf(Process.RunFailedError)
|
||||
if (!(err instanceof Process.RunFailedError)) throw err
|
||||
expect(err.code).toBe(3)
|
||||
expect(err.stderr.toString()).toBe("bad")
|
||||
})
|
||||
|
||||
test("aborts a running process", async () => {
|
||||
const abort = new AbortController()
|
||||
const started = Date.now()
|
||||
setTimeout(() => abort.abort(), 25)
|
||||
|
||||
const out = await Process.run(node("setInterval(() => {}, 1000)"), {
|
||||
abort: abort.signal,
|
||||
nothrow: true,
|
||||
})
|
||||
|
||||
expect(out.code).not.toBe(0)
|
||||
expect(Date.now() - started).toBeLessThan(1000)
|
||||
}, 3000)
|
||||
|
||||
test("kills after timeout when process ignores terminate signal", async () => {
|
||||
if (process.platform === "win32") return
|
||||
|
||||
const abort = new AbortController()
|
||||
const started = Date.now()
|
||||
setTimeout(() => abort.abort(), 25)
|
||||
|
||||
const out = await Process.run(node('process.on("SIGTERM", () => {}); setInterval(() => {}, 1000)'), {
|
||||
abort: abort.signal,
|
||||
nothrow: true,
|
||||
timeout: 25,
|
||||
})
|
||||
|
||||
expect(out.code).not.toBe(0)
|
||||
expect(Date.now() - started).toBeLessThan(1000)
|
||||
}, 3000)
|
||||
|
||||
test("uses cwd when spawning commands", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const out = await Process.run(node("process.stdout.write(process.cwd())"), {
|
||||
cwd: tmp.path,
|
||||
})
|
||||
expect(out.stdout.toString()).toBe(tmp.path)
|
||||
})
|
||||
|
||||
test("merges environment overrides", async () => {
|
||||
const out = await Process.run(node('process.stdout.write(process.env.OPENCODE_TEST ?? "")'), {
|
||||
env: {
|
||||
OPENCODE_TEST: "set",
|
||||
},
|
||||
})
|
||||
expect(out.stdout.toString()).toBe("set")
|
||||
})
|
||||
|
||||
test("uses shell in run on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
const out = await Process.run(["set", "OPENCODE_TEST_SHELL"], {
|
||||
shell: true,
|
||||
env: {
|
||||
OPENCODE_TEST_SHELL: "ok",
|
||||
},
|
||||
})
|
||||
|
||||
expect(out.code).toBe(0)
|
||||
expect(out.stdout.toString()).toContain("OPENCODE_TEST_SHELL=ok")
|
||||
})
|
||||
|
||||
test("runs cmd scripts with spaces on Windows without shell", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
const dir = path.join(tmp.path, "with space")
|
||||
const file = path.join(dir, "echo cmd.cmd")
|
||||
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await Bun.write(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n")
|
||||
|
||||
const proc = Process.spawn([file, "--stdio"], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
expect(await proc.exited).toBe(0)
|
||||
})
|
||||
|
||||
test("rejects missing commands without leaking unhandled errors", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const cmd = path.join(tmp.path, "missing" + (process.platform === "win32" ? ".cmd" : ""))
|
||||
const err = await Process.spawn([cmd], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
}).exited.catch((err) => err)
|
||||
|
||||
expect(err).toBeInstanceOf(Error)
|
||||
if (!(err instanceof Error)) throw err
|
||||
expect(err).toMatchObject({
|
||||
code: "ENOENT",
|
||||
})
|
||||
})
|
||||
})
|
||||
21
packages/tfcode/test/util/timeout.test.ts
Normal file
21
packages/tfcode/test/util/timeout.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { withTimeout } from "../../src/util/timeout"
|
||||
|
||||
describe("util.timeout", () => {
|
||||
test("should resolve when promise completes before timeout", async () => {
|
||||
const fastPromise = new Promise<string>((resolve) => {
|
||||
setTimeout(() => resolve("fast"), 10)
|
||||
})
|
||||
|
||||
const result = await withTimeout(fastPromise, 100)
|
||||
expect(result).toBe("fast")
|
||||
})
|
||||
|
||||
test("should reject when promise exceeds timeout", async () => {
|
||||
const slowPromise = new Promise<string>((resolve) => {
|
||||
setTimeout(() => resolve("slow"), 200)
|
||||
})
|
||||
|
||||
await expect(withTimeout(slowPromise, 50)).rejects.toThrow("Operation timed out after 50ms")
|
||||
})
|
||||
})
|
||||
100
packages/tfcode/test/util/which.test.ts
Normal file
100
packages/tfcode/test/util/which.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { which } from "../../src/util/which"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
async function cmd(dir: string, name: string, exec = true) {
|
||||
const ext = process.platform === "win32" ? ".cmd" : ""
|
||||
const file = path.join(dir, name + ext)
|
||||
const body = process.platform === "win32" ? "@echo off\r\n" : "#!/bin/sh\n"
|
||||
await fs.writeFile(file, body)
|
||||
if (process.platform !== "win32") {
|
||||
await fs.chmod(file, exec ? 0o755 : 0o644)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
function env(PATH: string): NodeJS.ProcessEnv {
|
||||
return {
|
||||
PATH,
|
||||
PATHEXT: process.env["PATHEXT"],
|
||||
}
|
||||
}
|
||||
|
||||
function envPath(Path: string): NodeJS.ProcessEnv {
|
||||
return {
|
||||
Path,
|
||||
PathExt: process.env["PathExt"] ?? process.env["PATHEXT"],
|
||||
}
|
||||
}
|
||||
|
||||
function same(a: string | null, b: string) {
|
||||
if (process.platform === "win32") {
|
||||
expect(a?.toLowerCase()).toBe(b.toLowerCase())
|
||||
return
|
||||
}
|
||||
|
||||
expect(a).toBe(b)
|
||||
}
|
||||
|
||||
describe("util.which", () => {
|
||||
test("returns null when command is missing", () => {
|
||||
expect(which("opencode-missing-command-for-test")).toBeNull()
|
||||
})
|
||||
|
||||
test("finds a command from PATH override", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const bin = path.join(tmp.path, "bin")
|
||||
await fs.mkdir(bin)
|
||||
const file = await cmd(bin, "tool")
|
||||
|
||||
same(which("tool", env(bin)), file)
|
||||
})
|
||||
|
||||
test("uses first PATH match", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const a = path.join(tmp.path, "a")
|
||||
const b = path.join(tmp.path, "b")
|
||||
await fs.mkdir(a)
|
||||
await fs.mkdir(b)
|
||||
const first = await cmd(a, "dupe")
|
||||
await cmd(b, "dupe")
|
||||
|
||||
same(which("dupe", env([a, b].join(path.delimiter))), first)
|
||||
})
|
||||
|
||||
test("returns null for non-executable file on unix", async () => {
|
||||
if (process.platform === "win32") return
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
const bin = path.join(tmp.path, "bin")
|
||||
await fs.mkdir(bin)
|
||||
await cmd(bin, "noexec", false)
|
||||
|
||||
expect(which("noexec", env(bin))).toBeNull()
|
||||
})
|
||||
|
||||
test("uses PATHEXT on windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
const bin = path.join(tmp.path, "bin")
|
||||
await fs.mkdir(bin)
|
||||
const file = path.join(bin, "pathext.CMD")
|
||||
await fs.writeFile(file, "@echo off\r\n")
|
||||
|
||||
expect(which("pathext", { PATH: bin, PATHEXT: ".CMD" })).toBe(file)
|
||||
})
|
||||
|
||||
test("uses Windows Path casing fallback", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
const bin = path.join(tmp.path, "bin")
|
||||
await fs.mkdir(bin)
|
||||
const file = await cmd(bin, "mixed")
|
||||
|
||||
same(which("mixed", envPath(bin)), file)
|
||||
})
|
||||
})
|
||||
90
packages/tfcode/test/util/wildcard.test.ts
Normal file
90
packages/tfcode/test/util/wildcard.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { test, expect } from "bun:test"
|
||||
import { Wildcard } from "../../src/util/wildcard"
|
||||
|
||||
test("match handles glob tokens", () => {
|
||||
expect(Wildcard.match("file1.txt", "file?.txt")).toBe(true)
|
||||
expect(Wildcard.match("file12.txt", "file?.txt")).toBe(false)
|
||||
expect(Wildcard.match("foo+bar", "foo+bar")).toBe(true)
|
||||
})
|
||||
|
||||
test("match with trailing space+wildcard matches command with or without args", () => {
|
||||
// "ls *" should match "ls" (no args) and "ls -la" (with args)
|
||||
expect(Wildcard.match("ls", "ls *")).toBe(true)
|
||||
expect(Wildcard.match("ls -la", "ls *")).toBe(true)
|
||||
expect(Wildcard.match("ls foo bar", "ls *")).toBe(true)
|
||||
|
||||
// "ls*" (no space) should NOT match "ls" alone — wait, it should because .* matches empty
|
||||
// but it WILL match "lstmeval" which is the dangerous case users should avoid
|
||||
expect(Wildcard.match("ls", "ls*")).toBe(true)
|
||||
expect(Wildcard.match("lstmeval", "ls*")).toBe(true)
|
||||
|
||||
// "ls *" (with space) should NOT match "lstmeval"
|
||||
expect(Wildcard.match("lstmeval", "ls *")).toBe(false)
|
||||
|
||||
// multi-word commands
|
||||
expect(Wildcard.match("git status", "git *")).toBe(true)
|
||||
expect(Wildcard.match("git", "git *")).toBe(true)
|
||||
expect(Wildcard.match("git commit -m foo", "git *")).toBe(true)
|
||||
})
|
||||
|
||||
test("all picks the most specific pattern", () => {
|
||||
const rules = {
|
||||
"*": "deny",
|
||||
"git *": "ask",
|
||||
"git status": "allow",
|
||||
}
|
||||
expect(Wildcard.all("git status", rules)).toBe("allow")
|
||||
expect(Wildcard.all("git log", rules)).toBe("ask")
|
||||
expect(Wildcard.all("echo hi", rules)).toBe("deny")
|
||||
})
|
||||
|
||||
test("allStructured matches command sequences", () => {
|
||||
const rules = {
|
||||
"git *": "ask",
|
||||
"git status*": "allow",
|
||||
}
|
||||
expect(Wildcard.allStructured({ head: "git", tail: ["status", "--short"] }, rules)).toBe("allow")
|
||||
expect(Wildcard.allStructured({ head: "npm", tail: ["run", "build", "--watch"] }, { "npm run *": "allow" })).toBe(
|
||||
"allow",
|
||||
)
|
||||
expect(Wildcard.allStructured({ head: "ls", tail: ["-la"] }, rules)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("allStructured prioritizes flag-specific patterns", () => {
|
||||
const rules = {
|
||||
"find *": "allow",
|
||||
"find * -delete*": "ask",
|
||||
"sort*": "allow",
|
||||
"sort -o *": "ask",
|
||||
}
|
||||
expect(Wildcard.allStructured({ head: "find", tail: ["src", "-delete"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "find", tail: ["src", "-print"] }, rules)).toBe("allow")
|
||||
expect(Wildcard.allStructured({ head: "sort", tail: ["-o", "out.txt"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "sort", tail: ["--reverse"] }, rules)).toBe("allow")
|
||||
})
|
||||
|
||||
test("allStructured handles sed flags", () => {
|
||||
const rules = {
|
||||
"sed * -i*": "ask",
|
||||
"sed -n*": "allow",
|
||||
}
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-i", "file"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-i.bak", "file"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-n", "1p", "file"] }, rules)).toBe("allow")
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-i", "-n", "/./p", "myfile.txt"] }, rules)).toBe("ask")
|
||||
})
|
||||
|
||||
test("match normalizes slashes for cross-platform globbing", () => {
|
||||
expect(Wildcard.match("C:\\Windows\\System32\\*", "C:/Windows/System32/*")).toBe(true)
|
||||
expect(Wildcard.match("C:/Windows/System32/drivers", "C:\\Windows\\System32\\*")).toBe(true)
|
||||
})
|
||||
|
||||
test("match handles case-insensitivity on Windows", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(Wildcard.match("C:\\windows\\system32\\hosts", "C:/Windows/System32/*")).toBe(true)
|
||||
expect(Wildcard.match("c:/windows/system32/hosts", "C:\\Windows\\System32\\*")).toBe(true)
|
||||
} else {
|
||||
// Unix paths are case-sensitive
|
||||
expect(Wildcard.match("/users/test/file", "/Users/test/*")).toBe(false)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user