Skip to content

Commit 0387659

Browse files
committed
Create BATS tests for limactl-mcp
Signed-off-by: Jan Dubois <[email protected]>
1 parent 689b928 commit 0387659

File tree

1 file changed

+233
-0
lines changed

1 file changed

+233
-0
lines changed

hack/bats/tests/mcp.bats

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# SPDX-FileCopyrightText: Copyright The Lima Authors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
load "../helpers/load"
5+
6+
NAME=bats
7+
8+
# TODO Move helper functions to shared location
9+
run_yq() {
10+
run -0 --separate-stderr limactl yq "$@"
11+
}
12+
13+
json_edit() {
14+
limactl yq --input-format json --output-format json --indent 0 "$@"
15+
}
16+
17+
# TODO The reusable Lima instance setup is copied from preserve-env.bats
18+
# TODO and should be factored out into helper functions.
19+
local_setup_file() {
20+
if [[ -n "${LIMA_BATS_REUSE_INSTANCE:-}" ]]; then
21+
run limactl list --format '{{.Status}}' "$NAME"
22+
[[ $status == 0 ]] && [[ $output == "Running" ]] && return
23+
fi
24+
limactl unprotect "$NAME" || :
25+
limactl delete --force "$NAME" || :
26+
# Make sure that the host agent doesn't inherit file handles 3 or 4.
27+
# Otherwise bats will not finish until the host agent exits.
28+
limactl start --yes --name "$NAME" template://default 3>&- 4>&-
29+
}
30+
31+
local_teardown_file() {
32+
if [[ -z "${LIMA_BATS_REUSE_INSTANCE:-}" ]]; then
33+
limactl delete --force "$NAME"
34+
fi
35+
}
36+
37+
local_setup() {
38+
coproc MCP { limactl mcp serve "$NAME"; }
39+
40+
ID=0
41+
mcp initialize '{"protocolVersion":"2025-06-18"}'
42+
43+
# Each mcp request should increment the ID
44+
[[ $ID -eq 1 ]]
45+
46+
run_yq .serverInfo.name <<<"$output"
47+
assert_output "lima"
48+
}
49+
50+
local_teardown() {
51+
kill "${MCP_PID:?}" 2>&1 >/dev/null || :
52+
}
53+
54+
mcp() {
55+
local method=$1
56+
local params=${2:-}
57+
58+
local request
59+
printf -v request '{"jsonrpc":"2.0","id":%d,"method":"%s"}' "$((++ID))" "$method"
60+
if [[ -n $params ]]; then
61+
request=$(json_edit ".params=${params}" <<<"$request")
62+
fi
63+
64+
# send request to MCP server stdin
65+
echo "$request" >&"${MCP[1]}"
66+
67+
# read response from MCP server stdout with 5s timeout
68+
local json
69+
read -t 5 -r json <&"${MCP[0]}"
70+
71+
# verify that the response matches the request; also validates the output is valid JSON
72+
run_yq .id <<<"$json"
73+
assert_output "$ID"
74+
75+
# there must be no error object in the response
76+
run_yq .error <<<"$json"
77+
assert_output "null"
78+
79+
# set $output to .result
80+
run_yq .result <<<"$json"
81+
}
82+
83+
tools_call() {
84+
local name=$1
85+
local args=${2:-}
86+
87+
local params
88+
printf -v params '{"name":"%s"}' "$name"
89+
if [[ -n $args ]]; then
90+
params=$(json_edit ".arguments=${args}" <<<"$params")
91+
fi
92+
mcp tools/call "$params"
93+
}
94+
95+
@test 'list tools' {
96+
mcp tools/list
97+
run_yq '.tools[].name' <<<"$output"
98+
assert_line glob
99+
assert_line list_directory
100+
assert_line read_file
101+
assert_line run_shell_command
102+
assert_line search_file_content
103+
assert_line write_file
104+
}
105+
106+
@test 'run shell command returns command output' {
107+
run -0 limactl shell "$NAME" cat /etc/os-release
108+
assert_output
109+
expected=$output
110+
111+
tools_call run_shell_command '{"directory":"/etc","command":["cat","os-release"]}'
112+
json=$output
113+
114+
run_yq '.content[0].type' <<<"$json"
115+
assert_output "text"
116+
117+
# The text property is a string encoding of an embedded JSON object
118+
run_yq '.content[0].text' <<<"$json"
119+
text=$output
120+
121+
run_yq '.exit_code' <<<"$text"
122+
assert_output 0
123+
124+
run_yq '.stdout' <<<"$text"
125+
assert_output "$expected"
126+
127+
run_yq '.stderr' <<<"$text"
128+
refute_output
129+
}
130+
131+
@test 'run shell command returns stderr and exit code' {
132+
tools_call run_shell_command '{"directory":"/","command":["bash","-c","echo NO>&2; exit 13"]}'
133+
134+
# The text property is a string encoding of an embedded JSON object
135+
run_yq '.content[0].text' <<<"$output"
136+
text=$output
137+
138+
run_yq '.exit_code' <<<"$text"
139+
assert_output 13
140+
141+
run_yq '.stdout' <<<"$text"
142+
refute_output
143+
144+
run_yq '.stderr' <<<"$text"
145+
assert_output "NO"
146+
}
147+
148+
@test 'read_file reads a file' {
149+
run -0 limactl shell "$NAME" cat /etc/os-release
150+
assert_output
151+
expected=$output
152+
153+
tools_call read_file '{"path":"/etc/os-release"}'
154+
155+
run_yq '.content[0].text' <<<"$output"
156+
assert_output "$expected"
157+
}
158+
159+
@test 'read_file returns an error when path does not exist' {
160+
tools_call read_file '{"path":"/etc/os-release-info"}'
161+
json=$output
162+
163+
run_yq '.isError' <<<"$json"
164+
assert_output "true"
165+
166+
run_yq '.content[0].text' <<<"$json"
167+
assert_output "file does not exist"
168+
}
169+
170+
@test 'read_file returns an error when path is not absolute' {
171+
tools_call read_file '{"path":"os-release"}'
172+
json=$output
173+
174+
run_yq '.isError' <<<"$json"
175+
assert_output "true"
176+
177+
run_yq '.content[0].text' <<<"$json"
178+
assert_output --partial "expected an absolute path"
179+
}
180+
181+
@test 'write_file creates new file and overwrites existing file' {
182+
limactl shell "$NAME" rm -f /tmp/mcp.test
183+
tools_call write_file '{"path":"/tmp/mcp.test","content":"foo"}'
184+
185+
run_yq '.content[0].text' <<<"$output"
186+
assert_output "OK"
187+
188+
run -0 limactl shell "$NAME" cat /tmp/mcp.test
189+
assert_output "foo"
190+
191+
tools_call write_file '{"path":"/tmp/mcp.test","content":"bar"}'
192+
193+
run_yq '.content[0].text' <<<"$output"
194+
assert_output "OK"
195+
196+
run -0 limactl shell "$NAME" cat /tmp/mcp.test
197+
assert_output "bar"
198+
}
199+
200+
@test 'write_file returns an error when directory does not exist or is not writable' {
201+
limactl shell "$NAME" rm -rf /tmp/tmp
202+
tools_call write_file '{"path":"/tmp/tmp/tmp","content":"tmp"}'
203+
json=$output
204+
205+
run_yq '.content[0].type' <<<"$json"
206+
assert_output "text"
207+
208+
run_yq '.content[0].text' <<<"$json"
209+
assert_output "file does not exist"
210+
211+
limactl shell "$NAME" mkdir -p /tmp/tmp
212+
limactl shell "$NAME" chmod 444 /tmp/tmp
213+
tools_call write_file '{"path":"/tmp/tmp/tmp","content":"tmp"}'
214+
json=$output
215+
216+
run_yq '.isError' <<<"$json"
217+
assert_output "true"
218+
219+
run_yq '.content[0].text' <<<"$json"
220+
assert_output "permission denied"
221+
}
222+
223+
@test 'write_file returns an error when path is not absolute' {
224+
tools_call write_file '{"path":"tmp/mcp.test","content":"baz"}'
225+
json=$output
226+
227+
run_yq '.isError' <<<"$json"
228+
assert_output "true"
229+
230+
run_yq '.content[0].text' <<<"$json"
231+
assert_output --partial "expected an absolute path"
232+
}
233+

0 commit comments

Comments
 (0)