Skip to content

Commit 1234276

Browse files
committed
Use startLine from API response for real line numbers in fetch artifacts
Pass start_line to _add_line_numbers so symbols/chunks display correct file positions instead of always numbering from 1. Falls back to 1 when startLine is absent for backward compatibility.
1 parent c45174b commit 1234276

2 files changed

Lines changed: 95 additions & 4 deletions

File tree

src/tests/test_fetch_artifacts.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,74 @@
33
import pytest
44
from unittest.mock import AsyncMock, MagicMock, patch
55
from fastmcp import Context
6-
from tools.fetch_artifacts import fetch_artifacts
6+
from tools.fetch_artifacts import fetch_artifacts, _add_line_numbers, _build_artifacts_xml
7+
8+
9+
class TestAddLineNumbers:
10+
"""Test cases for _add_line_numbers helper."""
11+
12+
def test_multi_line_content(self):
13+
content = "line1\nline2\nline3"
14+
result = _add_line_numbers(content)
15+
assert result == "1 | line1\n2 | line2\n3 | line3"
16+
17+
def test_single_line_content(self):
18+
content = "only one line"
19+
result = _add_line_numbers(content)
20+
assert result == "1 | only one line"
21+
22+
def test_empty_content(self):
23+
assert _add_line_numbers("") == ""
24+
25+
def test_right_aligned_padding(self):
26+
lines = "\n".join(f"line{i}" for i in range(100))
27+
result = _add_line_numbers(lines)
28+
first_line = result.split("\n")[0]
29+
assert first_line == " 1 | line0"
30+
last_line = result.split("\n")[99]
31+
assert last_line == "100 | line99"
32+
33+
def test_start_line_offset(self):
34+
result = _add_line_numbers("a\nb", start_line=50)
35+
assert result == "50 | a\n51 | b"
36+
37+
def test_start_line_default(self):
38+
result = _add_line_numbers("x", start_line=1)
39+
assert result == "1 | x"
40+
41+
def test_start_line_right_aligned_padding(self):
42+
result = _add_line_numbers("a\nb\nc", start_line=98)
43+
assert result == " 98 | a\n 99 | b\n100 | c"
44+
45+
def test_start_line_empty_content(self):
46+
assert _add_line_numbers("", start_line=50) == ""
47+
48+
49+
class TestBuildArtifactsXmlStartLine:
50+
"""Test _build_artifacts_xml uses startLine from API response."""
51+
52+
def test_artifact_with_start_line(self):
53+
data = {"artifacts": [
54+
{"identifier": "repo::file.py::func", "content": "line1\nline2", "contentByteSize": 10, "startLine": 50}
55+
]}
56+
result = _build_artifacts_xml(data)
57+
assert "50 | line1" in result
58+
assert "51 | line2" in result
59+
60+
def test_artifact_without_start_line_defaults_to_1(self):
61+
data = {"artifacts": [
62+
{"identifier": "repo::file.py::func", "content": "line1\nline2", "contentByteSize": 10}
63+
]}
64+
result = _build_artifacts_xml(data)
65+
assert "1 | line1" in result
66+
assert "2 | line2" in result
67+
68+
def test_artifact_with_null_start_line_defaults_to_1(self):
69+
data = {"artifacts": [
70+
{"identifier": "repo::file.py", "content": "hello", "contentByteSize": 5, "startLine": None}
71+
]}
72+
result = _build_artifacts_xml(data)
73+
assert "1 | hello" in result
774

875

976
@pytest.mark.asyncio
@@ -52,8 +119,9 @@ async def test_fetch_artifacts_returns_xml(mock_get_api_key):
52119
assert isinstance(result, str)
53120
assert "<artifacts>" in result
54121
assert "</artifacts>" in result
55-
# Found artifact has content
56-
assert "def login(user, pwd):" in result
122+
# Found artifact has line-numbered content
123+
assert "1 | def login(user, pwd):" in result
124+
assert "2 | return True" in result
57125
assert 'contentByteSize="38"' in result
58126
assert 'identifier="owner/repo::src/auth.py::login"' in result
59127
# Not-found artifact is skipped (not in output)
@@ -204,6 +272,8 @@ async def test_fetch_artifacts_escapes_xml(mock_get_api_key):
204272
identifiers=["owner/repo::file.py::func"],
205273
)
206274

275+
# Line numbers are added before escaping
276+
assert "1 | if x &lt; 10 &amp;&amp; y &gt; 5:" in result
207277
assert "&lt;" in result
208278
assert "&amp;" in result
209279
assert "<artifacts>" in result

src/tools/fetch_artifacts.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,25 @@ async def fetch_artifacts(
8585
return f"<error>{error_msg}</error>"
8686

8787

88+
def _add_line_numbers(content: str, start_line: int = 1) -> str:
89+
"""Add line numbers to content for easier navigation.
90+
91+
Returns content with each line prefixed by its line number,
92+
right-aligned and separated by ' | '.
93+
94+
Args:
95+
content: The text content to number.
96+
start_line: 1-based line number for the first line (default 1).
97+
"""
98+
if not content:
99+
return content
100+
101+
lines = content.split("\n")
102+
width = len(str(start_line + len(lines) - 1))
103+
numbered = [f"{start_line + i:>{width}} | {line}" for i, line in enumerate(lines)]
104+
return "\n".join(numbered)
105+
106+
88107
def _build_artifacts_xml(data: dict) -> str:
89108
"""Build XML representation of fetched artifacts.
90109
@@ -106,7 +125,9 @@ def _build_artifacts_xml(data: dict) -> str:
106125
attrs = [f'identifier="{identifier}"']
107126
if content_byte_size is not None:
108127
attrs.append(f'contentByteSize="{content_byte_size}"')
109-
escaped_content = html.escape(content)
128+
start_line = artifact.get("startLine") or 1
129+
numbered_content = _add_line_numbers(content, start_line)
130+
escaped_content = html.escape(numbered_content)
110131
xml_parts.append(f' <artifact {" ".join(attrs)}>{escaped_content}</artifact>')
111132

112133
xml_parts.append("</artifacts>")

0 commit comments

Comments
 (0)