Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions packages/opencode/test/mcp/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { describe, test, expect } from "bun:test"
import { tmpdir } from "../fixture/fixture"
import { mkdir, writeFile, readFile } from "fs/promises"
import path from "path"
import {
resolveConfigPath,
addMcpToConfig,
removeMcpFromConfig,
listMcpInConfig,
findAllConfigPaths,
} from "../../src/mcp/config"

describe("MCP config: resolveConfigPath", () => {
test("returns .altimate-code subdir config when it exists", async () => {
await using tmp = await tmpdir()
const configDir = path.join(tmp.path, ".altimate-code")
await mkdir(configDir, { recursive: true })
await writeFile(path.join(configDir, "altimate-code.json"), "{}")
const result = await resolveConfigPath(tmp.path)
expect(result).toBe(path.join(configDir, "altimate-code.json"))
})

test("prefers .altimate-code over .opencode subdir", async () => {
await using tmp = await tmpdir()
await mkdir(path.join(tmp.path, ".altimate-code"), { recursive: true })
await writeFile(path.join(tmp.path, ".altimate-code", "altimate-code.json"), "{}")
await mkdir(path.join(tmp.path, ".opencode"), { recursive: true })
await writeFile(path.join(tmp.path, ".opencode", "opencode.json"), "{}")
const result = await resolveConfigPath(tmp.path)
expect(result).toBe(path.join(tmp.path, ".altimate-code", "altimate-code.json"))
})

test("falls back to root-level config", async () => {
await using tmp = await tmpdir()
await writeFile(path.join(tmp.path, "opencode.json"), "{}")
const result = await resolveConfigPath(tmp.path)
expect(result).toBe(path.join(tmp.path, "opencode.json"))
})

test("returns first candidate path when no config exists", async () => {
await using tmp = await tmpdir()
const result = await resolveConfigPath(tmp.path)
expect(result).toBe(path.join(tmp.path, ".altimate-code", "altimate-code.json"))
})

test("global=true skips subdirectory configs", async () => {
await using tmp = await tmpdir()
await mkdir(path.join(tmp.path, ".altimate-code"), { recursive: true })
await writeFile(path.join(tmp.path, ".altimate-code", "altimate-code.json"), "{}")
await writeFile(path.join(tmp.path, "opencode.json"), "{}")
const result = await resolveConfigPath(tmp.path, true)
expect(result).toBe(path.join(tmp.path, "opencode.json"))
})
})

describe("MCP config: addMcpToConfig + removeMcpFromConfig round-trip", () => {
test("adds MCP server to empty config", async () => {
await using tmp = await tmpdir()
const configPath = path.join(tmp.path, "opencode.json")
await addMcpToConfig("test-server", { type: "local", command: ["node", "server.js"] } as any, configPath)
const content = JSON.parse(await readFile(configPath, "utf-8"))
expect(content.mcp["test-server"]).toMatchObject({ type: "local", command: ["node", "server.js"] })
})

test("adds MCP server to existing config preserving other fields", async () => {
await using tmp = await tmpdir()
const configPath = path.join(tmp.path, "opencode.json")
await writeFile(configPath, JSON.stringify({ provider: { default: "anthropic" } }))
await addMcpToConfig("my-server", { type: "remote", url: "https://example.com" } as any, configPath)
const content = JSON.parse(await readFile(configPath, "utf-8"))
expect(content.provider.default).toBe("anthropic")
expect(content.mcp["my-server"].url).toBe("https://example.com")
})

test("remove returns false for nonexistent config file", async () => {
await using tmp = await tmpdir()
const result = await removeMcpFromConfig("nope", path.join(tmp.path, "missing.json"))
expect(result).toBe(false)
})

test("remove returns false for nonexistent server name", async () => {
await using tmp = await tmpdir()
const configPath = path.join(tmp.path, "opencode.json")
await writeFile(configPath, JSON.stringify({ mcp: { existing: { type: "local", command: ["x"] } } }))
const result = await removeMcpFromConfig("nonexistent", configPath)
expect(result).toBe(false)
})

test("add then remove round-trips correctly", async () => {
await using tmp = await tmpdir()
const configPath = path.join(tmp.path, "opencode.json")
await addMcpToConfig("ephemeral", { type: "local", command: ["test"] } as any, configPath)
const listed = await listMcpInConfig(configPath)
expect(listed).toContain("ephemeral")
const removed = await removeMcpFromConfig("ephemeral", configPath)
expect(removed).toBe(true)
const after = await listMcpInConfig(configPath)
expect(after).not.toContain("ephemeral")
})
})

describe("MCP config: listMcpInConfig", () => {
test("returns empty array for missing file", async () => {
await using tmp = await tmpdir()
const result = await listMcpInConfig(path.join(tmp.path, "nope.json"))
expect(result).toEqual([])
})

test("returns empty array for config without mcp key", async () => {
await using tmp = await tmpdir()
const configPath = path.join(tmp.path, "opencode.json")
await writeFile(configPath, JSON.stringify({ provider: {} }))
const result = await listMcpInConfig(configPath)
expect(result).toEqual([])
})

test("lists all server names", async () => {
await using tmp = await tmpdir()
const configPath = path.join(tmp.path, "opencode.json")
await writeFile(
configPath,
JSON.stringify({
mcp: {
alpha: { type: "local", command: ["a"] },
beta: { type: "remote", url: "https://b.com" },
},
}),
)
const result = await listMcpInConfig(configPath)
expect(result).toEqual(expect.arrayContaining(["alpha", "beta"]))
expect(result).toHaveLength(2)
})
})

