1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
# frozen_string_literal: true
RSpec.describe "read_file tool" do
let(:worktree_path) { Dir.mktmpdir("read-file-test") }
let(:context) { { worktree_path: } }
let(:registry) { Dispatch::Tools::Registry.new }
before { Dispatch::Tool::Files.register(registry) }
after { FileUtils.remove_entry(worktree_path) }
subject(:tool) { registry.get("read_file") }
describe "reading a full file" do
it "returns file contents with line numbers" do
File.write(File.join(worktree_path, "hello.txt"), "line one\nline two\nline three\n")
result = tool.call({ "path" => "hello.txt" }, context:)
expect(result.success?).to be true
expect(result.output).to include("line one")
expect(result.output).to include("line two")
expect(result.output).to include("line three")
end
it "prefixes each line with its line number" do
File.write(File.join(worktree_path, "numbered.txt"), "alpha\nbeta\ngamma\n")
result = tool.call({ "path" => "numbered.txt" }, context:)
expect(result.success?).to be true
lines = result.output.split("\n")
expect(lines[0]).to match(/\A\s*0.*alpha/)
expect(lines[1]).to match(/\A\s*1.*beta/)
expect(lines[2]).to match(/\A\s*2.*gamma/)
end
end
describe "reading a line range" do
before do
content = (0..9).map { |i| "line #{i}" }.join("\n") + "\n"
File.write(File.join(worktree_path, "lines.txt"), content)
end
it "returns only the specified line range (0-based)" do
result = tool.call({ "path" => "lines.txt", "start_line" => 2, "end_line" => 4 }, context:)
expect(result.success?).to be true
expect(result.output).to include("line 2")
expect(result.output).to include("line 3")
expect(result.output).to include("line 4")
expect(result.output).not_to include("line 1")
expect(result.output).not_to include("line 5")
end
it "reads from start_line to end of file when end_line is -1" do
result = tool.call({ "path" => "lines.txt", "start_line" => 8, "end_line" => -1 }, context:)
expect(result.success?).to be true
expect(result.output).to include("line 8")
expect(result.output).to include("line 9")
expect(result.output).not_to include("line 7")
end
it "reads from the beginning when only end_line is specified" do
result = tool.call({ "path" => "lines.txt", "end_line" => 1 }, context:)
expect(result.success?).to be true
expect(result.output).to include("line 0")
expect(result.output).to include("line 1")
expect(result.output).not_to include("line 2")
end
end
describe "error cases" do
it "returns failure when the file does not exist" do
result = tool.call({ "path" => "nonexistent.txt" }, context:)
expect(result.failure?).to be true
expect(result.error).to match(/not found|does not exist/i)
end
it "returns failure when the path escapes the sandbox" do
result = tool.call({ "path" => "../../../etc/passwd" }, context:)
expect(result.failure?).to be true
expect(result.error).to match(/sandbox|outside/i)
end
it "returns failure for a binary file" do
binary_path = File.join(worktree_path, "binary.bin")
File.write(binary_path, "Hello\x00World\x00Binary\x00Content")
result = tool.call({ "path" => "binary.bin" }, context:)
expect(result.failure?).to be true
expect(result.error).to match(/binary/i)
end
end
describe "parameter validation" do
it "requires the path parameter" do
result = tool.call({}, context:)
expect(result.failure?).to be true
end
end
end
|