Skip to content

Commit 0ee958a

Browse files
authored
test: add python MCP bootstrap spec (#1457)
## What changed - add `docker/mcp-bootstrap-spec.py` as a small Python spec for the MCP bootstrap behavior - refactor `docker/run-all.bats` to compare `run-all.sh` output and generated artifacts against that spec - keep the existing docs/path checks while replacing the repetitive MCP matrix assertions with spec-based checks ## Why it changed The MCP bootstrap behavior in `docker/run-all.sh` is easy to drift as the matrix grows. This keeps the runtime implementation in shell, but moves the expected behavior model into a compact, testable Python script that acts as a second implementation for the tests. ## User impact No runtime behavior change. This only strengthens tests for the Tempo/Grafana MCP bootstrap combinations. ## Validation - `python3 -m py_compile docker/mcp-bootstrap-spec.py` - `bats docker/run-all.bats` - `bats docker/run-otelcol.bats docker/run-all.bats` Stacked on top of #1385 via base branch `tempo-mcp`.
1 parent fb49c7f commit 0ee958a

8 files changed

Lines changed: 218 additions & 116 deletions

File tree

.github/renovate-tracked-deps.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"cosign"
4646
]
4747
},
48-
".github/workflows/lint.yml": {
48+
".github/workflows/ci.yml": {
4949
"regex": [
5050
"mise"
5151
]
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
2-
name: Lint
2+
name: CI
33

44
on:
55
pull_request:
66

77
permissions: {}
88

99
jobs:
10-
lint:
10+
ci:
1111
runs-on: ubuntu-24.04
1212

1313
permissions:

docker/run-all.bats

Lines changed: 98 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -92,145 +92,141 @@ run_run_all() {
9292
timeout 3s bash ./run-all.sh
9393
}
9494

95+
run_mcp_case() {
96+
local tempo_enabled=$1
97+
local sa_mode=$2
98+
local version=${3:-latest}
99+
if [[ "$tempo_enabled" == "true" ]]; then
100+
export TEMPO_EXTRA_ARGS="--query-frontend.mcp-server.enabled=true"
101+
else
102+
unset TEMPO_EXTRA_ARGS
103+
fi
104+
export STUB_SA_MODE="$sa_mode"
105+
run run_run_all "$version"
106+
}
107+
108+
assert_contains() {
109+
local needle=$1
110+
[[ "$output" == *"$needle"* ]]
111+
}
112+
113+
assert_not_contains() {
114+
local needle=$1
115+
[[ "$output" != *"$needle"* ]]
116+
}
117+
118+
assert_has_file() {
119+
[ -f "$1" ]
120+
}
121+
122+
assert_no_file() {
123+
[ ! -f "$1" ]
124+
}
125+
126+
assert_file_contains() {
127+
grep -Fq "$2" "$1"
128+
}
129+
130+
assert_file_not_contains() {
131+
! grep -Fq "$2" "$1"
132+
}
133+
95134
@test "docs URL uses main for latest" {
96135
local expected="https://github.com/grafana/docker-otel-lgtm/blob/main/docs/mcp-integration.md"
97-
local expected_line=" Docs: $expected"
98136
run run_run_all latest
99-
[[ "$output" == *"$expected_line"* ]]
100-
[[ "$output" != *"/blob/vlatest/"* ]]
137+
assert_contains " Docs: $expected"
138+
assert_not_contains "/blob/vlatest/"
101139
}
102140

103141
@test "docs URL uses main when version is empty" {
104142
local expected="https://github.com/grafana/docker-otel-lgtm/blob/main/docs/mcp-integration.md"
105-
local expected_line=" Docs: $expected"
106143
run run_run_all ""
107-
[[ "$output" == *"$expected_line"* ]]
144+
assert_contains " Docs: $expected"
108145
}
109146

110147
@test "docs URL uses main for main tag" {
111148
local expected="https://github.com/grafana/docker-otel-lgtm/blob/main/docs/mcp-integration.md"
112-
local expected_line=" Docs: $expected"
113149
run run_run_all main
114-
[[ "$output" == *"$expected_line"* ]]
115-
[[ "$output" != *"/blob/vmain/"* ]]
150+
assert_contains " Docs: $expected"
151+
assert_not_contains "/blob/vmain/"
116152
}
117153

118154
@test "printed MCP commands escape configurable paths" {
119155
local configdir="$TESTDIR/etc/lgtm with spaces"
120156
local escaped_configdir=${configdir// /\\ }
121157
CONFIGDIR="$configdir" run run_run_all latest
122-
[[ "$output" == *"bash <(docker exec lgtm cat ${escaped_configdir}/claude-mcp-setup.sh)"* ]]
123-
[[ "$output" == *"docker exec lgtm cat ${escaped_configdir}/mcp.json"* ]]
158+
assert_contains "bash <(docker exec lgtm cat ${escaped_configdir}/claude-mcp-setup.sh)"
159+
assert_contains "docker exec lgtm cat ${escaped_configdir}/mcp.json"
124160
}
125161

126162
@test "docs URL prefixes bare release version with v" {
127163
local expected
128164
expected="https://github.com/grafana/docker-otel-lgtm/blob/v1.2.3-test/docs/mcp-integration.md"
129-
local expected_line=" Docs: $expected"
130165
run run_run_all 1.2.3-test
131-
[[ "$output" == *"$expected_line"* ]]
166+
assert_contains " Docs: $expected"
132167
}
133168

134169
@test "docs URL does not double-prefix version that already starts with v" {
135170
local expected
136171
expected="https://github.com/grafana/docker-otel-lgtm/blob/v1.2.3-test/docs/mcp-integration.md"
137-
local expected_line=" Docs: $expected"
138172
run run_run_all v1.2.3-test
139-
[[ "$output" == *"$expected_line"* ]]
140-
[[ "$output" != *"/blob/vv1.2.3-test/"* ]]
173+
assert_contains " Docs: $expected"
174+
assert_not_contains "/blob/vv1.2.3-test/"
141175
}
142176

143-
@test "MCP bootstrap writes helper artifacts with expected contents" {
144-
export TEMPO_EXTRA_ARGS="--query-frontend.mcp-server.enabled=true"
145-
export STUB_SA_MODE=success
146-
147-
run run_run_all latest
148-
[ -f "$CONFIGDIR/mcp.json" ]
149-
[ -f "$CONFIGDIR/claude-mcp-setup.sh" ]
150-
[ -f "$TOKENFILE" ]
151-
152-
grep -Fq \
153-
'"GRAFANA_URL": "http://localhost:3000"' \
154-
"$CONFIGDIR/mcp.json"
155-
grep -Fq \
156-
'"GRAFANA_SERVICE_ACCOUNT_TOKEN": "token123"' \
157-
"$CONFIGDIR/mcp.json"
158-
grep -Fq '"url": "http://localhost:3200/api/mcp"' "$CONFIGDIR/mcp.json"
159-
160-
grep -Fq \
161-
'claude mcp add grafana -e "GRAFANA_URL=http://localhost:3000"' \
162-
"$CONFIGDIR/claude-mcp-setup.sh"
163-
grep -Fq 'GRAFANA_SERVICE_ACCOUNT_TOKEN=token123' "$CONFIGDIR/claude-mcp-setup.sh"
164-
grep -Fq \
165-
'claude mcp add --transport http tempo "http://localhost:3200/api/mcp"' \
166-
"$CONFIGDIR/claude-mcp-setup.sh"
167-
168-
grep -Fqx 'token123' "$TOKENFILE"
177+
@test "tempo enabled with service account writes both MCP servers" {
178+
run_mcp_case true success latest
179+
assert_contains "Tempo MCP: server enabled at http://localhost:3200/api/mcp"
180+
assert_contains "Grafana MCP: server enabled with service account token"
181+
assert_contains " - 3200: Tempo endpoint (MCP at http://localhost:3200/api/mcp)"
182+
assert_has_file "$CONFIGDIR/mcp.json"
183+
assert_has_file "$CONFIGDIR/claude-mcp-setup.sh"
184+
assert_has_file "$TOKENFILE"
185+
assert_file_contains "$CONFIGDIR/mcp.json" '"grafana"'
186+
assert_file_contains "$CONFIGDIR/mcp.json" '"tempo"'
187+
assert_file_contains "$CONFIGDIR/mcp.json" 'GRAFANA_SERVICE_ACCOUNT_TOKEN": "token123"'
188+
assert_file_contains "$CONFIGDIR/claude-mcp-setup.sh" 'claude mcp add grafana'
189+
assert_file_contains "$CONFIGDIR/claude-mcp-setup.sh" 'claude mcp add --transport http tempo'
190+
assert_file_contains "$TOKENFILE" 'token123'
169191
}
170192

171-
@test "enabled tempo with service account writes both MCP servers" {
172-
export TEMPO_EXTRA_ARGS="--query-frontend.mcp-server.enabled=true"
173-
export STUB_SA_MODE=success
174-
175-
run run_run_all latest
176-
[[ "$output" == *"Tempo MCP: server enabled at http://localhost:3200/api/mcp"* ]]
177-
[[ "$output" == *"Grafana MCP: server enabled with service account token"* ]]
178-
[[ "$output" == *" - 3200: Tempo endpoint (MCP at http://localhost:3200/api/mcp)"* ]]
179-
180-
grep -Fq '"grafana"' "$CONFIGDIR/mcp.json"
181-
grep -Fq '"tempo"' "$CONFIGDIR/mcp.json"
182-
grep -Fq 'GRAFANA_SERVICE_ACCOUNT_TOKEN": "token123"' "$CONFIGDIR/mcp.json"
183-
grep -Fq 'claude mcp add grafana' "$CONFIGDIR/claude-mcp-setup.sh"
184-
grep -Fq \
185-
'claude mcp add --transport http tempo "http://localhost:3200/api/mcp"' \
186-
"$CONFIGDIR/claude-mcp-setup.sh"
187-
grep -Fqx 'token123' "$TOKENFILE"
193+
@test "tempo disabled with service account writes grafana-only MCP config" {
194+
run_mcp_case false success latest
195+
assert_contains "Tempo MCP: server disabled; enable with"
196+
assert_contains "TEMPO_EXTRA_ARGS=--query-frontend.mcp-server.enabled=true"
197+
assert_contains "Grafana MCP: server enabled with service account token"
198+
assert_contains " - 3200: Tempo endpoint"
199+
assert_not_contains " - 3200: Tempo endpoint (MCP at http://localhost:3200/api/mcp)"
200+
assert_has_file "$CONFIGDIR/mcp.json"
201+
assert_has_file "$CONFIGDIR/claude-mcp-setup.sh"
202+
assert_has_file "$TOKENFILE"
203+
assert_file_contains "$CONFIGDIR/mcp.json" '"grafana"'
204+
assert_file_not_contains "$CONFIGDIR/mcp.json" '"tempo"'
205+
assert_file_contains "$CONFIGDIR/claude-mcp-setup.sh" 'claude mcp add grafana'
206+
assert_file_not_contains "$CONFIGDIR/claude-mcp-setup.sh" 'claude mcp add --transport http tempo'
188207
}
189208

190-
@test "disabled tempo with service account writes grafana-only MCP config" {
191-
unset TEMPO_EXTRA_ARGS
192-
export STUB_SA_MODE=success
193-
194-
run run_run_all latest
195-
[[ "$output" == *"Tempo MCP: server disabled;"* ]]
196-
[[ "$output" == *"TEMPO_EXTRA_ARGS=--query-frontend.mcp-server.enabled=true"* ]]
197-
[[ "$output" == *"Grafana MCP: server enabled with service account token"* ]]
198-
[[ "$output" == *" - 3200: Tempo endpoint"* ]]
199-
[[ "$output" != *" - 3200: Tempo endpoint (MCP at http://localhost:3200/api/mcp)"* ]]
200-
201-
grep -Fq '"grafana"' "$CONFIGDIR/mcp.json"
202-
run ! grep -Fq '"tempo"' "$CONFIGDIR/mcp.json"
203-
grep -Fq 'claude mcp add grafana' "$CONFIGDIR/claude-mcp-setup.sh"
204-
run ! grep -Fq 'claude mcp add --transport http tempo' "$CONFIGDIR/claude-mcp-setup.sh"
209+
@test "tempo enabled without service account writes tempo-only MCP config" {
210+
run_mcp_case true missing latest
211+
assert_contains "Tempo MCP: server enabled at http://localhost:3200/api/mcp"
212+
assert_contains "Grafana MCP: server unavailable; could not create service account token"
213+
assert_has_file "$CONFIGDIR/mcp.json"
214+
assert_has_file "$CONFIGDIR/claude-mcp-setup.sh"
215+
assert_no_file "$TOKENFILE"
216+
assert_file_not_contains "$CONFIGDIR/mcp.json" '"grafana"'
217+
assert_file_contains "$CONFIGDIR/mcp.json" '"tempo"'
218+
assert_file_not_contains "$CONFIGDIR/claude-mcp-setup.sh" 'claude mcp add grafana'
219+
assert_file_contains "$CONFIGDIR/claude-mcp-setup.sh" 'claude mcp add --transport http tempo'
205220
}
206221

207-
@test "enabled tempo without service account writes tempo-only MCP config" {
208-
export TEMPO_EXTRA_ARGS="--query-frontend.mcp-server.enabled=true"
209-
export STUB_SA_MODE=missing
210-
211-
run run_run_all latest
212-
[[ "$output" == *"Tempo MCP: server enabled at http://localhost:3200/api/mcp"* ]]
213-
[[ "$output" == *"Grafana MCP: server unavailable; could not create service account token"* ]]
214-
215-
run ! grep -Fq '"grafana"' "$CONFIGDIR/mcp.json"
216-
grep -Fq '"tempo"' "$CONFIGDIR/mcp.json"
217-
run ! grep -Fq 'claude mcp add grafana' "$CONFIGDIR/claude-mcp-setup.sh"
218-
grep -Fq \
219-
'claude mcp add --transport http tempo "http://localhost:3200/api/mcp"' \
220-
"$CONFIGDIR/claude-mcp-setup.sh"
221-
[ ! -f "$TOKENFILE" ]
222-
}
223-
224-
@test "disabled tempo without service account writes empty MCP config" {
225-
unset TEMPO_EXTRA_ARGS
226-
export STUB_SA_MODE=missing
227-
228-
run run_run_all latest
229-
[[ "$output" == *"Tempo MCP: server disabled;"* ]]
230-
[[ "$output" == *"TEMPO_EXTRA_ARGS=--query-frontend.mcp-server.enabled=true"* ]]
231-
[[ "$output" == *"Grafana MCP: server unavailable; could not create service account token"* ]]
232-
233-
grep -Fq '"mcpServers": {}' "$CONFIGDIR/mcp.json"
234-
run ! grep -Fq 'claude mcp add ' "$CONFIGDIR/claude-mcp-setup.sh"
235-
[ ! -f "$TOKENFILE" ]
222+
@test "tempo disabled without service account writes empty MCP config" {
223+
run_mcp_case false missing latest
224+
assert_contains "Tempo MCP: server disabled; enable with"
225+
assert_contains "TEMPO_EXTRA_ARGS=--query-frontend.mcp-server.enabled=true"
226+
assert_contains "Grafana MCP: server unavailable; could not create service account token"
227+
assert_has_file "$CONFIGDIR/mcp.json"
228+
assert_has_file "$CONFIGDIR/claude-mcp-setup.sh"
229+
assert_no_file "$TOKENFILE"
230+
assert_file_contains "$CONFIGDIR/mcp.json" '"mcpServers": {}'
231+
assert_file_not_contains "$CONFIGDIR/claude-mcp-setup.sh" 'claude mcp add '
236232
}

mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ FLINT_CONFIG_DIR = ".github/config"
3232

3333
[tasks."test:unit"]
3434
description = "Run unit tests"
35-
run = "bats docker/run-otelcol.bats docker/run-all.bats"
35+
run = "bats docker/run-otelcol.bats docker/run-all.bats run-lgtm.bats"
3636

3737
[tasks."ci"]
3838
description = "Run all checks (lint + unit tests)"

run-lgtm.bats

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env bats
2+
bats_require_minimum_version 1.5.0
3+
4+
setup() {
5+
TESTDIR=$(mktemp -d)
6+
BINDIR="$TESTDIR/bin"
7+
mkdir -p "$BINDIR"
8+
mkdir -p "$TESTDIR/work/container"/{grafana,prometheus,loki}
9+
cp "$BATS_TEST_DIRNAME/run-lgtm.sh" "$TESTDIR/work/"
10+
cp "$BATS_TEST_DIRNAME/run-lgtm.ps1" "$TESTDIR/work/"
11+
cp "$BATS_TEST_DIRNAME/run-lgtm.cmd" "$TESTDIR/work/"
12+
13+
for runtime in podman docker; do
14+
cat >"$BINDIR/$runtime" <<'SCRIPT'
15+
#!/usr/bin/env bash
16+
exit 0
17+
SCRIPT
18+
chmod +x "$BINDIR/$runtime"
19+
done
20+
}
21+
22+
teardown() {
23+
rm -rf "$TESTDIR"
24+
}
25+
26+
run_launcher() {
27+
cd "$TESTDIR/work" || return 1
28+
PATH="$BINDIR:$PATH" bash ./run-lgtm.sh "$@" --dry-run
29+
}
30+
31+
assert_output_contains() {
32+
[[ "$output" == *"$1"* ]]
33+
}
34+
35+
@test "dry-run prints preferred runtime and release-tag image" {
36+
run run_launcher 1.2.3 false
37+
[ "$status" -eq 0 ]
38+
assert_output_contains 'runtime=podman'
39+
assert_output_contains 'image=docker.io/grafana/otel-lgtm:1.2.3'
40+
assert_output_contains 'arg=-e'
41+
assert_output_contains 'arg=CONTAINER_RUNTIME=podman'
42+
assert_output_contains 'arg=OTEL_COLLECTOR_DEBUG_EXPORTER='
43+
assert_output_contains 'arg=--env-file'
44+
}
45+
46+
@test "dry-run local image uses requested release tag for preferred runtime" {
47+
run run_launcher 1.2.3 true
48+
[ "$status" -eq 0 ]
49+
assert_output_contains 'image=localhost/grafana/otel-lgtm:1.2.3'
50+
}
51+
52+
@test "dry-run includes OBI flags when enabled from environment" {
53+
cd "$TESTDIR/work" || return 1
54+
run env PATH="$BINDIR:$PATH" ENABLE_OBI=true bash ./run-lgtm.sh latest false --dry-run
55+
[ "$status" -eq 0 ]
56+
assert_output_contains 'arg=--pid=host'
57+
assert_output_contains 'arg=--privileged'
58+
assert_output_contains 'arg=ENABLE_OBI=true'
59+
}
60+
61+
@test "powershell launcher forwards OTEL collector debug exporter in dry-run output" {
62+
grep -Fq 'OTEL_COLLECTOR_DEBUG_EXPORTER=' "$TESTDIR/work/run-lgtm.ps1"
63+
grep -Fq 'if ('"$"'DryRun)' "$TESTDIR/work/run-lgtm.ps1"
64+
}
65+
66+
@test "cmd wrapper passes local image flag and dry-run switch correctly" {
67+
grep -Fq 'set "localimg=%~2"' "$TESTDIR/work/run-lgtm.cmd"
68+
grep -Fq 'if /I "%~3"=="--dry-run" set "dryrun=-DryRun"' "$TESTDIR/work/run-lgtm.cmd"
69+
}

run-lgtm.cmd

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
@echo off
22

33
set "releasetag=%~1"
4-
set "local=%~2"
4+
set "localimg=%~2"
5+
set "dryrun="
56

67
if "%releasetag%"=="" set "releasetag=latest"
78
if "%localimg%"=="" set "localimg=0"
89
if "%localimg%"=="false" set "localimg=0"
910
if "%localimg%"=="true" set "localimg=1"
11+
if /I "%~3"=="--dry-run" set "dryrun=-DryRun"
1012

11-
powershell -ExecutionPolicy ByPass -NoProfile -Command "& '%~dp0\run-lgtm.ps1' -ReleaseTag '%releasetag%' -UseLocalImage %localimg%"
13+
powershell -ExecutionPolicy ByPass -NoProfile -Command "& '%~dp0\run-lgtm.ps1' -ReleaseTag '%releasetag%' -UseLocalImage %localimg% %dryrun%"
1214
exit /b %ERRORLEVEL%

0 commit comments

Comments
 (0)