Skip to content

Commit 76392cc

Browse files
committed
test: add comprehensive coverage for modelLimits feature
Add test suite for modelLimits including schema validation, config validation, and wildcard matching. Ensures correct behavior for exact and wildcard patterns, percentage parsing, and edge cases.
1 parent 387c335 commit 76392cc

File tree

3 files changed

+313
-0
lines changed

3 files changed

+313
-0
lines changed

tests/config-model-limits.test.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import assert from "node:assert"
2+
import { describe, it } from "node:test"
3+
import { getInvalidConfigKeys, validateConfigTypes } from "../lib/config"
4+
5+
function createConfig(modelLimits?: Record<string, number | string>) {
6+
return {
7+
enabled: true,
8+
debug: false,
9+
pruneNotification: "minimal",
10+
pruneNotificationType: "chat",
11+
commands: {
12+
enabled: true,
13+
protectedTools: [],
14+
},
15+
turnProtection: {
16+
enabled: false,
17+
turns: 0,
18+
},
19+
protectedFilePatterns: [],
20+
tools: {
21+
settings: {
22+
nudgeEnabled: true,
23+
nudgeFrequency: 5,
24+
protectedTools: [],
25+
contextLimit: "60%",
26+
...(modelLimits !== undefined ? { modelLimits } : {}),
27+
},
28+
distill: {
29+
permission: "allow",
30+
showDistillation: false,
31+
},
32+
compress: {
33+
permission: "deny",
34+
showCompression: false,
35+
},
36+
prune: {
37+
permission: "allow",
38+
},
39+
},
40+
strategies: {
41+
deduplication: {
42+
enabled: true,
43+
protectedTools: [],
44+
},
45+
supersedeWrites: {
46+
enabled: true,
47+
},
48+
purgeErrors: {
49+
enabled: true,
50+
turns: 4,
51+
protectedTools: [],
52+
},
53+
},
54+
}
55+
}
56+
57+
describe("Config Validation - modelLimits", () => {
58+
it("accepts valid modelLimits configuration", () => {
59+
const config = createConfig({
60+
"anthropic/claude-3.5-sonnet": "70%",
61+
"anthropic/claude-3-opus": 150000,
62+
"gpt-4": "80%",
63+
})
64+
65+
const errors = validateConfigTypes(config)
66+
assert.strictEqual(errors.length, 0)
67+
})
68+
69+
it("rejects invalid modelLimits string value", () => {
70+
const config = createConfig({
71+
"anthropic/claude-3.5-sonnet": "invalid",
72+
})
73+
74+
const errors = validateConfigTypes(config)
75+
assert.ok(
76+
errors.some(
77+
(error) => error.key === "tools.settings.modelLimits.anthropic/claude-3.5-sonnet",
78+
),
79+
)
80+
})
81+
82+
it("rejects modelLimits when not an object", () => {
83+
const config = createConfig()
84+
;(config.tools.settings as any).modelLimits = "not-an-object"
85+
86+
const errors = validateConfigTypes(config)
87+
assert.ok(errors.some((error) => error.key === "tools.settings.modelLimits"))
88+
})
89+
90+
it("works without modelLimits", () => {
91+
const config = createConfig()
92+
93+
const errors = validateConfigTypes(config)
94+
assert.strictEqual(errors.length, 0)
95+
})
96+
97+
it("rejects malformed percentage strings", () => {
98+
const config = createConfig({
99+
model1: "abc%",
100+
model2: "50 %",
101+
model3: "%50",
102+
model4: "50.5.5%",
103+
})
104+
105+
const errors = validateConfigTypes(config)
106+
assert.ok(errors.some((error) => error.key === "tools.settings.modelLimits.model1"))
107+
assert.ok(errors.some((error) => error.key === "tools.settings.modelLimits.model2"))
108+
assert.ok(errors.some((error) => error.key === "tools.settings.modelLimits.model3"))
109+
assert.ok(errors.some((error) => error.key === "tools.settings.modelLimits.model4"))
110+
})
111+
112+
it("rejects strings without percent suffix", () => {
113+
const config = createConfig({ model: "50" })
114+
115+
const errors = validateConfigTypes(config)
116+
assert.ok(errors.some((error) => error.key === "tools.settings.modelLimits.model"))
117+
})
118+
119+
it("rejects empty strings", () => {
120+
const config = createConfig({ model: "" })
121+
122+
const errors = validateConfigTypes(config)
123+
assert.ok(errors.some((error) => error.key === "tools.settings.modelLimits.model"))
124+
})
125+
126+
it("accepts boundary percentages and numbers", () => {
127+
const config = createConfig({
128+
p0: "0%",
129+
p100: "100%",
130+
n0: 0,
131+
negative: -50000,
132+
above100: "150%",
133+
decimal: "50.5%",
134+
huge: 1000000000000,
135+
})
136+
137+
const errors = validateConfigTypes(config)
138+
assert.strictEqual(errors.length, 0)
139+
})
140+
141+
it("rejects modelLimits arrays", () => {
142+
const config = createConfig()
143+
;(config.tools.settings as any).modelLimits = ["not-an-object"]
144+
145+
const errors = validateConfigTypes(config)
146+
assert.ok(errors.some((error) => error.key === "tools.settings.modelLimits"))
147+
})
148+
149+
it("does not flag model-specific keys as unknown config keys", () => {
150+
const config = createConfig({
151+
"anthropic/claude-3.5-sonnet": "70%",
152+
"openai/gpt-4o": 120000,
153+
})
154+
155+
const invalidKeys = getInvalidConfigKeys(config)
156+
assert.strictEqual(invalidKeys.length, 0)
157+
})
158+
})
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import assert from "node:assert"
2+
import { describe, it } from "node:test"
3+
import { findModelLimit } from "../lib/messages/inject"
4+
5+
describe("findModelLimit", () => {
6+
it("prefers exact matches over wildcard matches", () => {
7+
const modelLimits = {
8+
"ollama/zen-1": "35%",
9+
"*/zen-1": "40%",
10+
}
11+
12+
const limit = findModelLimit("ollama/zen-1", modelLimits)
13+
assert.strictEqual(limit, "35%")
14+
})
15+
16+
it("matches provider wildcard patterns", () => {
17+
const modelLimits = {
18+
"*/zen-1": "40%",
19+
}
20+
21+
const limit = findModelLimit("opencode/zen-1", modelLimits)
22+
assert.strictEqual(limit, "40%")
23+
})
24+
25+
it("matches model wildcard patterns", () => {
26+
const modelLimits = {
27+
"ollama/*": "25%",
28+
}
29+
30+
const limit = findModelLimit("ollama/zen-3", modelLimits)
31+
assert.strictEqual(limit, "25%")
32+
})
33+
34+
it("matches substring wildcard patterns", () => {
35+
const modelLimits = {
36+
"*sonnet*": 120000,
37+
}
38+
39+
const limit = findModelLimit("anthropic/claude-3.5-sonnet", modelLimits)
40+
assert.strictEqual(limit, 120000)
41+
})
42+
43+
it("prefers the most specific wildcard pattern", () => {
44+
const modelLimits = {
45+
"*sonnet*": "45%",
46+
"ollama/*": "25%",
47+
}
48+
49+
const limit = findModelLimit("ollama/sonnet", modelLimits)
50+
assert.strictEqual(limit, "25%")
51+
})
52+
53+
it("uses lexical order as deterministic tiebreaker", () => {
54+
const modelLimits = {
55+
"a*": 100,
56+
"*a": 200,
57+
}
58+
59+
const limit = findModelLimit("a", modelLimits)
60+
assert.strictEqual(limit, 200)
61+
})
62+
63+
it("returns undefined when no pattern matches", () => {
64+
const modelLimits = {
65+
"ollama/*": "25%",
66+
}
67+
68+
const limit = findModelLimit("openai/gpt-5", modelLimits)
69+
assert.strictEqual(limit, undefined)
70+
})
71+
})

