Skip to content

Commit 1ac29f6

Browse files
bobbyjohnstxclaude
andcommitted
test: add tests for JSON sanitization, model size config, ECONNREFUSED handling, context warnings
Add focused unit tests for 7 recent features: 1. JSON sanitization (sanitizeToolCallJson) - strips markdown fences, fixes trailing commas 2. Model size config override (modelSizeB) - config size field takes priority over name parsing 3. ECONNREFUSED error classification - friendly message mentioning Ollama/vLLM 4. Capability gating (toolcall: false) - models without toolcall skip tool injection 5. Provider model size field - config size flows from config to provider model 6. Auto-compact on model switch - detects context overflow when switching to smaller model 7. Context window warning - warns for models under 8K context All tests validate logic without requiring full Effect service stack. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ebf7de3 commit 1ac29f6

6 files changed

Lines changed: 550 additions & 2 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { test, expect, describe } from "bun:test"
2+
3+
// Test the pure logic for model switch auto-compact and context warnings
4+
// From src/cli/cmd/run/runtime.ts lines 329-379
5+
6+
describe("model switch auto-compact logic", () => {
7+
test("detects context overflow when switching to smaller model", () => {
8+
const currentTokens = 20000
9+
const newModelContext = 8192
10+
// From runtime.ts line 357-359: usableContext calculation
11+
const outputReserve = 4096
12+
const usableContext = Math.max(0, newModelContext - outputReserve)
13+
14+
expect(currentTokens >= usableContext).toBe(true) // should compact
15+
})
16+
17+
test("no overflow when switching to larger model", () => {
18+
const currentTokens = 5000
19+
const newModelContext = 32768
20+
const outputReserve = 4096
21+
const usableContext = Math.max(0, newModelContext - outputReserve)
22+
23+
expect(currentTokens >= usableContext).toBe(false) // no compact needed
24+
})
25+
26+
test("edge case: current usage exactly at limit triggers compact", () => {
27+
const currentTokens = 4096
28+
const newModelContext = 8192
29+
const outputReserve = 4096
30+
const usableContext = Math.max(0, newModelContext - outputReserve)
31+
32+
expect(currentTokens >= usableContext).toBe(true) // should compact
33+
})
34+
35+
test("edge case: current usage just under limit does not trigger compact", () => {
36+
const currentTokens = 4095
37+
const newModelContext = 8192
38+
const outputReserve = 4096
39+
const usableContext = Math.max(0, newModelContext - outputReserve)
40+
41+
expect(currentTokens >= usableContext).toBe(false) // no compact
42+
})
43+
44+
test("handles model with limit.input instead of limit.output", () => {
45+
const currentTokens = 100000
46+
const limitInput = 120000
47+
const inputReserve = 20000
48+
const usableContext = Math.max(0, limitInput - inputReserve)
49+
50+
expect(currentTokens >= usableContext).toBe(true) // should compact
51+
})
52+
53+
test("very large model with plenty of headroom", () => {
54+
const currentTokens = 50000
55+
const newModelContext = 1000000 // 1M context
56+
const outputReserve = 4096
57+
const usableContext = Math.max(0, newModelContext - outputReserve)
58+
59+
expect(currentTokens >= usableContext).toBe(false) // no compact needed
60+
})
61+
62+
test("tiny model with minimal context", () => {
63+
const currentTokens = 1000
64+
const newModelContext = 2048
65+
const outputReserve = 4096
66+
const usableContext = Math.max(0, newModelContext - outputReserve)
67+
68+
// usableContext will be 0 (2048 - 4096 = negative, clamped to 0)
69+
expect(usableContext).toBe(0)
70+
expect(currentTokens >= usableContext).toBe(true) // should compact
71+
})
72+
})
73+
74+
describe("context window warning logic", () => {
75+
test("warns for models under 8K context", () => {
76+
const contextLimit = 4096
77+
const threshold = 8192
78+
79+
expect(contextLimit < threshold).toBe(true) // should warn
80+
})
81+
82+
test("no warning for models at exactly 8K context", () => {
83+
const contextLimit = 8192
84+
const threshold = 8192
85+
86+
expect(contextLimit < threshold).toBe(false) // no warning
87+
})
88+
89+
test("no warning for models above 8K context", () => {
90+
const contextLimit = 32768
91+
const threshold = 8192
92+
93+
expect(contextLimit < threshold).toBe(false) // no warning
94+
})
95+
96+
test("warns for very small models (2K)", () => {
97+
const contextLimit = 2048
98+
const threshold = 8192
99+
100+
expect(contextLimit < threshold).toBe(true) // should warn
101+
})
102+
103+
test("no warning for large models (128K)", () => {
104+
const contextLimit = 131072 // 128K
105+
const threshold = 8192
106+
107+
expect(contextLimit < threshold).toBe(false) // no warning
108+
})
109+
110+
test("edge case: 8191 should warn", () => {
111+
const contextLimit = 8191
112+
const threshold = 8192
113+
114+
expect(contextLimit < threshold).toBe(true) // should warn
115+
})
116+
117+
test("edge case: 8193 should not warn", () => {
118+
const contextLimit = 8193
119+
const threshold = 8192
120+
121+
expect(contextLimit < threshold).toBe(false) // no warning
122+
})
123+
124+
test("context limit of 0 should warn", () => {
125+
const contextLimit = 0
126+
const threshold = 8192
127+
128+
expect(contextLimit < threshold).toBe(true) // should warn
129+
})
130+
131+
test("computes correct K value for warning message", () => {
132+
const contextLimit = 4096
133+
const contextK = Math.round(contextLimit / 1024)
134+
135+
expect(contextK).toBe(4)
136+
})
137+
138+
test("computes K value for 2K model", () => {
139+
const contextLimit = 2048
140+
const contextK = Math.round(contextLimit / 1024)
141+
142+
expect(contextK).toBe(2)
143+
})
144+
145+
test("computes K value for odd sizes", () => {
146+
const contextLimit = 7168 // 7K
147+
const contextK = Math.round(contextLimit / 1024)
148+
149+
expect(contextK).toBe(7)
150+
})
151+
})

