Skip to content

Commit d10303c

Browse files
authored
test: MCP config CRUD + Locale utility coverage, found duration >=24h bug (#369)
Add 16 tests for MCP config operations (resolveConfigPath, addMcpToConfig, removeMcpFromConfig, listMcpInConfig, findAllConfigPaths) — all previously untested. Add 13 tests for Locale utilities (number, duration, truncateMiddle, pluralize). Discovered and filed #368: Locale.duration >=24h branch has days/hours swapped, always showing "0d Xh". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> https://claude.ai/code/session_0144wYQpTz6tEZgSYUnzns2A
1 parent 87ceda2 commit d10303c

File tree

2 files changed

+247
-0
lines changed

2 files changed

+247
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { tmpdir } from "../fixture/fixture"
3+
import { mkdir, writeFile, readFile } from "fs/promises"
4+
import path from "path"
5+
import {
6+
resolveConfigPath,
7+
addMcpToConfig,
8+
removeMcpFromConfig,
9+
listMcpInConfig,
10+
findAllConfigPaths,
11+
} from "../../src/mcp/config"
12+
13+
describe("MCP config: resolveConfigPath", () => {
14+
test("returns .altimate-code subdir config when it exists", async () => {
15+
await using tmp = await tmpdir()
16+
const configDir = path.join(tmp.path, ".altimate-code")
17+
await mkdir(configDir, { recursive: true })
18+
await writeFile(path.join(configDir, "altimate-code.json"), "{}")
19+
const result = await resolveConfigPath(tmp.path)
20+
expect(result).toBe(path.join(configDir, "altimate-code.json"))
21+
})
22+
23+
test("prefers .altimate-code over .opencode subdir", async () => {
24+
await using tmp = await tmpdir()
25+
await mkdir(path.join(tmp.path, ".altimate-code"), { recursive: true })
26+
await writeFile(path.join(tmp.path, ".altimate-code", "altimate-code.json"), "{}")
27+
await mkdir(path.join(tmp.path, ".opencode"), { recursive: true })
28+
await writeFile(path.join(tmp.path, ".opencode", "opencode.json"), "{}")
29+
const result = await resolveConfigPath(tmp.path)
30+
expect(result).toBe(path.join(tmp.path, ".altimate-code", "altimate-code.json"))
31+
})
32+
33+
test("falls back to root-level config", async () => {
34+
await using tmp = await tmpdir()
35+
await writeFile(path.join(tmp.path, "opencode.json"), "{}")
36+
const result = await resolveConfigPath(tmp.path)
37+
expect(result).toBe(path.join(tmp.path, "opencode.json"))
38+
})
39+
40+
test("returns first candidate path when no config exists", async () => {
41+
await using tmp = await tmpdir()
42+
const result = await resolveConfigPath(tmp.path)
43+
expect(result).toBe(path.join(tmp.path, ".altimate-code", "altimate-code.json"))
44+
})
45+
46+
test("global=true skips subdirectory configs", async () => {
47+
await using tmp = await tmpdir()
48+
await mkdir(path.join(tmp.path, ".altimate-code"), { recursive: true })
49+
await writeFile(path.join(tmp.path, ".altimate-code", "altimate-code.json"), "{}")
50+
await writeFile(path.join(tmp.path, "opencode.json"), "{}")
51+
const result = await resolveConfigPath(tmp.path, true)
52+
expect(result).toBe(path.join(tmp.path, "opencode.json"))
53+
})
54+
})
55+
56+
describe("MCP config: addMcpToConfig + removeMcpFromConfig round-trip", () => {
57+
test("adds MCP server to empty config", async () => {
58+
await using tmp = await tmpdir()
59+
const configPath = path.join(tmp.path, "opencode.json")
60+
await addMcpToConfig("test-server", { type: "local", command: ["node", "server.js"] } as any, configPath)
61+
const content = JSON.parse(await readFile(configPath, "utf-8"))
62+
expect(content.mcp["test-server"]).toMatchObject({ type: "local", command: ["node", "server.js"] })
63+
})
64+
65+
test("adds MCP server to existing config preserving other fields", async () => {
66+
await using tmp = await tmpdir()
67+
const configPath = path.join(tmp.path, "opencode.json")
68+
await writeFile(configPath, JSON.stringify({ provider: { default: "anthropic" } }))
69+
await addMcpToConfig("my-server", { type: "remote", url: "https://example.com" } as any, configPath)
70+
const content = JSON.parse(await readFile(configPath, "utf-8"))
71+
expect(content.provider.default).toBe("anthropic")
72+
expect(content.mcp["my-server"].url).toBe("https://example.com")
73+
})
74+
75+
test("remove returns false for nonexistent config file", async () => {
76+
await using tmp = await tmpdir()
77+
const result = await removeMcpFromConfig("nope", path.join(tmp.path, "missing.json"))
78+
expect(result).toBe(false)
79+
})
80+
81+
test("remove returns false for nonexistent server name", async () => {
82+
await using tmp = await tmpdir()
83+
const configPath = path.join(tmp.path, "opencode.json")
84+
await writeFile(configPath, JSON.stringify({ mcp: { existing: { type: "local", command: ["x"] } } }))
85+
const result = await removeMcpFromConfig("nonexistent", configPath)
86+
expect(result).toBe(false)
87+
})
88+
89+
test("add then remove round-trips correctly", async () => {
90+
await using tmp = await tmpdir()
91+
const configPath = path.join(tmp.path, "opencode.json")
92+
await addMcpToConfig("ephemeral", { type: "local", command: ["test"] } as any, configPath)
93+
const listed = await listMcpInConfig(configPath)
94+
expect(listed).toContain("ephemeral")
95+
const removed = await removeMcpFromConfig("ephemeral", configPath)
96+
expect(removed).toBe(true)
97+
const after = await listMcpInConfig(configPath)
98+
expect(after).not.toContain("ephemeral")
99+
})
100+
})
101+
102+
describe("MCP config: listMcpInConfig", () => {
103+
test("returns empty array for missing file", async () => {
104+
await using tmp = await tmpdir()
105+
const result = await listMcpInConfig(path.join(tmp.path, "nope.json"))
106+
expect(result).toEqual([])
107+
})
108+
109+
test("returns empty array for config without mcp key", async () => {
110+
await using tmp = await tmpdir()
111+
const configPath = path.join(tmp.path, "opencode.json")
112+
await writeFile(configPath, JSON.stringify({ provider: {} }))
113+
const result = await listMcpInConfig(configPath)
114+
expect(result).toEqual([])
115+
})
116+
117+
test("lists all server names", async () => {
118+
await using tmp = await tmpdir()
119+
const configPath = path.join(tmp.path, "opencode.json")
120+
await writeFile(
121+
configPath,
122+
JSON.stringify({
123+
mcp: {
124+
alpha: { type: "local", command: ["a"] },
125+
beta: { type: "remote", url: "https://b.com" },
126+
},
127+
}),
128+
)
129+
const result = await listMcpInConfig(configPath)
130+
expect(result).toEqual(expect.arrayContaining(["alpha", "beta"]))
131+
expect(result).toHaveLength(2)
132+
})
133+
})
134+
135+
describe("MCP config: findAllConfigPaths", () => {
136+
test("returns paths from both project and global dirs", async () => {
137+
await using projTmp = await tmpdir()
138+
await using globalTmp = await tmpdir()
139+
await writeFile(path.join(projTmp.path, "opencode.json"), "{}")
140+
await writeFile(path.join(globalTmp.path, "altimate-code.json"), "{}")
141+
const result = await findAllConfigPaths(projTmp.path, globalTmp.path)
142+
expect(result).toContain(path.join(projTmp.path, "opencode.json"))
143+
expect(result).toContain(path.join(globalTmp.path, "altimate-code.json"))
144+
})
145+
146+
test("includes project subdirs but not global subdirs", async () => {
147+
await using projTmp = await tmpdir()
148+
await using globalTmp = await tmpdir()
149+
// Create config in project .opencode subdir
150+
await mkdir(path.join(projTmp.path, ".opencode"), { recursive: true })
151+
await writeFile(path.join(projTmp.path, ".opencode", "opencode.json"), "{}")
152+
// Create config in global .opencode subdir (should NOT be found)
153+
await mkdir(path.join(globalTmp.path, ".opencode"), { recursive: true })
154+
await writeFile(path.join(globalTmp.path, ".opencode", "opencode.json"), "{}")
155+
const result = await findAllConfigPaths(projTmp.path, globalTmp.path)
156+
expect(result).toContain(path.join(projTmp.path, ".opencode", "opencode.json"))
157+
expect(result).not.toContain(path.join(globalTmp.path, ".opencode", "opencode.json"))
158+
})
159+
160+
test("returns empty when no config files exist", async () => {
161+
await using projTmp = await tmpdir()
162+
await using globalTmp = await tmpdir()
163+
const result = await findAllConfigPaths(projTmp.path, globalTmp.path)
164+
expect(result).toEqual([])
165+
})
166+
})
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { Locale } from "../../src/util/locale"
3+
4+
describe("Locale.number", () => {
5+
test("formats millions", () => {
6+
expect(Locale.number(1500000)).toBe("1.5M")
7+
expect(Locale.number(1000000)).toBe("1.0M")
8+
})
9+
10+
test("formats thousands", () => {
11+
expect(Locale.number(1500)).toBe("1.5K")
12+
expect(Locale.number(1000)).toBe("1.0K")
13+
})
14+
15+
test("boundary: 999999 renders as K not M", () => {
16+
expect(Locale.number(999999)).toBe("1000.0K")
17+
})
18+
19+
test("returns raw string for small numbers", () => {
20+
expect(Locale.number(999)).toBe("999")
21+
expect(Locale.number(0)).toBe("0")
22+
})
23+
})
24+
25+
describe("Locale.duration", () => {
26+
test("milliseconds", () => {
27+
expect(Locale.duration(500)).toBe("500ms")
28+
expect(Locale.duration(0)).toBe("0ms")
29+
})
30+
31+
test("seconds", () => {
32+
expect(Locale.duration(1500)).toBe("1.5s")
33+
expect(Locale.duration(2500)).toBe("2.5s")
34+
})
35+
36+
test("minutes and seconds", () => {
37+
expect(Locale.duration(90000)).toBe("1m 30s")
38+
expect(Locale.duration(3599999)).toBe("59m 59s")
39+
})
40+
41+
test("hours and minutes", () => {
42+
expect(Locale.duration(3600000)).toBe("1h 0m")
43+
expect(Locale.duration(5400000)).toBe("1h 30m")
44+
})
45+
46+
// BUG: Locale.duration >=24h has swapped days/hours calculation.
47+
// hours = Math.floor(input / 3600000) gives total hours (25), not remainder.
48+
// days = Math.floor((input % 3600000) / 86400000) always yields 0.
49+
// Correct: days = Math.floor(input / 86400000), hours = Math.floor((input % 86400000) / 3600000)
50+
// 90000000ms = 25h = 1d 1h — should display "1d 1h"
51+
// See: https://github.com/AltimateAI/altimate-code/issues/368
52+
test.skip("FIXME: days and hours for >=24h are calculated correctly", () => {
53+
expect(Locale.duration(90000000)).toBe("1d 1h")
54+
})
55+
})
56+
57+
describe("Locale.truncateMiddle", () => {
58+
test("returns original if short enough", () => {
59+
expect(Locale.truncateMiddle("hello", 35)).toBe("hello")
60+
})
61+
62+
test("truncates long strings with ellipsis in middle", () => {
63+
const long = "abcdefghijklmnopqrstuvwxyz1234567890abcdef"
64+
const result = Locale.truncateMiddle(long, 20)
65+
expect(result.length).toBe(20)
66+
expect(result).toContain("\u2026")
67+
expect(result.startsWith("abcdefghij")).toBe(true)
68+
expect(result.endsWith("bcdef")).toBe(true)
69+
})
70+
})
71+
72+
describe("Locale.pluralize", () => {
73+
test("uses singular for count=1", () => {
74+
expect(Locale.pluralize(1, "{} item", "{} items")).toBe("1 item")
75+
})
76+
77+
test("uses plural for count!=1", () => {
78+
expect(Locale.pluralize(0, "{} item", "{} items")).toBe("0 items")
79+
expect(Locale.pluralize(5, "{} item", "{} items")).toBe("5 items")
80+
})
81+
})

0 commit comments

Comments
 (0)