tests/schema-model-limits.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, it } from "node:test"
2+
import assert from "node:assert"
3+
import { readFile } from "fs/promises"
4+
import { fileURLToPath } from "url"
5+
import { dirname, join } from "path"
6+
7+
const __filename = fileURLToPath(import.meta.url)
8+
const __dirname = dirname(__filename)
9+
10+
describe("Schema Validation - modelLimits", () => {
11+
it("should accept valid modelLimits configuration", async () => {
12+
const schema = JSON.parse(await readFile(join(__dirname, "../dcp.schema.json"), "utf-8"))
13+
const modelLimitsSchema =
14+
schema.properties?.tools?.properties?.settings?.properties?.modelLimits
15+
16+
assert.ok(modelLimitsSchema, "modelLimits field should exist")
17+
assert.strictEqual(modelLimitsSchema.type, "object")
18+
assert.ok(modelLimitsSchema.additionalProperties)
19+
assert.ok(modelLimitsSchema.additionalProperties.oneOf)
20+
assert.strictEqual(modelLimitsSchema.additionalProperties.oneOf.length, 2)
21+
})
22+
23+
it("should support number values in modelLimits", async () => {
24+
const schema = JSON.parse(await readFile(join(__dirname, "../dcp.schema.json"), "utf-8"))
25+
const numberSchema =
26+
schema.properties?.tools?.properties?.settings?.properties?.modelLimits
27+
?.additionalProperties?.oneOf?.[0]
28+
29+
assert.ok(numberSchema, "number schema should exist")
30+
assert.strictEqual(numberSchema.type, "number")
31+
})
32+
33+
it("should support percentage strings in modelLimits", async () => {
34+
const schema = JSON.parse(await readFile(join(__dirname, "../dcp.schema.json"), "utf-8"))
35+
const percentSchema =
36+
schema.properties?.tools?.properties?.settings?.properties?.modelLimits
37+
?.additionalProperties?.oneOf?.[1]
38+
39+
assert.ok(percentSchema, "percentage schema should exist")
40+
assert.strictEqual(percentSchema.type, "string")
41+
assert.ok(percentSchema.pattern)
42+
assert.strictEqual(percentSchema.pattern, "^\\d+(?:\\.\\d+)?%$")
43+
})
44+
45+
// Test valid percentage patterns
46+
it("should accept valid percentage patterns", async () => {
47+
const schema = JSON.parse(await readFile(join(__dirname, "../dcp.schema.json"), "utf-8"))
48+
const pattern =
49+
schema.properties?.tools?.properties?.settings?.properties?.modelLimits
50+
?.additionalProperties?.oneOf?.[1]?.pattern
51+
52+
const validPatterns = ["0%", "50%", "100%", "50.5%", "0.1%", "99.99%", "1000%"]
53+
const regex = new RegExp(pattern)
54+
55+
for (const test of validPatterns) {
56+
assert.ok(regex.test(test), `Should accept: ${test}`)
57+
}
58+
})
59+
60+
it("should reject invalid percentage patterns", async () => {
61+
const schema = JSON.parse(await readFile(join(__dirname, "../dcp.schema.json"), "utf-8"))
62+
const pattern =
63+
schema.properties?.tools?.properties?.settings?.properties?.modelLimits
64+
?.additionalProperties?.oneOf?.[1]?.pattern
65+
66+
const invalidPatterns = [
67+
"abc%", // non-numeric
68+
"50 %", // space before %
69+
"%50", // % before number
70+
"50.5.5%", // multiple decimals
71+
"%%", // no number
72+
"", // empty string
73+
"50", // no %
74+
"-50%", // negative (regex doesn't support -)
75+
".5%", // starts with decimal
76+
"50.%", // decimal without fraction
77+
]
78+
const regex = new RegExp(pattern)
79+
80+
for (const test of invalidPatterns) {
81+
assert.ok(!regex.test(test), `Should reject: ${test}`)
82+
}
83+
})
84+
})

0 commit comments

Comments
 (0)