From 751b0d50230f3c979f46fae6309d2fac66c5ff64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 06:34:30 +0000 Subject: [PATCH 1/7] Initial plan From 3b6e9a06c93cac15a86012d1c974fa16c7185c1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 06:37:00 +0000 Subject: [PATCH 2/7] feat: add quality ratchet (baseline + check script + workflow) - Add .quality-baseline.json capturing current ruff violation counts per file - Add .github/scripts/check_quality.py to compare PR vs baseline - Add .github/workflows/quality-ratchet.yml triggered on pull_request Implements DRC recommendation from issue #159: zero-upfront-refactoring ratchet that prevents new violations while allowing organic debt erosion. --- .github/scripts/check_quality.py | 155 ++++++++++++++++++++++++++ .github/workflows/quality-ratchet.yml | 89 +++++++++++++++ .quality-baseline.json | 14 +++ 3 files changed, 258 insertions(+) create mode 100644 .github/scripts/check_quality.py create mode 100644 .github/workflows/quality-ratchet.yml create mode 100644 .quality-baseline.json diff --git a/.github/scripts/check_quality.py b/.github/scripts/check_quality.py new file mode 100644 index 0000000..6ac7bc2 --- /dev/null +++ b/.github/scripts/check_quality.py @@ -0,0 +1,155 @@ +"""Quality ratchet: compare current ruff violations against a committed baseline. + +Exit codes: + 0 — no regressions (violations per file ≤ baseline) + 1 — regressions detected (at least one file has more violations than baseline) + +Usage: + python .github/scripts/check_quality.py [--baseline .quality-baseline.json] +""" + +from __future__ import annotations + +import argparse +import collections +import json +import os +import subprocess +import sys +from pathlib import Path + + +def run_ruff(repo_root: Path) -> dict[str, int]: + """Run ruff and return per-file violation counts (relative paths).""" + result = subprocess.run( + ["ruff", "check", ".", "--output-format=json"], + cwd=repo_root, + capture_output=True, + text=True, + ) + # ruff exits 1 when violations found; that is expected + try: + data = json.loads(result.stdout) + except json.JSONDecodeError: + print("❌ Failed to parse ruff JSON output.", file=sys.stderr) + print(result.stderr, file=sys.stderr) + sys.exit(1) + + root_prefix = str(repo_root) + os.sep + counts: dict[str, int] = collections.Counter() + for item in data: + rel = item["filename"].replace(root_prefix, "") + counts[rel] += 1 + return dict(counts) + + +def load_baseline(path: Path) -> dict[str, int]: + """Load the committed baseline file.""" + if not path.exists(): + print(f"❌ Baseline file not found: {path}", file=sys.stderr) + print(" Run: python .github/scripts/check_quality.py --update-baseline", file=sys.stderr) + sys.exit(1) + data = json.loads(path.read_text()) + return data.get("per_file", {}) + + +def compare(current: dict[str, int], baseline: dict[str, int]) -> tuple[list[str], list[str]]: + """Return (regressions, improvements). + + A regression is any file whose current count exceeds its baseline count. + A new file with violations (absent from baseline) is also a regression. + """ + all_files = set(current) | set(baseline) + regressions: list[str] = [] + improvements: list[str] = [] + + for f in sorted(all_files): + curr = current.get(f, 0) + base = baseline.get(f, 0) + if curr > base: + regressions.append(f" ❌ {f} ({base} → {curr}, +{curr - base})") + elif curr < base: + improvements.append(f" ✅ {f} ({base} → {curr}, -{base - curr})") + + return regressions, improvements + + +def update_baseline(repo_root: Path, baseline_path: Path) -> None: + """Regenerate the baseline from the current ruff output and write it.""" + import datetime + + result = subprocess.run( + ["ruff", "--version"], capture_output=True, text=True + ) + ruff_version = result.stdout.strip().replace("ruff ", "") + + current = run_ruff(repo_root) + data = { + "generated_at": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds"), + "ruff_version": ruff_version, + "total_violations": sum(current.values()), + "per_file": dict(sorted(current.items())), + } + baseline_path.write_text(json.dumps(data, indent=2) + "\n") + print(f"✅ Baseline updated: {sum(current.values())} violations across {len(current)} files") + print(f" Written to {baseline_path}") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--baseline", + default=".quality-baseline.json", + help="Path to baseline JSON file (default: .quality-baseline.json)", + ) + parser.add_argument( + "--update-baseline", + action="store_true", + help="Regenerate the baseline from current state and exit 0", + ) + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[2] + baseline_path = repo_root / args.baseline + + if args.update_baseline: + update_baseline(repo_root, baseline_path) + return + + baseline = load_baseline(baseline_path) + current = run_ruff(repo_root) + + total_baseline = sum(baseline.values()) + total_current = sum(current.values()) + regressions, improvements = compare(current, baseline) + + print("=" * 60) + print(" Quality Ratchet Report") + print("=" * 60) + print(f" Baseline violations : {total_baseline}") + print(f" Current violations : {total_current}") + delta = total_current - total_baseline + delta_str = f"+{delta}" if delta > 0 else str(delta) + print(f" Delta : {delta_str}") + print() + + if improvements: + print(f"Improvements ({len(improvements)} file(s)):") + print("\n".join(improvements)) + print() + + if regressions: + print(f"Regressions ({len(regressions)} file(s)) — BLOCKING:") + print("\n".join(regressions)) + print() + print("Fix the regressions above, or run:") + print(" ruff check --fix") + print() + print("❌ Quality ratchet FAILED — this PR introduces new violations.") + sys.exit(1) + + print("✅ Quality ratchet PASSED — no new violations introduced.") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/quality-ratchet.yml b/.github/workflows/quality-ratchet.yml new file mode 100644 index 0000000..e4e9ead --- /dev/null +++ b/.github/workflows/quality-ratchet.yml @@ -0,0 +1,89 @@ +name: Quality Ratchet + +on: + pull_request: + types: [opened, synchronize, ready_for_review] + paths: + - '**.py' + - 'pyproject.toml' + - '.quality-baseline.json' + +permissions: + contents: read + pull-requests: write + +jobs: + ratchet: + name: ruff ratchet (no new violations) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout PR head + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install ruff + run: pip install "ruff==0.15.20" + + - name: Run quality ratchet + id: ratchet + run: python .github/scripts/check_quality.py + continue-on-error: true + + - name: Comment on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v8 + with: + script: | + const outcome = '${{ steps.ratchet.outcome }}'; + const passed = outcome === 'success'; + + const header = passed + ? '## ✅ Quality Ratchet — PASSED' + : '## ❌ Quality Ratchet — FAILED'; + + const body = passed + ? `${header}\n\nNo new ruff violations introduced by this PR. 🎉` + : `${header}\n\nThis PR introduces new ruff violations compared to the baseline.\n\n` + + `Run \`python .github/scripts/check_quality.py\` locally to see the details, ` + + `then fix the regressions or run \`ruff check --fix\` to auto-fix them.\n\n` + + `> **Tip:** You can improve the baseline by fixing existing violations in the ` + + `files you touch, then updating it with:\n` + + `> \`python .github/scripts/check_quality.py --update-baseline\``; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.includes('Quality Ratchet') + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body, + }); + } + + - name: Fail if ratchet failed + if: steps.ratchet.outcome == 'failure' + run: exit 1 diff --git a/.quality-baseline.json b/.quality-baseline.json new file mode 100644 index 0000000..63e6fe5 --- /dev/null +++ b/.quality-baseline.json @@ -0,0 +1,14 @@ +{ + "generated_at": "2026-07-04T06:36:49+00:00", + "ruff_version": "0.15.20", + "total_violations": 24, + "per_file": { + ".github/scripts/generate_rollback_manifest.py": 1, + "autopilot/staleness_engine.py": 5, + "tests/test_autonomous_agents.py": 6, + "tests/unit/test_dependency_agent.py": 1, + "tests/unit/test_security_scan_agent.py": 2, + "tests/unit/test_staleness_engine.py": 3, + "tests/unit/test_triage_agent.py": 6 + } +} From 9a706b67d494231bc0a84bdc4c4741e6fea28726 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 06:38:35 +0000 Subject: [PATCH 3/7] fix: address code review feedback on quality ratchet - Move datetime import to module level in check_quality.py - Extract RUFF_VERSION env var in quality-ratchet.yml workflow - Simplify JS failure message to single template literal --- .github/scripts/check_quality.py | 3 +-- .github/workflows/quality-ratchet.yml | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/scripts/check_quality.py b/.github/scripts/check_quality.py index 6ac7bc2..ec2cd88 100644 --- a/.github/scripts/check_quality.py +++ b/.github/scripts/check_quality.py @@ -12,6 +12,7 @@ import argparse import collections +import datetime import json import os import subprocess @@ -76,8 +77,6 @@ def compare(current: dict[str, int], baseline: dict[str, int]) -> tuple[list[str def update_baseline(repo_root: Path, baseline_path: Path) -> None: """Regenerate the baseline from the current ruff output and write it.""" - import datetime - result = subprocess.run( ["ruff", "--version"], capture_output=True, text=True ) diff --git a/.github/workflows/quality-ratchet.yml b/.github/workflows/quality-ratchet.yml index e4e9ead..a8c04ee 100644 --- a/.github/workflows/quality-ratchet.yml +++ b/.github/workflows/quality-ratchet.yml @@ -12,6 +12,9 @@ permissions: contents: read pull-requests: write +env: + RUFF_VERSION: '0.15.20' + jobs: ratchet: name: ruff ratchet (no new violations) @@ -29,7 +32,7 @@ jobs: cache: 'pip' - name: Install ruff - run: pip install "ruff==0.15.20" + run: pip install "ruff==${{ env.RUFF_VERSION }}" - name: Run quality ratchet id: ratchet @@ -50,12 +53,14 @@ jobs: const body = passed ? `${header}\n\nNo new ruff violations introduced by this PR. 🎉` - : `${header}\n\nThis PR introduces new ruff violations compared to the baseline.\n\n` + - `Run \`python .github/scripts/check_quality.py\` locally to see the details, ` + - `then fix the regressions or run \`ruff check --fix\` to auto-fix them.\n\n` + - `> **Tip:** You can improve the baseline by fixing existing violations in the ` + - `files you touch, then updating it with:\n` + - `> \`python .github/scripts/check_quality.py --update-baseline\``; + : `${header} + +This PR introduces new ruff violations compared to the baseline. + +Run \`python .github/scripts/check_quality.py\` locally to see the details, then fix the regressions or run \`ruff check --fix\` to auto-fix them. + +> **Tip:** You can improve the baseline by fixing existing violations in the files you touch, then updating it with: +> \`python .github/scripts/check_quality.py --update-baseline\``; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, From 5cfb973ee28ddc148bb916dc313a1cfe97d2a0f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Jul 2026 06:42:11 +0000 Subject: [PATCH 4/7] style: auto-fix pre-commit issues [skip ci] --- .github/scripts/check_quality.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/scripts/check_quality.py b/.github/scripts/check_quality.py index ec2cd88..78e3c30 100644 --- a/.github/scripts/check_quality.py +++ b/.github/scripts/check_quality.py @@ -48,13 +48,18 @@ def load_baseline(path: Path) -> dict[str, int]: """Load the committed baseline file.""" if not path.exists(): print(f"❌ Baseline file not found: {path}", file=sys.stderr) - print(" Run: python .github/scripts/check_quality.py --update-baseline", file=sys.stderr) + print( + " Run: python .github/scripts/check_quality.py --update-baseline", + file=sys.stderr, + ) sys.exit(1) data = json.loads(path.read_text()) return data.get("per_file", {}) -def compare(current: dict[str, int], baseline: dict[str, int]) -> tuple[list[str], list[str]]: +def compare( + current: dict[str, int], baseline: dict[str, int] +) -> tuple[list[str], list[str]]: """Return (regressions, improvements). A regression is any file whose current count exceeds its baseline count. @@ -77,20 +82,22 @@ def compare(current: dict[str, int], baseline: dict[str, int]) -> tuple[list[str def update_baseline(repo_root: Path, baseline_path: Path) -> None: """Regenerate the baseline from the current ruff output and write it.""" - result = subprocess.run( - ["ruff", "--version"], capture_output=True, text=True - ) + result = subprocess.run(["ruff", "--version"], capture_output=True, text=True) ruff_version = result.stdout.strip().replace("ruff ", "") current = run_ruff(repo_root) data = { - "generated_at": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds"), + "generated_at": datetime.datetime.now(datetime.UTC).isoformat( + timespec="seconds" + ), "ruff_version": ruff_version, "total_violations": sum(current.values()), "per_file": dict(sorted(current.items())), } baseline_path.write_text(json.dumps(data, indent=2) + "\n") - print(f"✅ Baseline updated: {sum(current.values())} violations across {len(current)} files") + print( + f"✅ Baseline updated: {sum(current.values())} violations across {len(current)} files" + ) print(f" Written to {baseline_path}") From d63dcb8795426842353f92db2fcf7999ed7c94a6 Mon Sep 17 00:00:00 2001 From: Gadget Lab <232155002+labgadget015-dotcom@users.noreply.github.com> Date: Sat, 4 Jul 2026 08:26:39 +0100 Subject: [PATCH 5/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/scripts/check_quality.py | 38 ++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/scripts/check_quality.py b/.github/scripts/check_quality.py index 78e3c30..d0bf7ae 100644 --- a/.github/scripts/check_quality.py +++ b/.github/scripts/check_quality.py @@ -22,25 +22,45 @@ def run_ruff(repo_root: Path) -> dict[str, int]: """Run ruff and return per-file violation counts (relative paths).""" - result = subprocess.run( - ["ruff", "check", ".", "--output-format=json"], - cwd=repo_root, - capture_output=True, - text=True, - ) + try: + result = subprocess.run( + ["ruff", "check", ".", "--output-format=json"], + cwd=repo_root, + capture_output=True, + text=True, + ) + except FileNotFoundError: + print( + "❌ ruff is not installed or not on PATH. Install it with: pip install ruff", + file=sys.stderr, + ) + sys.exit(1) + # ruff exits 1 when violations found; that is expected try: data = json.loads(result.stdout) except json.JSONDecodeError: print("❌ Failed to parse ruff JSON output.", file=sys.stderr) - print(result.stderr, file=sys.stderr) + if result.stdout.strip(): + print(result.stdout, file=sys.stderr) + if result.stderr.strip(): + print(result.stderr, file=sys.stderr) sys.exit(1) - root_prefix = str(repo_root) + os.sep counts: dict[str, int] = collections.Counter() + repo_root_resolved = repo_root.resolve() for item in data: - rel = item["filename"].replace(root_prefix, "") + filename = item.get("filename", "") + p = Path(filename) + if p.is_absolute(): + try: + rel = p.resolve().relative_to(repo_root_resolved).as_posix() + except ValueError: + rel = p.as_posix() + else: + rel = p.as_posix() counts[rel] += 1 + return dict(counts) From 4e6cae2d1ecce8f23329fdae11c13f184d23e96a Mon Sep 17 00:00:00 2001 From: Gadget Lab <232155002+labgadget015-dotcom@users.noreply.github.com> Date: Sat, 4 Jul 2026 08:27:01 +0100 Subject: [PATCH 6/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/scripts/check_quality.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/scripts/check_quality.py b/.github/scripts/check_quality.py index d0bf7ae..05c68ce 100644 --- a/.github/scripts/check_quality.py +++ b/.github/scripts/check_quality.py @@ -73,8 +73,19 @@ def load_baseline(path: Path) -> dict[str, int]: file=sys.stderr, ) sys.exit(1) - data = json.loads(path.read_text()) - return data.get("per_file", {}) + + try: + data = json.loads(path.read_text()) + except json.JSONDecodeError as e: + print(f"❌ Failed to parse baseline JSON: {path} ({e})", file=sys.stderr) + sys.exit(1) + + per_file = data.get("per_file", {}) + if not isinstance(per_file, dict): + print(f"❌ Baseline file has invalid 'per_file' shape: {path}", file=sys.stderr) + sys.exit(1) + + return per_file def compare( From 34c4d68e82ea0c5bec82acb920400265b20621ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Jul 2026 07:27:06 +0000 Subject: [PATCH 7/7] style: auto-fix pre-commit issues [skip ci] --- .github/scripts/check_quality.py | 1 - .github/scripts/generate_rollback_manifest.py | 4 ++-- autopilot/staleness_engine.py | 10 +++++----- tests/test_autonomous_agents.py | 10 ++++------ tests/unit/test_dependency_agent.py | 1 - tests/unit/test_security_scan_agent.py | 2 -- tests/unit/test_staleness_engine.py | 8 +++----- tests/unit/test_triage_agent.py | 13 +++++++------ 8 files changed, 21 insertions(+), 28 deletions(-) diff --git a/.github/scripts/check_quality.py b/.github/scripts/check_quality.py index 05c68ce..3c4c2fb 100644 --- a/.github/scripts/check_quality.py +++ b/.github/scripts/check_quality.py @@ -14,7 +14,6 @@ import collections import datetime import json -import os import subprocess import sys from pathlib import Path diff --git a/.github/scripts/generate_rollback_manifest.py b/.github/scripts/generate_rollback_manifest.py index 54dcb7a..c7c9de9 100644 --- a/.github/scripts/generate_rollback_manifest.py +++ b/.github/scripts/generate_rollback_manifest.py @@ -7,7 +7,7 @@ """ import os -from datetime import datetime, timezone +from datetime import UTC, datetime def main(): @@ -17,7 +17,7 @@ def main(): author = os.environ.get("AUTHOR", "unknown") files_changed = os.environ.get("FILES_CHANGED", "").split() - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + timestamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC") path = "docs/ROLLBACK.md" os.makedirs("docs", exist_ok=True) diff --git a/autopilot/staleness_engine.py b/autopilot/staleness_engine.py index 5df6cb8..ff98769 100644 --- a/autopilot/staleness_engine.py +++ b/autopilot/staleness_engine.py @@ -26,7 +26,7 @@ import logging import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -204,7 +204,7 @@ def generate_comment( cost = result.get("usage", {}) self._cost_log.append( { - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "repo": repo_full_name, "tier": tier, "usage": cost, @@ -231,7 +231,7 @@ def scan_repos(self, repos: list[dict[str, Any]]) -> list[dict[str, Any]]: repo_full_name, pr_number, pr_title, pr_url, age_days, tier """ stale: list[dict[str, Any]] = [] - now = datetime.now(timezone.utc) + now = datetime.now(UTC) for repo_cfg in repos: owner = repo_cfg["owner"] @@ -243,7 +243,7 @@ def scan_repos(self, repos: list[dict[str, Any]]) -> list[dict[str, Any]]: for pr in pulls: created = pr.created_at if created.tzinfo is None: - created = created.replace(tzinfo=timezone.utc) + created = created.replace(tzinfo=UTC) age_days = (now - created).days tier = self.assign_tier(age_days) if tier > 0: @@ -291,7 +291,7 @@ def process_stale_prs( Returns a summary dict with counts and per-PR actions taken. """ state = self.load_state() - now_iso = datetime.now(timezone.utc).isoformat() + now_iso = datetime.now(UTC).isoformat() actions: list[dict[str, Any]] = [] for pr_info in stale_prs: diff --git a/tests/test_autonomous_agents.py b/tests/test_autonomous_agents.py index 4a88150..822dc8c 100644 --- a/tests/test_autonomous_agents.py +++ b/tests/test_autonomous_agents.py @@ -1,8 +1,7 @@ """Tests for autonomous_agent package — agents and CLI.""" -import re -from datetime import datetime, timezone -from unittest.mock import AsyncMock, MagicMock, Mock, patch +from datetime import UTC, datetime +from unittest.mock import Mock, patch import pytest from click.testing import CliRunner @@ -20,7 +19,6 @@ main, ) - # ── helpers ─────────────────────────────────────────────────────────────────── @@ -73,7 +71,7 @@ async def test_execute_stale_branch_requires_approval(self): branch = Mock() branch.name = "old-feature" - old_date = datetime(2025, 1, 1, tzinfo=timezone.utc) + old_date = datetime(2025, 1, 1, tzinfo=UTC) branch.commit.commit.author.date = old_date repo.get_branches.return_value = [branch] self.github.get_repository.return_value = repo @@ -92,7 +90,7 @@ async def test_execute_stale_branch_auto_delete(self): branch = Mock() branch.name = "old-feature" - old_date = datetime(2025, 1, 1, tzinfo=timezone.utc) + old_date = datetime(2025, 1, 1, tzinfo=UTC) branch.commit.commit.author.date = old_date branch.commit.sha = "abc123" diff --git a/tests/unit/test_dependency_agent.py b/tests/unit/test_dependency_agent.py index c385473..52641f0 100644 --- a/tests/unit/test_dependency_agent.py +++ b/tests/unit/test_dependency_agent.py @@ -4,7 +4,6 @@ import asyncio import json -from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/unit/test_security_scan_agent.py b/tests/unit/test_security_scan_agent.py index a7702af..d0fb780 100644 --- a/tests/unit/test_security_scan_agent.py +++ b/tests/unit/test_security_scan_agent.py @@ -4,8 +4,6 @@ import asyncio import json -import tempfile -from pathlib import Path from unittest.mock import AsyncMock, MagicMock, mock_open, patch diff --git a/tests/unit/test_staleness_engine.py b/tests/unit/test_staleness_engine.py index 5f47706..5a735e9 100644 --- a/tests/unit/test_staleness_engine.py +++ b/tests/unit/test_staleness_engine.py @@ -2,14 +2,12 @@ from __future__ import annotations -import json -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path -from unittest.mock import MagicMock, patch, AsyncMock +from unittest.mock import AsyncMock, MagicMock, patch import pytest - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -41,7 +39,7 @@ def _make_pr(number=1, title="Test PR", days_old=10, html_url="http://example.co pr.number = number pr.title = title pr.html_url = html_url - pr.created_at = datetime.now(timezone.utc) - timedelta(days=days_old) + pr.created_at = datetime.now(UTC) - timedelta(days=days_old) return pr diff --git a/tests/unit/test_triage_agent.py b/tests/unit/test_triage_agent.py index 0745683..023f63b 100644 --- a/tests/unit/test_triage_agent.py +++ b/tests/unit/test_triage_agent.py @@ -4,9 +4,7 @@ import asyncio import json -import tempfile -from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch def run(coro): @@ -94,9 +92,10 @@ def test_returns_empty_when_file_missing(self, tmp_path, monkeypatch): assert result == [] def test_returns_records_within_window(self, tmp_path, monkeypatch): - import agents.triage_agent as mod from datetime import datetime, timedelta + import agents.triage_agent as mod + monkeypatch.setattr(mod, "project_root", tmp_path) costs_path = tmp_path / ".llm_costs.jsonl" recent = (datetime.now() - timedelta(days=1)).isoformat() @@ -138,9 +137,10 @@ def setup_method(self): self.agent = _make_agent() def test_no_slack_url_prints_and_returns(self, tmp_path, monkeypatch, capsys): - import agents.triage_agent as mod from datetime import datetime, timedelta + import agents.triage_agent as mod + monkeypatch.setattr(mod, "project_root", tmp_path) monkeypatch.delenv("SLACK_WEBHOOK_URL", raising=False) @@ -174,9 +174,10 @@ def test_empty_costs_returns_zero(self, tmp_path, monkeypatch): assert result["records"] == 0 def test_multiple_models_aggregated(self, tmp_path, monkeypatch): - import agents.triage_agent as mod from datetime import datetime, timedelta + import agents.triage_agent as mod + monkeypatch.setattr(mod, "project_root", tmp_path) monkeypatch.delenv("SLACK_WEBHOOK_URL", raising=False)