‎packages/tinycode/test/provider/provider.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,3 +1632,32 @@ it.instance(
16321632
expect(providers[ProviderID.openai]).toBeUndefined()
16331633
}),
16341634
)
1635+
1636+
it.instance(
1637+
"config model size field is preserved on provider model",
1638+
Effect.gen(function* () {
1639+
const providers = yield* list
1640+
const model = providers[ProviderID.make("size-test")].models["llama-7b"]
1641+
expect(model.size).toBe(7)
1642+
}),
1643+
{
1644+
config: {
1645+
provider: {
1646+
"size-test": {
1647+
name: "Size Test Provider",
1648+
npm: "@ai-sdk/openai-compatible",
1649+
env: [],
1650+
models: {
1651+
"llama-7b": {
1652+
name: "Llama 7B",
1653+
tool_call: true,
1654+
limit: { context: 8192, output: 2048 },
1655+
size: 7,
1656+
},
1657+
},
1658+
options: { apiKey: "test" },
1659+
},
1660+
},
1661+
},
1662+
},
1663+
)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { test, expect, describe } from "bun:test"
2+
3+
// Test the ECONNREFUSED error classification logic
4+
// This tests the logic from src/session/message-v2.ts around line 1131-1147
5+
6+
type SystemError = Error & {
7+
code?: string
8+
syscall?: string
9+
}
10+
11+
// Recreate the error classification logic from message-v2.ts
12+
function classifyError(e: unknown): { friendly: boolean; message: string } {
13+
if ((e as SystemError)?.code === "ECONNREFUSED") {
14+
return {
15+
friendly: true,
16+
message:
17+
"Connection refused — the model provider is not reachable. Check that Ollama or vLLM is running.",
18+
}
19+
}
20+
return {
21+
friendly: false,
22+
message: e instanceof Error ? e.message : String(e),
23+
}
24+
}
25+
26+
describe("ECONNREFUSED error handling", () => {
27+
test("error with ECONNREFUSED code returns friendly message", () => {
28+
const error: SystemError = new Error("connect ECONNREFUSED 127.0.0.1:11434")
29+
error.code = "ECONNREFUSED"
30+
error.syscall = "connect"
31+
32+
const result = classifyError(error)
33+
expect(result.friendly).toBe(true)
34+
expect(result.message).toContain("Connection refused")
35+
expect(result.message).toContain("Ollama")
36+
expect(result.message).toContain("vLLM")
37+
})
38+
39+
test("error with ECONNREFUSED in code field only", () => {
40+
const error = new Error("Some network error") as SystemError
41+
error.code = "ECONNREFUSED"
42+
43+
const result = classifyError(error)
44+
expect(result.friendly).toBe(true)
45+
expect(result.message).toContain("model provider is not reachable")
46+
})
47+
48+
test("non-connection error passes through unchanged", () => {
49+
const error = new Error("Some other error")
50+
51+
const result = classifyError(error)
52+
expect(result.friendly).toBe(false)
53+
expect(result.message).toBe("Some other error")
54+
})
55+
56+
test("ENOTFOUND error is not classified as ECONNREFUSED", () => {
57+
const error = new Error("getaddrinfo ENOTFOUND") as SystemError
58+
error.code = "ENOTFOUND"
59+
60+
const result = classifyError(error)
61+
expect(result.friendly).toBe(false)
62+
expect(result.message).toBe("getaddrinfo ENOTFOUND")
63+
})
64+
65+
test("ETIMEDOUT error is not classified as ECONNREFUSED", () => {
66+
const error = new Error("connect ETIMEDOUT") as SystemError
67+
error.code = "ETIMEDOUT"
68+
69+
const result = classifyError(error)
70+
expect(result.friendly).toBe(false)
71+
expect(result.message).toBe("connect ETIMEDOUT")
72+
})
73+
74+
test("generic network error without code field", () => {
75+
const error = new Error("fetch failed")
76+
77+
const result = classifyError(error)
78+
expect(result.friendly).toBe(false)
79+
expect(result.message).toBe("fetch failed")
80+
})
81+
82+
test("non-Error object is stringified", () => {
83+
const result = classifyError("string error")
84+
expect(result.friendly).toBe(false)
85+
expect(result.message).toBe("string error")
86+
})
87+
88+
test("null error is stringified", () => {
89+
const result = classifyError(null)
90+
expect(result.friendly).toBe(false)
91+
expect(result.message).toBe("null")
92+
})
93+
94+
test("undefined error is stringified", () => {
95+
const result = classifyError(undefined)
96+
expect(result.friendly).toBe(false)
97+
expect(result.message).toBe("undefined")
98+
})
99+
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { test, expect, describe } from "bun:test"
2+
import type { Provider } from "@/provider/provider"
3+
4+
// Test the capability gating logic for toolcall: false
5+
// This tests the logic from src/session/llm/request.ts line 139
6+
7+
describe("capability gating - toolcall: false", () => {
8+
test("models without toolcall capability should skip tool injection", () => {
9+
const model = {
10+
capabilities: { toolcall: false },
11+
} as Provider.Model
12+
13+
// The logic in request.ts line 139:
14+
// const tools = input.model.capabilities.toolcall === false ? {} : resolveTools(input)
15+
const shouldSkipTools = model.capabilities.toolcall === false
16+
expect(shouldSkipTools).toBe(true)
17+
})
18+
19+
test("models with toolcall capability should include tools", () => {
20+
const model = {
21+
capabilities: { toolcall: true },
22+
} as Provider.Model
23+
24+
const shouldSkipTools = model.capabilities.toolcall === false
25+
expect(shouldSkipTools).toBe(false)
26+
})
27+
28+
test("models with undefined toolcall capability should include tools", () => {
29+
const model = {
30+
capabilities: {},
31+
} as Provider.Model
32+
33+
const shouldSkipTools = model.capabilities.toolcall === false
34+
expect(shouldSkipTools).toBe(false)
35+
})
36+
37+
test("models with null capabilities should not skip tools", () => {
38+
const model = {
39+
capabilities: { toolcall: null as any },
40+
} as Provider.Model
41+
42+
const shouldSkipTools = model.capabilities.toolcall === false
43+
expect(shouldSkipTools).toBe(false)
44+
})
45+
46+
test("validates the exact condition used in request.ts", () => {
47+
// The exact condition from request.ts:
48+
// input.model.capabilities.toolcall === false
49+
const testCases = [
50+
{ toolcall: false, expected: true },
51+
{ toolcall: true, expected: false },
52+
{ toolcall: undefined, expected: false },
53+
{ toolcall: null, expected: false },
54+
{ toolcall: 0, expected: false },
55+
{ toolcall: "", expected: false },
56+
]
57+
58+
for (const { toolcall, expected } of testCases) {
59+
const model = {
60+
capabilities: { toolcall: toolcall as any },
61+
} as Provider.Model
62+
63+
const result = model.capabilities.toolcall === false
64+
expect(result).toBe(expected)
65+
}
66+
})
67+
})

0 commit comments

Comments
 (0)