Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
5 changes: 4 additions & 1 deletion .github/workflows/claude-quality-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Claude Quality Gate

on:
pull_request:
branches: [main]
branches: [main, test]
types: [opened, synchronize, reopened]

permissions:
Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
51 changes: 49 additions & 2 deletions .github/workflows/publish-testpypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
52 changes: 45 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions ptop3/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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


Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ include = ["ptop3*"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
addopts = "-v --tb=short"

[tool.coverage.run]
Expand Down
1 change: 0 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Shared fixtures for ptop3 tests."""
import sys
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[1]
Expand Down
17 changes: 15 additions & 2 deletions tests/test_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion tests/test_swap_clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading