Skip to content

Commit 5c48253

Browse files
committed
working
1 parent d62a260 commit 5c48253

File tree

4 files changed

+654
-8
lines changed

4 files changed

+654
-8
lines changed
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { validateFileSizeForContext } from "../contextValidator"
3+
import { Task } from "../../task/Task"
4+
import { promises as fs } from "fs"
5+
import { readLines } from "../../../integrations/misc/read-lines"
6+
import * as sharedApi from "../../../shared/api"
7+
8+
vi.mock("fs", () => ({
9+
promises: {
10+
stat: vi.fn(),
11+
},
12+
}))
13+
14+
vi.mock("../../../integrations/misc/read-lines", () => ({
15+
readLines: vi.fn(),
16+
}))
17+
18+
vi.mock("../../../shared/api", () => ({
19+
getModelMaxOutputTokens: vi.fn(),
20+
}))
21+
22+
describe("contextValidator", () => {
23+
let mockTask: Task
24+
25+
beforeEach(() => {
26+
vi.clearAllMocks()
27+
28+
// Mock Task instance
29+
mockTask = {
30+
api: {
31+
getModel: vi.fn().mockReturnValue({
32+
id: "test-model",
33+
info: {
34+
contextWindow: 100000,
35+
maxTokens: 4096,
36+
},
37+
}),
38+
countTokens: vi.fn().mockResolvedValue(1000),
39+
},
40+
getTokenUsage: vi.fn().mockReturnValue({
41+
contextTokens: 10000,
42+
}),
43+
apiConfiguration: {
44+
apiProvider: "anthropic",
45+
},
46+
providerRef: {
47+
deref: vi.fn().mockReturnValue({
48+
getState: vi.fn().mockResolvedValue({}),
49+
}),
50+
},
51+
} as any
52+
53+
// Mock getModelMaxOutputTokens to return a consistent value
54+
vi.mocked(sharedApi.getModelMaxOutputTokens).mockReturnValue(4096)
55+
})
56+
57+
describe("validateFileSizeForContext", () => {
58+
it("should apply 25% buffer to remaining context and read incrementally", async () => {
59+
const mockStats = { size: 50000 }
60+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
61+
62+
// Mock readLines to return content in batches
63+
// Each batch is 100 lines, returning content that results in 1200 tokens per batch
64+
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
65+
const start = startLine ?? 0
66+
const end = endLine ?? 99
67+
const lines = end - start + 1
68+
return `test content line\n`.repeat(lines)
69+
})
70+
71+
// Mock token count - 12 tokens per line (1200 per 100-line batch)
72+
let callCount = 0
73+
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
74+
callCount++
75+
const text = content[0].text
76+
const lines = text.split("\n").length - 1
77+
return lines * 12 // 12 tokens per line
78+
})
79+
80+
const result = await validateFileSizeForContext(
81+
"/test/file.ts",
82+
1000, // totalLines
83+
-1, // currentMaxReadFileLine
84+
mockTask,
85+
)
86+
87+
// New calculation:
88+
// Context window = 100k, current usage = 10k
89+
// Remaining = 90k
90+
// With 25% buffer on remaining: usable = 90k * 0.75 = 67.5k
91+
// Reserved for response ~2k
92+
// Available should be around 65.5k tokens
93+
// File needs 12k tokens total (1000 lines * 12 tokens)
94+
expect(result.shouldLimit).toBe(false)
95+
96+
// Verify readLines was called multiple times (incremental reading)
97+
expect(readLines).toHaveBeenCalled()
98+
99+
// Verify the new calculation approach
100+
const remaining = 100000 - 10000 // 90k remaining
101+
const usableRemaining = remaining * 0.75 // 67.5k with 25% buffer
102+
expect(usableRemaining).toBe(67500)
103+
})
104+
105+
it("should handle different context usage levels correctly", async () => {
106+
const mockStats = { size: 50000 }
107+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
108+
109+
// Mock readLines
110+
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
111+
const start = startLine ?? 0
112+
const end = endLine ?? 99
113+
const lines = end - start + 1
114+
return `test content line\n`.repeat(lines)
115+
})
116+
117+
// Mock token count - 50 tokens per line
118+
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
119+
const text = content[0].text
120+
const lines = text.split("\n").length - 1
121+
return lines * 50
122+
})
123+
124+
// Test with 50% context already used
125+
mockTask.getTokenUsage = vi.fn().mockReturnValue({
126+
contextTokens: 50000, // 50% of 100k context used
127+
})
128+
129+
const result = await validateFileSizeForContext(
130+
"/test/file.ts",
131+
2000, // totalLines
132+
-1,
133+
mockTask,
134+
)
135+
136+
// With 50k remaining and 25% buffer: 50k * 0.75 = 37.5k usable
137+
// Minus ~2k for response = ~35.5k available
138+
// File needs 100k tokens (2000 lines * 50 tokens)
139+
// Should limit the file
140+
expect(result.shouldLimit).toBe(true)
141+
expect(result.safeMaxLines).toBeLessThan(2000)
142+
expect(result.reason).toContain("exceeds available context space")
143+
})
144+
145+
it("should limit file when it exceeds available space with buffer", async () => {
146+
// Set up a scenario where file is too large
147+
const mockStats = { size: 500000 } // Large file
148+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
149+
150+
// Mock readLines to return content in batches
151+
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
152+
const start = startLine ?? 0
153+
const end = endLine ?? 99
154+
const lines = end - start + 1
155+
return `large content line\n`.repeat(lines)
156+
})
157+
158+
// Mock large token count - 100 tokens per line
159+
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
160+
const text = content[0].text
161+
const lines = text.split("\n").length - 1
162+
return lines * 100 // 100 tokens per line
163+
})
164+
165+
const result = await validateFileSizeForContext(
166+
"/test/largefile.ts",
167+
10000, // totalLines
168+
-1,
169+
mockTask,
170+
)
171+
172+
expect(result.shouldLimit).toBe(true)
173+
expect(result.safeMaxLines).toBeGreaterThan(0)
174+
expect(result.safeMaxLines).toBeLessThan(10000) // Should stop before reading all lines
175+
expect(result.reason).toContain("exceeds available context space")
176+
})
177+
178+
it("should handle very large files through incremental reading", async () => {
179+
// Set up a file larger than 50MB
180+
const mockStats = { size: 60_000_000 } // 60MB file
181+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
182+
183+
// Mock readLines to return content in batches
184+
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
185+
const start = startLine ?? 0
186+
const end = endLine ?? 99
187+
const lines = end - start + 1
188+
return `large file content line\n`.repeat(lines)
189+
})
190+
191+
// Mock very high token count per line (simulating dense content)
192+
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
193+
const text = content[0].text
194+
const lines = text.split("\n").length - 1
195+
return lines * 200 // 200 tokens per line for very large file
196+
})
197+
198+
const result = await validateFileSizeForContext(
199+
"/test/hugefile.ts",
200+
100000, // totalLines
201+
-1,
202+
mockTask,
203+
)
204+
205+
expect(result.shouldLimit).toBe(true)
206+
// Should have attempted to read the file incrementally
207+
expect(readLines).toHaveBeenCalled()
208+
// Should stop early due to token limits
209+
expect(result.safeMaxLines).toBeLessThan(1000)
210+
expect(result.reason).toContain("exceeds available context space")
211+
})
212+
213+
it("should handle read failures gracefully", async () => {
214+
const mockStats = { size: 100000 } // 100KB file
215+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
216+
217+
// Mock readLines to fail
218+
vi.mocked(readLines).mockRejectedValue(new Error("Read error"))
219+
220+
const result = await validateFileSizeForContext(
221+
"/test/problematic.ts",
222+
2000, // totalLines
223+
-1,
224+
mockTask,
225+
)
226+
227+
// Should return a safe default when reading fails
228+
expect(result.shouldLimit).toBe(true)
229+
expect(result.safeMaxLines).toBe(50) // Minimum useful lines
230+
})
231+
232+
it("should handle very limited context space", async () => {
233+
const mockStats = { size: 10000 } // 10KB file
234+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
235+
236+
// Set very high context usage
237+
// With new calculation: 100k - 95k = 5k remaining
238+
// 5k * 0.75 = 3.75k usable
239+
// Minus ~2k for response = ~1.75k available
240+
mockTask.getTokenUsage = vi.fn().mockReturnValue({
241+
contextTokens: 95000, // 95% of context used
242+
})
243+
244+
// Mock small token count
245+
mockTask.api.countTokens = vi.fn().mockImplementation(async (content) => {
246+
const text = content[0].text
247+
const lines = text.split("\n").length - 1
248+
return lines * 10 // 10 tokens per line
249+
})
250+
251+
// Mock readLines
252+
vi.mocked(readLines).mockImplementation(async (path, endLine, startLine) => {
253+
const start = startLine ?? 0
254+
const end = endLine ?? 99
255+
const lines = end - start + 1
256+
return `test line\n`.repeat(lines)
257+
})
258+
259+
const result = await validateFileSizeForContext(
260+
"/test/smallfile.ts",
261+
500, // totalLines
262+
-1,
263+
mockTask,
264+
)
265+
266+
expect(result.shouldLimit).toBe(true)
267+
// With the new calculation using full model max tokens (4096),
268+
// we have less space available, so we get the minimum 50 lines
269+
expect(result.safeMaxLines).toBe(50)
270+
expect(result.reason).toContain("Very limited context space")
271+
})
272+
273+
it("should handle negative available space gracefully", async () => {
274+
const mockStats = { size: 10000 } // 10KB file
275+
vi.mocked(fs.stat).mockResolvedValue(mockStats as any)
276+
277+
// Set extremely high context usage
278+
// With 100k - 99k = 1k remaining
279+
// 1k * 0.75 = 750 tokens usable
280+
// Minus 2k for response = negative available space
281+
mockTask.getTokenUsage = vi.fn().mockReturnValue({
282+
contextTokens: 99000, // 99% of context used
283+
})
284+
285+
const result = await validateFileSizeForContext(
286+
"/test/smallfile.ts",
287+
500, // totalLines
288+
-1,
289+
mockTask,
290+
)
291+
292+
expect(result.shouldLimit).toBe(true)
293+
expect(result.safeMaxLines).toBe(50) // Should be limited to minimum useful lines
294+
expect(result.reason).toContain("Very limited context space")
295+
// With negative available space, readLines won't be called
296+
expect(readLines).not.toHaveBeenCalled()
297+
})
298+
299+
it("should limit file when it is too large and would be truncated", async () => {
300+
const filePath = "/test/large-file.ts"
301+
const totalLines = 10000
302+
const currentMaxReadFileLine = -1 // Unlimited
303+
304+
// Set up context to have limited space
305+
mockTask.getTokenUsage = vi.fn().mockReturnValue({
306+
contextTokens: 90000, // 90% of context used
307+
})
308+
309+
// Mock token counting to simulate a large file
310+
mockTask.api.countTokens = vi.fn().mockResolvedValue(1000) // Each batch is 1000 tokens
311+
312+
// Mock readLines to return some content
313+
vi.mocked(readLines).mockResolvedValue("line content")
314+
315+
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
316+
317+
expect(result.shouldLimit).toBe(true)
318+
expect(result.safeMaxLines).toBeGreaterThan(0)
319+
expect(result.safeMaxLines).toBeLessThan(totalLines)
320+
expect(result.reason).toContain("File exceeds available context space")
321+
expect(result.reason).toContain("Use line_range to read specific sections")
322+
})
323+
324+
it("should limit file when very limited context space", async () => {
325+
const filePath = "/test/file.ts"
326+
const totalLines = 1000
327+
const currentMaxReadFileLine = -1
328+
329+
// Mock very high token usage leaving little room
330+
mockTask.getTokenUsage = vi.fn().mockReturnValue({
331+
contextTokens: 98000, // Almost all context used (98% of 100k)
332+
})
333+
334+
// Mock token counting to quickly exceed limit
335+
mockTask.api.countTokens = vi.fn().mockResolvedValue(500) // Each batch uses a lot of tokens
336+
337+
vi.mocked(readLines).mockResolvedValue("line content")
338+
339+
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
340+
341+
expect(result.shouldLimit).toBe(true)
342+
expect(result.reason).toContain("Very limited context space")
343+
expect(result.reason).toContain("Consider using search_files or line_range")
344+
})
345+
346+
it("should not limit when file fits within context", async () => {
347+
const filePath = "/test/small-file.ts"
348+
const totalLines = 100
349+
const currentMaxReadFileLine = -1
350+
351+
// Mock low token usage
352+
mockTask.api.countTokens = vi.fn().mockResolvedValue(10) // Small token count per batch
353+
354+
vi.mocked(readLines).mockResolvedValue("line content")
355+
356+
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
357+
358+
expect(result.shouldLimit).toBe(false)
359+
expect(result.safeMaxLines).toBe(currentMaxReadFileLine)
360+
})
361+
362+
it("should handle errors gracefully", async () => {
363+
const filePath = "/test/error-file.ts"
364+
const totalLines = 20000 // Large file
365+
const currentMaxReadFileLine = -1
366+
367+
// Mock an error in the API
368+
mockTask.api.getModel = vi.fn().mockImplementation(() => {
369+
throw new Error("API Error")
370+
})
371+
372+
const result = await validateFileSizeForContext(filePath, totalLines, currentMaxReadFileLine, mockTask)
373+
374+
// Should fall back to conservative limits
375+
expect(result.shouldLimit).toBe(true)
376+
expect(result.safeMaxLines).toBe(1000)
377+
expect(result.reason).toContain("Large file detected")
378+
})
379+
})
380+
})

0 commit comments

Comments
 (0)