Skip to content

Commit a5542d5

Browse files
authored
Merge pull request #6 from bx33661/codex/dev
Codex/dev
2 parents b639d5f + 46e8127 commit a5542d5

8 files changed

Lines changed: 252 additions & 16 deletions

File tree

.github/workflows/ci.yml

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ name: CI
22

33
on:
44
push:
5-
branches: [ "master", "main" ]
5+
branches: [ "master", "main", "codex/**" ]
66
pull_request:
77
branches: [ "master", "main" ]
8+
workflow_dispatch:
89

910
jobs:
1011
lint:
@@ -32,9 +33,11 @@ jobs:
3233
run: mypy --package wireshark_mcp --ignore-missing-imports --no-namespace-packages
3334

3435
test:
35-
runs-on: ubuntu-latest
36+
runs-on: ${{ matrix.os }}
3637
strategy:
38+
fail-fast: false
3739
matrix:
40+
os: [ubuntu-latest, windows-latest, macos-latest]
3841
python-version: ["3.10", "3.11", "3.12", "3.13"]
3942

4043
steps:
@@ -50,15 +53,72 @@ jobs:
5053
python -m pip install --upgrade pip
5154
pip install .[dev]
5255
56+
- name: Run tests with coverage
57+
run: |
58+
pytest tests/ --cov=wireshark_mcp --cov-report=term-missing -v
59+
60+
- name: Compile check
61+
run: python -m compileall src/
62+
63+
integration-linux:
64+
runs-on: ubuntu-latest
65+
steps:
66+
- uses: actions/checkout@v6
67+
68+
- name: Set up Python
69+
uses: actions/setup-python@v6
70+
with:
71+
python-version: "3.12"
72+
73+
- name: Install dependencies
74+
run: |
75+
python -m pip install --upgrade pip
76+
pip install .[dev]
77+
5378
- name: Install TShark
5479
run: |
5580
sudo apt-get update
5681
sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y tshark
5782
tshark -v
5883
59-
- name: Run tests with coverage
84+
- name: Run Linux TShark integration smoke tests
6085
run: |
61-
pytest tests/ --cov=wireshark_mcp --cov-report=term-missing -v
86+
pytest tests/test_client.py -k "real_tshark" -v
6287
63-
- name: Compile check
64-
run: python -m compileall src/
88+
package-smoke:
89+
runs-on: ${{ matrix.os }}
90+
strategy:
91+
fail-fast: false
92+
matrix:
93+
os: [ubuntu-latest, windows-latest, macos-latest]
94+
95+
steps:
96+
- uses: actions/checkout@v6
97+
98+
- name: Set up Python
99+
uses: actions/setup-python@v6
100+
with:
101+
python-version: "3.12"
102+
103+
- name: Install build tools
104+
run: |
105+
python -m pip install --upgrade pip
106+
pip install build
107+
108+
- name: Build package
109+
run: python -m build
110+
111+
- name: Validate packaged skill files
112+
run: |
113+
python -c "import glob, os, zipfile; wheel = max(glob.glob('dist/*.whl'), key=os.path.getmtime); names = set(zipfile.ZipFile(wheel).namelist()); assert any(name.endswith('wireshark_mcp/skills/wireshark-traffic-analysis/SKILL.md') for name in names), 'Skill package missing from wheel'"
114+
115+
- name: Install built wheel
116+
run: |
117+
python -c "import glob, os, subprocess, sys; wheel = max(glob.glob('dist/*.whl'), key=os.path.getmtime); subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--force-reinstall', wheel])"
118+
119+
- name: Run packaged CLI smoke tests
120+
run: |
121+
wireshark-mcp --version
122+
python -m wireshark_mcp.server --version
123+
wireshark-mcp --config
124+
wireshark-mcp --doctor

.github/workflows/publish.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ jobs:
2626
2727
- name: Build package
2828
run: python -m build
29+
30+
- name: Validate built wheel contents
31+
run: |
32+
python -c "import glob, os, zipfile; wheel = max(glob.glob('dist/*.whl'), key=os.path.getmtime); names = set(zipfile.ZipFile(wheel).namelist()); assert any(name.endswith('wireshark_mcp/skills/wireshark-traffic-analysis/SKILL.md') for name in names), 'Skill package missing from wheel'"
33+
34+
- name: Install built wheel
35+
run: |
36+
python -c "import glob, os, subprocess, sys; wheel = max(glob.glob('dist/*.whl'), key=os.path.getmtime); subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--force-reinstall', wheel])"
37+
38+
- name: Run release smoke tests
39+
run: |
40+
wireshark-mcp --version
41+
python -m wireshark_mcp.server --version
42+
wireshark-mcp --config
43+
wireshark-mcp --doctor
2944
3045
- name: Publish to PyPI
3146
uses: pypa/gh-action-pypi-publish@release/v1

