Files
continue/core/tools/implementations/readFileRange.integration.vitest.ts
continue[bot] d70bdbbf28 Fix readFileRange Kotlin Int overflow in IntelliJ plugin (#8976)
* Fix readFileRange Kotlin Int overflow in IntelliJ plugin

Replace Number.MAX_SAFE_INTEGER with Int.MAX_VALUE (2147483647) to prevent
JSON deserialization errors in IntelliJ plugins.

The issue occurred because JavaScript's Number.MAX_SAFE_INTEGER (2^53 - 1)
exceeds Kotlin's Int maximum value (2^31 - 1), causing the following error:
'java.lang.NumberFormatException: Expected an int but was 9007199254740991'

This change ensures compatibility with Kotlin's Int type while still reading
to the end of each line as intended.

Fixes #8517

Co-authored-by: dallin <dallin@continue.dev>
Generated with [Continue](https://continue.dev)

Co-Authored-By: Continue <noreply@continue.dev>

* Trigger CI re-run

The previous CI failure was a flaky test in the CLI extension, unrelated to our changes.

Co-authored-by: dallin <dallin@continue.dev>
Generated with [Continue](https://continue.dev)

Co-Authored-By: Continue <noreply@continue.dev>

* Extract magic number into MAX_CHAR_POSITION constant

Improve code maintainability by defining the Kotlin Int.MAX_VALUE as a
named constant with clear documentation.

Co-authored-by: dallin <dallin@continue.dev>
Generated with [Continue](https://continue.dev)

Co-Authored-By: Continue <noreply@continue.dev>

* Trigger CI re-run for flaky tests

Flaky UI tests failing intermittently on macOS (Node 18, 20) but passing
on all other platforms. Tests are unrelated to readFileRange changes.

Co-authored-by: dallin <dallin@continue.dev>
Generated with [Continue](https://continue.dev)

Co-Authored-By: Continue <noreply@continue.dev>

* Fix flaky CLI UI tests on macOS

Increase timeouts for UI rendering tests on macOS to prevent race conditions.
The tests were failing intermittently on macOS with Node 18/20 due to
insufficient wait times for UI stabilization.

Changes:
- Double timeouts on macOS in TUIChat.fileSearch.test.tsx
- Add extra 100ms wait on macOS in TUIChat.slashCommands.test.tsx
- Tests now pass consistently across all platforms

Co-authored-by: dallin <dallin@continue.dev>
Generated with [Continue](https://continue.dev)

Co-Authored-By: Continue <noreply@continue.dev>

* fix: revert test changes

---------

Co-authored-by: continue[bot] <continue[bot]@users.noreply.github.com>
Co-authored-by: Continue <noreply@continue.dev>
Co-authored-by: Dallin Romney <dallinromney@gmail.com>
2025-12-05 12:13:39 -08:00

205 lines
5.6 KiB
TypeScript

import { expect, test, vi } from "vitest";
import { ToolExtras } from "../..";
import { MAX_CHAR_POSITION, readFileRangeImpl } from "./readFileRange";
// Mock the dependencies
vi.mock("../../util/ideUtils", () => ({
resolveRelativePathInDir: vi.fn(),
}));
vi.mock("../../util/uri", () => ({
getUriPathBasename: vi.fn(),
}));
vi.mock("./readFileLimit", () => ({
throwIfFileExceedsHalfOfContext: vi.fn(),
}));
test("readFileRangeImpl handles out-of-bounds ranges gracefully", async () => {
const { resolveRelativePathInDir } = await import("../../util/ideUtils");
const { getUriPathBasename } = await import("../../util/uri");
const { throwIfFileExceedsHalfOfContext } = await import("./readFileLimit");
// Mock the utility functions
vi.mocked(resolveRelativePathInDir).mockResolvedValue("file:///test.txt");
vi.mocked(getUriPathBasename).mockReturnValue("test.txt");
vi.mocked(throwIfFileExceedsHalfOfContext).mockResolvedValue(undefined);
// Test case 1: Start line beyond end of file
const mockIdeOutOfBounds = {
readRangeInFile: vi.fn().mockResolvedValue(""), // IDE returns empty string
};
const mockExtras1 = {
ide: mockIdeOutOfBounds,
config: { selectedModelByRole: { chat: { contextLength: 8192 } } },
} as unknown as ToolExtras;
const result1 = await readFileRangeImpl(
{
filepath: "test.txt",
startLine: 100, // Beyond end of file
endLine: 105,
},
mockExtras1,
);
expect(result1).toHaveLength(1);
expect(result1[0].content).toBe("");
expect(result1[0].description).toBe("test.txt (lines 100-105)");
// Test case 2: End line beyond end of file
const mockIdePartialRange = {
readRangeInFile: vi.fn().mockResolvedValue("line5\nline6"), // IDE returns available content
};
const mockExtras2 = {
ide: mockIdePartialRange,
config: { selectedModelByRole: { chat: { contextLength: 8192 } } },
} as unknown as ToolExtras;
const result2 = await readFileRangeImpl(
{
filepath: "test.txt",
startLine: 5,
endLine: 100, // Beyond end of file
},
mockExtras2,
);
expect(result2).toHaveLength(1);
expect(result2[0].content).toBe("line5\nline6");
expect(result2[0].description).toBe("test.txt (lines 5-100)");
// Verify that IDE methods were called with correct 0-based ranges
expect(mockIdeOutOfBounds.readRangeInFile).toHaveBeenCalledWith(
"file:///test.txt",
{
start: { line: 99, character: 0 }, // 100 - 1
end: { line: 104, character: MAX_CHAR_POSITION }, // 105 - 1
},
);
expect(mockIdePartialRange.readRangeInFile).toHaveBeenCalledWith(
"file:///test.txt",
{
start: { line: 4, character: 0 }, // 5 - 1
end: { line: 99, character: MAX_CHAR_POSITION }, // 100 - 1
},
);
});
test("readFileRangeImpl validates line number constraints", async () => {
const mockExtras = {
ide: { readRangeInFile: vi.fn(), readFile: vi.fn() },
config: { selectedModelByRole: { chat: { contextLength: 8192 } } },
} as unknown as ToolExtras;
// Test startLine < 1 (invalid)
await expect(
readFileRangeImpl(
{
filepath: "test.txt",
startLine: 0,
endLine: 5,
},
mockExtras,
),
).rejects.toThrow("startLine must be 1 or greater");
// Test negative startLine (no longer supported)
await expect(
readFileRangeImpl(
{
filepath: "test.txt",
startLine: -1,
endLine: 5,
},
mockExtras,
),
).rejects.toThrow("Negative line numbers are not supported");
// Test endLine < 1 (invalid)
await expect(
readFileRangeImpl(
{
filepath: "test.txt",
startLine: 1,
endLine: 0,
},
mockExtras,
),
).rejects.toThrow("endLine must be 1 or greater");
// Test negative endLine (no longer supported)
await expect(
readFileRangeImpl(
{
filepath: "test.txt",
startLine: 1,
endLine: -1,
},
mockExtras,
),
).rejects.toThrow("Negative line numbers are not supported");
// Test endLine < startLine
await expect(
readFileRangeImpl(
{
filepath: "test.txt",
startLine: 10,
endLine: 5,
},
mockExtras,
),
).rejects.toThrow(
"endLine (5) must be greater than or equal to startLine (10)",
);
});
test("readFileRangeImpl handles normal ranges correctly", async () => {
const { resolveRelativePathInDir } = await import("../../util/ideUtils");
const { getUriPathBasename } = await import("../../util/uri");
const { throwIfFileExceedsHalfOfContext } = await import("./readFileLimit");
vi.mocked(resolveRelativePathInDir).mockResolvedValue("file:///test.txt");
vi.mocked(getUriPathBasename).mockReturnValue("test.txt");
vi.mocked(throwIfFileExceedsHalfOfContext).mockResolvedValue(undefined);
const mockIde = {
readRangeInFile: vi.fn().mockResolvedValue("line2\nline3\nline4"),
};
const mockExtras = {
ide: mockIde,
config: { selectedModelByRole: { chat: { contextLength: 8192 } } },
} as unknown as ToolExtras;
const result = await readFileRangeImpl(
{
filepath: "src/test.py",
startLine: 2,
endLine: 4,
},
mockExtras,
);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
name: "test.txt",
description: "src/test.py (lines 2-4)",
content: "line2\nline3\nline4",
uri: {
type: "file",
value: "file:///test.txt",
},
});
// Verify correct 0-based conversion
expect(mockIde.readRangeInFile).toHaveBeenCalledWith("file:///test.txt", {
start: { line: 1, character: 0 }, // 2 - 1
end: { line: 3, character: MAX_CHAR_POSITION }, // 4 - 1
});
});