fix: keep node file content byte-faithful in node_file_get

Splitting the file with keepends=False and rejoining with newlines
dropped the trailing newline of the last line (and every \r of CRLF
files), so returned content was shorter than the file on disk and did
not round-trip. Split with keepends=True and join the selected lines
verbatim; pagination semantics are unchanged.
This commit is contained in:
YueGuobin 2026-08-25 21:33:21 +08:00
parent 57b5baed7f
commit 636abde16c
No known key found for this signature in database
2 changed files with 49 additions and 3 deletions

View File

@ -474,23 +474,26 @@ def get_node_file_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> d
raw = raw[:MAX_NODE_FILE_BYTES]
truncated = True
lines = raw.splitlines(keepends=False)
# keepends keeps the content byte-faithful: the trailing newline of the
# last line and any \r\n endings survive the round trip
lines = raw.splitlines(keepends=True)
total_lines = len(lines)
# Apply offset/limit
selected = lines[offset: offset + limit] if offset < total_lines else []
has_more = (offset + limit) < total_lines or truncated
content = "".join(selected)
return {
"file_path": file_path,
"content": "\n".join(selected),
"content": content,
"metadata": {
"total_lines": total_lines,
"total_bytes": total_bytes,
"offset": offset,
"limit": limit,
"returned_lines": len(selected),
"returned_bytes": len("\n".join(selected).encode("utf-8")),
"returned_bytes": len(content.encode("utf-8")),
"truncated": truncated or has_more,
"has_more": has_more,
},

View File

@ -242,6 +242,49 @@ class TestNode:
result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, ctx)
assert "command" in result
@staticmethod
def _file_conn(text):
conn = _mock_conn()
conn.http_call.return_value.text = text
return conn
def test_file_get_keeps_trailing_newline(self, ctx):
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler
with patch(f"{AH}._get_connector") as m:
m.return_value = self._file_conn("line1\nline2\n")
result = get_node_file_handler({"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg"}, ctx)
assert result["content"] == "line1\nline2\n"
assert result["metadata"]["total_bytes"] == 12
assert result["metadata"]["returned_bytes"] == 12
assert result["metadata"]["has_more"] is False
def test_file_get_keeps_crlf_endings(self, ctx):
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler
with patch(f"{AH}._get_connector") as m:
m.return_value = self._file_conn("line1\r\nline2\r\n")
result = get_node_file_handler({"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg"}, ctx)
assert result["content"] == "line1\r\nline2\r\n"
assert result["metadata"]["returned_bytes"] == 14
def test_file_get_without_trailing_newline(self, ctx):
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler
with patch(f"{AH}._get_connector") as m:
m.return_value = self._file_conn("line1\nline2")
result = get_node_file_handler({"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg"}, ctx)
assert result["content"] == "line1\nline2"
def test_file_get_pagination(self, ctx):
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler
with patch(f"{AH}._get_connector") as m:
m.return_value = self._file_conn("line1\nline2\nline3\n")
result = get_node_file_handler(
{"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg", "offset": 1, "limit": 1}, ctx
)
assert result["content"] == "line2\n"
assert result["metadata"]["total_lines"] == 3
assert result["metadata"]["returned_lines"] == 1
assert result["metadata"]["has_more"] is True
# ── Link ────────────────────────────────────────────────────────────────