src/wireshark_mcp/tshark/client.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ def _require_tool(self, tool_name: str) -> dict[str, Any]:
126126
},
127127
}
128128

129+
def _get_checked_tool_path(self, tool_name: str) -> str:
130+
"""Return a tool path after availability has already been validated."""
131+
tool_path = self._tool_paths.get(tool_name)
132+
if not self._tool_is_available(tool_path):
133+
raise RuntimeError(f"{tool_name} tool not available")
134+
assert tool_path is not None
135+
return tool_path
136+
137+
def _select_capture_backend_path(self) -> str:
138+
"""Return the resolved executable for the preferred capture backend."""
139+
return self._get_checked_tool_path(self._select_capture_backend())
140+
129141
# --- Validation Methods ---
130142

131143
def _validate_file(self, filepath: str) -> dict[str, Any]:
@@ -212,6 +224,7 @@ async def check_capabilities(self) -> dict[str, Any]:
212224
async def get_version(tool_path: str | None) -> dict[str, Any]:
213225
if not self._tool_is_available(tool_path):
214226
return {"available": False}
227+
assert tool_path is not None
215228
try:
216229
proc = await asyncio.create_subprocess_exec(
217230
tool_path,
@@ -234,7 +247,7 @@ async def get_version(tool_path: str | None) -> dict[str, Any]:
234247

235248
async def list_interfaces(self) -> str:
236249
"""List interfaces (-D)."""
237-
backend = self.dumpcap_path if self._tool_is_available(self.dumpcap_path) else self.tshark_path
250+
backend = self._select_capture_backend_path()
238251
return await self._run_command([backend, "-D"])
239252

240253
# --- Capture Management ---
@@ -253,7 +266,7 @@ async def capture_packets(
253266
if not output_validation["success"]:
254267
return json.dumps(output_validation)
255268

256-
backend = self.dumpcap_path if self._tool_is_available(self.dumpcap_path) else self.tshark_path
269+
backend = self._select_capture_backend_path()
257270
cmd = [backend, "-i", interface, "-w", output_file]
258271

259272
if capture_filter:
@@ -666,7 +679,8 @@ async def get_file_info(self, pcap_file: str) -> str:
666679
if not required["success"]:
667680
return json.dumps(required)
668681

669-
return await self._run_command([self.capinfos_path, pcap_file])
682+
capinfos_path = self._get_checked_tool_path("capinfos")
683+
return await self._run_command([capinfos_path, pcap_file])
670684

671685
async def merge_pcap_files(self, output_file: str, input_files: list[str]) -> str:
672686
"""Mergecap: Merge multiple pcaps."""
@@ -683,7 +697,8 @@ async def merge_pcap_files(self, output_file: str, input_files: list[str]) -> st
683697
if not output_validation["success"]:
684698
return json.dumps(output_validation)
685699

686-
cmd = [self.mergecap_path, "-w", output_file] + input_files
700+
mergecap_path = self._get_checked_tool_path("mergecap")
701+
cmd = [mergecap_path, "-w", output_file] + input_files
687702
return await self._run_command(cmd)
688703

689704
async def editcap_trim(
@@ -706,7 +721,8 @@ async def editcap_trim(
706721
if not output_validation["success"]:
707722
return json.dumps(output_validation)
708723

709-
cmd = [self.editcap_path]
724+
editcap_path = self._get_checked_tool_path("editcap")
725+
cmd = [editcap_path]
710726
if start_time:
711727
cmd.extend(["-A", start_time])
712728
if stop_time:
@@ -745,7 +761,8 @@ async def editcap_split(
745761
}
746762
)
747763

748-
cmd = [self.editcap_path]
764+
editcap_path = self._get_checked_tool_path("editcap")
765+
cmd = [editcap_path]
749766
if packets_per_file > 0:
750767
cmd.extend(["-c", str(packets_per_file)])
751768
if seconds_per_file > 0:
@@ -767,7 +784,8 @@ async def editcap_time_shift(self, input_file: str, output_file: str, seconds: f
767784
if not output_validation["success"]:
768785
return json.dumps(output_validation)
769786

770-
cmd = [self.editcap_path, "-t", str(seconds), input_file, output_file]
787+
editcap_path = self._get_checked_tool_path("editcap")
788+
cmd = [editcap_path, "-t", str(seconds), input_file, output_file]
771789
return await self._run_command(cmd)
772790

773791
async def editcap_deduplicate(self, input_file: str, output_file: str, duplicate_window: int = 5) -> str:
@@ -784,7 +802,8 @@ async def editcap_deduplicate(self, input_file: str, output_file: str, duplicate
784802
if not output_validation["success"]:
785803
return json.dumps(output_validation)
786804

787-
cmd = [self.editcap_path, "-D", str(duplicate_window), input_file, output_file]
805+
editcap_path = self._get_checked_tool_path("editcap")
806+
cmd = [editcap_path, "-D", str(duplicate_window), input_file, output_file]
788807
return await self._run_command(cmd)
789808

790809
async def text2pcap_import(
@@ -808,7 +827,8 @@ async def text2pcap_import(
808827
if not output_validation["success"]:
809828
return json.dumps(output_validation)
810829

811-
cmd = [self.text2pcap_path]
830+
text2pcap_path = self._get_checked_tool_path("text2pcap")
831+
cmd = [text2pcap_path]
812832
if timestamp_format:
813833
cmd.extend(["-t", timestamp_format])
814834
if ascii_mode:

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ def _validate_file(self, filepath: str) -> dict[str, Any]:
3838
return super()._validate_file(filepath)
3939
return {"success": True}
4040

41+
@staticmethod
42+
def _tool_is_available(tool_path: str | None) -> bool:
43+
"""Treat any configured mock command name as available."""
44+
return bool(tool_path)
45+
4146
async def _run_command(
4247
self,
4348
cmd: list[str],

tests/test_client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for TSharkClient core functionality."""
22

33
import json
4+
import shutil
45

56
import pytest
67

@@ -106,6 +107,27 @@ def test_client_prefers_env_tool_paths(self, monkeypatch) -> None:
106107
assert client.dumpcap_path == "/opt/wireshark/dumpcap"
107108
assert client.text2pcap_path == "/opt/wireshark/text2pcap"
108109

110+
def test_describe_capabilities_reports_capture_backend_fallback(self, mock_client) -> None:
111+
capabilities = mock_client.describe_capabilities()
112+
assert capabilities["_meta"]["capture_backend"] == "dumpcap"
113+
assert capabilities["dumpcap"]["requirement"] == "optional"
114+
115+
mock_client.dumpcap_path = None
116+
mock_client._tool_paths["dumpcap"] = None
117+
118+
fallback_capabilities = mock_client.describe_capabilities()
119+
assert fallback_capabilities["_meta"]["capture_backend"] == "tshark"
120+
121+
@pytest.mark.asyncio
122+
async def test_check_capabilities_detects_real_tshark_when_installed(self) -> None:
123+
if shutil.which("tshark") is None:
124+
pytest.skip("tshark not installed on this host")
125+
126+
result = await TSharkClient().check_capabilities()
127+
128+
assert result["success"]
129+
assert result["data"]["tshark"]["available"] is True
130+
109131

110132
class TestRunCommand:
111133
"""Tests for _run_command error handling."""
@@ -140,6 +162,21 @@ async def test_binary_whitelist_allows_windows_exe_names_case_insensitive(self,
140162

141163

142164
class TestSuiteBehavior:
165+
@pytest.mark.asyncio
166+
async def test_list_interfaces_prefers_dumpcap_when_available(self, mock_client) -> None:
167+
result = await mock_client.list_interfaces()
168+
assert "dumpcap" in result
169+
assert mock_client._last_cmd[0] == "dumpcap"
170+
171+
@pytest.mark.asyncio
172+
async def test_list_interfaces_falls_back_to_tshark(self, mock_client) -> None:
173+
mock_client.dumpcap_path = None
174+
mock_client._tool_paths["dumpcap"] = None
175+
176+
result = await mock_client.list_interfaces()
177+
assert "tshark" in result
178+
assert mock_client._last_cmd[0] == "tshark"
179+
143180
@pytest.mark.asyncio
144181
async def test_capture_prefers_dumpcap_when_available(self, mock_client) -> None:
145182
result = await mock_client.capture_packets("en0", "/tmp/out.pcapng", duration=10)

tests/test_files.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,6 @@ async def test_text2pcap_import_command(self, mock_client: MockTSharkClient) ->
7474
ascii_mode=True,
7575
)
7676
assert "text2pcap" in result
77-
assert '-t %H:%M:%S.%f' in result
77+
assert "-t %H:%M:%S.%f" in result
7878
assert "-a" in result
7979
assert "-E ether" in result

tests/test_installer.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
_get_linux_config_home,
1212
_get_mcp_servers_dict,
1313
_get_python_executable,
14+
_iter_wireshark_search_dirs,
15+
_join_path,
1416
_read_json_config,
1517
_render_codex_toml_block,
1618
_write_json_config,
@@ -28,6 +30,7 @@ def test_get_python_executable_returns_string(self):
2830
assert len(result) > 0
2931

3032
def test_get_python_executable_in_venv(self, tmp_path, monkeypatch):
33+
monkeypatch.setattr("wireshark_mcp.installer.sys.platform", "linux")
3134
venv_dir = tmp_path / "venv"
3235
bin_dir = venv_dir / "bin"
3336
bin_dir.mkdir(parents=True)
@@ -276,10 +279,35 @@ def test_uninstall_removes_codex_toml_block(self, tmp_path):
276279

277280

278281
class TestPlatformConfigs:
282+
def test_join_path_uses_target_platform_separators(self):
283+
assert _join_path("/Users/tester", "Library", "Claude", platform="darwin") == "/Users/tester/Library/Claude"
284+
assert (
285+
_join_path(r"C:\Users\tester", "AppData", "Roaming", platform="win32") == r"C:\Users\tester\AppData\Roaming"
286+
)
287+
279288
def test_linux_config_home_uses_xdg(self, monkeypatch):
280289
monkeypatch.setenv("XDG_CONFIG_HOME", "/tmp/xdg-config")
281290
assert _get_linux_config_home("/home/tester") == "/tmp/xdg-config"
282291

292+
def test_mac_client_configs_use_application_support(self, monkeypatch):
293+
monkeypatch.setattr("wireshark_mcp.installer.sys.platform", "darwin")
294+
monkeypatch.setattr("wireshark_mcp.installer.os.path.expanduser", lambda _: "/Users/tester")
295+
296+
configs = _get_client_configs()
297+
298+
assert configs["Claude"] == (
299+
"/Users/tester/Library/Application Support/Claude",
300+
"claude_desktop_config.json",
301+
)
302+
assert configs["Zed"] == (
303+
"/Users/tester/Library/Application Support/Zed",
304+
"settings.json",
305+
)
306+
assert configs["VS Code"] == (
307+
"/Users/tester/Library/Application Support/Code/User",
308+
"settings.json",
309+
)
310+
283311
def test_linux_client_configs_use_xdg(self, monkeypatch):
284312
monkeypatch.setattr("wireshark_mcp.installer.sys.platform", "linux")
285313
monkeypatch.setattr("wireshark_mcp.installer.os.path.expanduser", lambda _: "/home/tester")
@@ -306,3 +334,27 @@ def test_windows_client_configs_include_supported_paths(self, monkeypatch):
306334
r"C:\Users\tester\AppData\Roaming\Code\User",
307335
"settings.json",
308336
)
337+
338+
def test_iter_wireshark_search_dirs_for_macos(self, monkeypatch):
339+
monkeypatch.setattr("wireshark_mcp.installer.sys.platform", "darwin")
340+
monkeypatch.setattr("wireshark_mcp.installer.os.path.expanduser", lambda _: "/Users/tester")
341+
342+
search_dirs = _iter_wireshark_search_dirs()
343+
344+
assert "/Applications/Wireshark.app/Contents/MacOS" in search_dirs
345+
assert "/Applications/Wireshark.app/Contents/Helpers" in search_dirs
346+
assert "/Users/tester/Applications/Wireshark.app/Contents/MacOS" in search_dirs
347+
assert "/opt/homebrew/bin" in search_dirs
348+
349+
def test_iter_wireshark_search_dirs_for_windows(self, monkeypatch):
350+
monkeypatch.setattr("wireshark_mcp.installer.sys.platform", "win32")
351+
monkeypatch.setattr("wireshark_mcp.installer.os.path.expanduser", lambda _: r"C:\Users\tester")
352+
monkeypatch.setenv("LOCALAPPDATA", r"C:\Users\tester\AppData\Local")
353+
monkeypatch.setenv("PROGRAMFILES", r"C:\Program Files")
354+
monkeypatch.setenv("PROGRAMFILES(X86)", r"C:\Program Files (x86)")
355+
356+
search_dirs = _iter_wireshark_search_dirs()
357+
358+
assert r"C:\Program Files\Wireshark" in search_dirs
359+
assert r"C:\Program Files (x86)\Wireshark" in search_dirs
360+
assert r"C:\Users\tester\AppData\Local\Programs\Wireshark" in search_dirs

0 commit comments

Comments
 (0)