Skip to content

Commit ec6926a

Browse files
ctjlewisclaude
andcommitted
test: close coverage gaps; ci: typecheck + coverage + real-gateway e2e
- serialize: every ToolResultOutput variant; applied: compact + unknown edit fallbacks; ephemeral: empty contents + approval-part tails - store: class-field initializer → constructor (bun counted the initializer as an uninvokable function; now 100/100) - CI on push/PR: unit job runs bun test src/ --coverage, then the e2e job hits the real gateway (AI_GATEWAY_API_KEY repo secret, set beforehand; forks skip — the tests self-skip without a key) 100% function / 99.78% line coverage across src/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c75eb06 commit ec6926a

6 files changed

Lines changed: 80 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- uses: oven-sh/setup-bun@v2
1616
- run: bun install --frozen-lockfile
1717
- run: bun run typecheck
18-
- run: bun test src/
18+
- run: bun test src/ --coverage
1919

2020
e2e:
2121
name: E2E (real gateway)

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3232

3333
# Finder (MacOS) folder config
3434
.DS_Store
35+
coverage

src/breakpoints/ephemeral.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,32 @@ describe("withEphemeralCacheControl", () => {
172172
})
173173
})
174174
})
175+
176+
describe("edge contents", () => {
177+
test("empty content arrays pass through untouched", () => {
178+
const user = withEphemeralCacheControl({ role: "user", content: [] })
179+
expect(user.content).toEqual([])
180+
const assistant = withEphemeralCacheControl({ role: "assistant", content: [] })
181+
expect(assistant.content).toEqual([])
182+
const tool = withEphemeralCacheControl({ role: "tool", content: [] })
183+
expect(tool.content).toEqual([])
184+
})
185+
186+
test("approval-part tails are left untagged (no providerOptions field)", () => {
187+
const assistant = withEphemeralCacheControl({
188+
role: "assistant",
189+
content: [
190+
{ type: "tool-approval-request", approvalId: "ap-1", toolCallId: "tc-1" },
191+
],
192+
} as never)
193+
expect(JSON.stringify(assistant)).not.toContain("cacheControl")
194+
195+
const tool = withEphemeralCacheControl({
196+
role: "tool",
197+
content: [
198+
{ type: "tool-approval-response", approvalId: "ap-1", approved: true },
199+
],
200+
} as never)
201+
expect(JSON.stringify(tool)).not.toContain("cacheControl")
202+
})
203+
})

src/edits/applied.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,14 @@ describe("describeEdit", () => {
5252
expect(describeEdit(CLEAR_THINKING)).toBe(
5353
"cleared 2 thinking turn(s); freed 800 tokens",
5454
)
55+
expect(describeEdit({ type: "compact_20260112" } as AppliedEdit)).toBe(
56+
"compaction applied",
57+
)
58+
})
59+
60+
test("unknown future edit types fall back to a generic line", () => {
61+
expect(describeEdit({ type: "frobnicate_20991231" } as never)).toBe(
62+
"edit applied: frobnicate_20991231",
63+
)
5564
})
5665
})

src/truncation/serialize.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, test } from "bun:test"
2+
3+
import { toolOutputText } from "./serialize"
4+
5+
describe("toolOutputText", () => {
6+
test("text + error-text pass through", () => {
7+
expect(toolOutputText({ type: "text", value: "plain" })).toBe("plain")
8+
expect(toolOutputText({ type: "error-text", value: "boom" })).toBe("boom")
9+
})
10+
11+
test("json + error-json stringify", () => {
12+
expect(toolOutputText({ type: "json", value: { a: 1 } })).toBe('{"a":1}')
13+
expect(toolOutputText({ type: "error-json", value: { err: true } })).toBe(
14+
'{"err":true}',
15+
)
16+
})
17+
18+
test("execution-denied uses the reason, with a fallback", () => {
19+
expect(toolOutputText({ type: "execution-denied", reason: "nope" })).toBe("nope")
20+
expect(toolOutputText({ type: "execution-denied" })).toBe("(execution denied)")
21+
})
22+
23+
test("content arrays join text items and label non-text ones", () => {
24+
expect(
25+
toolOutputText({
26+
type: "content",
27+
value: [
28+
{ type: "text", text: "first" },
29+
{ type: "media", data: "AAAA", mediaType: "image/png" },
30+
{ type: "text", text: "last" },
31+
],
32+
}),
33+
).toBe("first\n[media]\nlast")
34+
})
35+
})

src/truncation/store.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ export interface FullOutputStore {
2121

2222
/** Simple in-memory store; also usable as a per-request cache. */
2323
export class MemoryFullOutputStore implements FullOutputStore {
24-
private readonly outputs = new Map<string, string>()
24+
private readonly outputs: Map<string, string>
25+
26+
constructor() {
27+
this.outputs = new Map()
28+
}
2529

2630
set(id: string, body: string): void {
2731
this.outputs.set(id, body)

0 commit comments

Comments
 (0)