describe("MCP config: findAllConfigPaths", () => {
test("returns paths from both project and global dirs", async () => {
await using projTmp = await tmpdir()
await using globalTmp = await tmpdir()
await writeFile(path.join(projTmp.path, "opencode.json"), "{}")
await writeFile(path.join(globalTmp.path, "altimate-code.json"), "{}")
const result = await findAllConfigPaths(projTmp.path, globalTmp.path)
expect(result).toContain(path.join(projTmp.path, "opencode.json"))
expect(result).toContain(path.join(globalTmp.path, "altimate-code.json"))
})

test("includes project subdirs but not global subdirs", async () => {
await using projTmp = await tmpdir()
await using globalTmp = await tmpdir()
// Create config in project .opencode subdir
await mkdir(path.join(projTmp.path, ".opencode"), { recursive: true })
await writeFile(path.join(projTmp.path, ".opencode", "opencode.json"), "{}")
// Create config in global .opencode subdir (should NOT be found)
await mkdir(path.join(globalTmp.path, ".opencode"), { recursive: true })
await writeFile(path.join(globalTmp.path, ".opencode", "opencode.json"), "{}")
const result = await findAllConfigPaths(projTmp.path, globalTmp.path)
expect(result).toContain(path.join(projTmp.path, ".opencode", "opencode.json"))
expect(result).not.toContain(path.join(globalTmp.path, ".opencode", "opencode.json"))
})

test("returns empty when no config files exist", async () => {
await using projTmp = await tmpdir()
await using globalTmp = await tmpdir()
const result = await findAllConfigPaths(projTmp.path, globalTmp.path)
expect(result).toEqual([])
})
})
81 changes: 81 additions & 0 deletions packages/opencode/test/util/locale.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, test, expect } from "bun:test"
import { Locale } from "../../src/util/locale"

describe("Locale.number", () => {
test("formats millions", () => {
expect(Locale.number(1500000)).toBe("1.5M")
expect(Locale.number(1000000)).toBe("1.0M")
})

test("formats thousands", () => {
expect(Locale.number(1500)).toBe("1.5K")
expect(Locale.number(1000)).toBe("1.0K")
})

test("boundary: 999999 renders as K not M", () => {
expect(Locale.number(999999)).toBe("1000.0K")
})

test("returns raw string for small numbers", () => {
expect(Locale.number(999)).toBe("999")
expect(Locale.number(0)).toBe("0")
})
})

describe("Locale.duration", () => {
test("milliseconds", () => {
expect(Locale.duration(500)).toBe("500ms")
expect(Locale.duration(0)).toBe("0ms")
})

test("seconds", () => {
expect(Locale.duration(1500)).toBe("1.5s")
expect(Locale.duration(2500)).toBe("2.5s")
})

test("minutes and seconds", () => {
expect(Locale.duration(90000)).toBe("1m 30s")
expect(Locale.duration(3599999)).toBe("59m 59s")
})

test("hours and minutes", () => {
expect(Locale.duration(3600000)).toBe("1h 0m")
expect(Locale.duration(5400000)).toBe("1h 30m")
})

// BUG: Locale.duration >=24h has swapped days/hours calculation.
// hours = Math.floor(input / 3600000) gives total hours (25), not remainder.
// days = Math.floor((input % 3600000) / 86400000) always yields 0.
// Correct: days = Math.floor(input / 86400000), hours = Math.floor((input % 86400000) / 3600000)
// 90000000ms = 25h = 1d 1h — should display "1d 1h"
// See: https://github.com/AltimateAI/altimate-code/issues/368
test.skip("FIXME: days and hours for >=24h are calculated correctly", () => {
expect(Locale.duration(90000000)).toBe("1d 1h")
})
})

describe("Locale.truncateMiddle", () => {
test("returns original if short enough", () => {
expect(Locale.truncateMiddle("hello", 35)).toBe("hello")
})

test("truncates long strings with ellipsis in middle", () => {
const long = "abcdefghijklmnopqrstuvwxyz1234567890abcdef"
const result = Locale.truncateMiddle(long, 20)
expect(result.length).toBe(20)
expect(result).toContain("\u2026")
expect(result.startsWith("abcdefghij")).toBe(true)
expect(result.endsWith("bcdef")).toBe(true)
})
})

describe("Locale.pluralize", () => {
test("uses singular for count=1", () => {
expect(Locale.pluralize(1, "{} item", "{} items")).toBe("1 item")
})

test("uses plural for count!=1", () => {
expect(Locale.pluralize(0, "{} item", "{} items")).toBe("0 items")
expect(Locale.pluralize(5, "{} item", "{} items")).toBe("5 items")
})
})
Loading