diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index c4b82c0..5fb6022 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -12,12 +12,6 @@ on: jobs: claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - runs-on: ubuntu-latest permissions: contents: read @@ -29,21 +23,31 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 1 + fetch-depth: 0 - name: Run Claude Code Review id: claude-review uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} prompt: | - Review this pull request and focus on bugs, behavior regressions, security risks, - missing tests, and documentation mismatches. + You are a code review agent for the ptop3 project. + Review this pull request against the standards in + .github/instructions/code-review.instructions.md. Use the repository diff against the base branch as the review scope. - Post review feedback as PR comments only. - Do NOT modify files, create commits, or push branch changes. - If there are no findings, post a short approval-style summary comment. + Focus on: + - correctness bugs or behavioral regressions + - security issues and missing guards around privileged actions + - maintainability issues in changed code + - missing or weak test coverage for the changed behavior + + For each issue found, post an inline PR review comment at the exact file and line + with a concise explanation and a concrete suggested fix. + + Do NOT modify files or create commits. All feedback must be via PR comments. + If no issues are found, post a brief approval-style summary of what you checked. # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options diff --git a/.github/workflows/claude-quality-gate.yml b/.github/workflows/claude-quality-gate.yml index f38b24e..f7c61ca 100644 --- a/.github/workflows/claude-quality-gate.yml +++ b/.github/workflows/claude-quality-gate.yml @@ -2,7 +2,7 @@ name: Claude Quality Gate on: pull_request: - branches: [main] + branches: [main, test] types: [opened, synchronize, reopened] permissions: @@ -24,6 +24,7 @@ jobs: - uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} prompt: | You are a test quality agent for the ptop3 project. @@ -55,6 +56,7 @@ jobs: - uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} prompt: | You are a documentation quality agent for the ptop3 project. @@ -92,6 +94,7 @@ jobs: - uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} prompt: | You are a code quality and security agent for the ptop3 project. diff --git a/.github/workflows/publish-testpypi.yml b/.github/workflows/publish-testpypi.yml index 71569f9..a78b7f9 100644 --- a/.github/workflows/publish-testpypi.yml +++ b/.github/workflows/publish-testpypi.yml @@ -15,8 +15,10 @@ jobs: publish: needs: [test] runs-on: ubuntu-latest + environment: testpypi permissions: contents: read # checkout only + id-token: write # required for Trusted Publishing steps: - uses: actions/checkout@v4 @@ -27,7 +29,53 @@ jobs: python-version: "3.12" - name: Install build tools - run: pip install build + run: pip install build packaging + + - name: Set unique TestPyPI version + env: + RUN_NUMBER: ${{ github.run_number }} + run: | + python - <<'PY' + import os + import re + from pathlib import Path + + from packaging.version import Version + + pyproject = Path("pyproject.toml") + init_py = Path("ptop3/__init__.py") + + pyproject_text = pyproject.read_text() + match = re.search(r'^version = "([^"]+)"', pyproject_text, re.M) + if not match: + raise SystemExit("Could not find project version in pyproject.toml") + + base = Version(match.group(1)) + test_version = f"{base.major}.{base.minor}.{base.micro + 1}.dev{os.environ['RUN_NUMBER']}" + + pyproject.write_text( + re.sub( + r'^version = "[^"]+"', + f'version = "{test_version}"', + pyproject_text, + count=1, + flags=re.M, + ) + ) + + init_text = init_py.read_text() + init_py.write_text( + re.sub( + r'^__version__ = "[^"]+"', + f'__version__ = "{test_version}"', + init_text, + count=1, + flags=re.M, + ) + ) + + print(f"Publishing TestPyPI version: {test_version}") + PY - name: Build package run: python -m build @@ -36,5 +84,4 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ - password: ${{ secrets.TEST_PYPI_API_TOKEN }} skip-existing: true diff --git a/.gitignore b/.gitignore index d9fff16..3120dbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,51 @@ __pycache__/ *.py[cod] -*.egg-info/ -dist/ -build/ -.eggs/ +*$py.class + +# Virtual environments .venv/ venv/ -.mypy_cache/ -.ruff_cache/ -.pytest_cache/ +env/ +ENV/ +.python-version + +# Packaging +.Python +build/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +pip-wheel-metadata/ *.egg +*.egg-info/ MANIFEST + +# Test and coverage outputs +.coverage +.coverage.* +htmlcov/ +.pytest_cache/ +.hypothesis/ +.tox/ +.nox/ +coverage.xml +*.cover +*.py,cover + +# Type checkers and linters +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.ruff_cache/ + +# Notebooks and local tooling +.ipynb_checkpoints/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bfbca1..4a07e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - TestPyPI publish now triggers on merge to `test` branch (was `main`) - PyPI production publish is now manual (`workflow_dispatch`) instead of automatic on tag - GitHub Release creation on tag push remains automatic +- `ptop3` no longer advertises or supports the non-functional `net` sort mode in the TUI/CLI ### Added - `CLAUDE.md` with project context for Claude Code agents diff --git a/ptop3/monitor.py b/ptop3/monitor.py index 36a16e1..9b25bec 100644 --- a/ptop3/monitor.py +++ b/ptop3/monitor.py @@ -226,7 +226,7 @@ def _sample_process( rss_mb = rss_bytes / (1024 * 1024) cpu = 0.0 if lite and rss_mb < 2.0 else proc.cpu_percent() - swap_mb = _swap_value(proc.pid, rss_mb, lite) + swap_mb = _swap_value(proc.pid, rss_mb, lite, read_swap=self.read_vmswap_mb) io_read_mb, io_write_mb = _io_values(proc, rss_mb, lite) try: @@ -291,11 +291,13 @@ def _matches_filter(filter_search, app: str, name: str, cmdline: str) -> bool: return bool(filter_search(app) or filter_search(name) or (cmdline and filter_search(cmdline))) -def _swap_value(pid: int, rss_mb: float, lite: bool) -> float: +def _swap_value(pid: int, rss_mb: float, lite: bool, read_swap=None) -> float: + if read_swap is None: + read_swap = read_vmswap_mb if not lite and rss_mb > 50: - return read_vmswap_mb(pid) + return read_swap(pid) if lite and rss_mb > 200: - return read_vmswap_mb(pid) + return read_swap(pid) return 0.0 diff --git a/pyproject.toml b/pyproject.toml index 71be716..12ba198 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ include = ["ptop3*"] [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = ["."] addopts = "-v --tb=short" [tool.coverage.run] diff --git a/tests/conftest.py b/tests/conftest.py index 6555959..890ea3d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ """Shared fixtures for ptop3 tests.""" import sys from pathlib import Path - import pytest ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 372f533..bcc6ffc 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -352,7 +352,7 @@ def test_get_proc_rows_filters_and_collects_metrics(monkeypatch): monkeypatch.setattr(monitor.psutil, "process_iter", lambda attrs=None: procs) monkeypatch.setattr(monitor.psutil, "virtual_memory", lambda: SimpleNamespace(total=1024 * 1024 * 1024)) - monkeypatch.setattr(monitor, "read_vmswap_mb", lambda pid: 4.0 if pid == 10 else 0.0) + monkeypatch.setattr(monitor.ProcessSampler, "read_vmswap_mb", lambda self, pid: 4.0 if pid == 10 else 0.0) rows = monitor.get_proc_rows(filter_re=monitor.re.compile("python")) @@ -849,6 +849,19 @@ def raising_status(): assert row.status == "unknown" +def test_sampler_sample_process_uses_sampler_swap_reader(monkeypatch): + sampler = monitor.ProcessSampler() + proc = _FakeProc(1, 0, "python3", ["python3"], rss_mb=64, cpu=3.0) + + monkeypatch.setattr(monitor, "read_vmswap_mb", lambda pid: 0.0) + monkeypatch.setattr(monitor.ProcessSampler, "read_vmswap_mb", lambda self, pid: 7.0) + + row = sampler._sample_process(proc, 1.0, 1.0, False, None) + + assert row is not None + assert row.swap_mb == 7.0 + + def test_sampler_sample_process_filtered_out_and_memory_denied(monkeypatch): sampler = monitor.ProcessSampler() proc = _FakeProc(1, 0, "bash", ["bash"], rss_mb=4, cpu=1.0) @@ -1258,7 +1271,7 @@ def cmdline(self): assert monitor._safe_cmdline(MissingCmdProc()) == "" - monkeypatch.setattr(monitor, "read_vmswap_mb", lambda pid: 12.0) + monkeypatch.setattr(monitor.ProcessSampler, "read_vmswap_mb", lambda self, pid: 12.0) sampler = monitor.ProcessSampler() row = sampler._sample_process(_FakeProc(1, 0, "python3", ["python3"], rss_mb=256, cpu=3.0), 1.0, 1.0, True, None) assert row is not None diff --git a/tests/test_swap_clean.py b/tests/test_swap_clean.py index daf3971..4533558 100644 --- a/tests/test_swap_clean.py +++ b/tests/test_swap_clean.py @@ -256,7 +256,9 @@ def test_swap_clean_file_by_file_fallback(tmp_path, capsys): ) assert rc == 0 - assert "file-by-file" in capsys.readouterr().out + captured = capsys.readouterr() + assert "Not enough RAM to clean all swap at once. Trying file-by-file..." in captured.err + assert "Swap clean completed (file-by-file)." in captured.out called_cmds = [call.args[0] for call in mock_run.call_args_list] assert ["swapoff", "/swap-b"] in called_cmds assert ["swapon", "/swap-b"] in called_cmds