diff --git a/tasks.py b/tasks.py index f4a99283..f20cc950 100644 --- a/tasks.py +++ b/tasks.py @@ -23,7 +23,7 @@ REPOS = [ "Timmy_Foundation/the-nexus", "Timmy_Foundation/timmy-config", ] -NET_LINE_LIMIT = 10 +NET_LINE_LIMIT = 500 # ── Local Model Inference via Hermes Harness ───────────────────────── @@ -1190,9 +1190,11 @@ def review_prs(): net = sum(f.additions - f.deletions for f in files) if net > NET_LINE_LIMIT: rejected += 1 + file_list = ", ".join(f.filename for f in files[:10]) g.create_comment( repo, pr.number, f"❌ Net +{net} lines exceeds the {NET_LINE_LIMIT}-line limit. " + f"Files: {file_list}. " f"Find {net - NET_LINE_LIMIT} lines to cut. See CONTRIBUTING.md." ) return {"reviewed": reviewed, "rejected": rejected} @@ -1539,7 +1541,8 @@ def memory_compress(): inference_down_count = 0 for t in ticks: - for action in t.get("actions", []): + decision = t.get("decision", {}) + for action in decision.get("actions", []): alerts.append(f"[{t['tick_id']}] {action}") p = t.get("perception", {}) if not p.get("gitea_alive"): @@ -1584,8 +1587,9 @@ def good_morning_report(): # --- GATHER OVERNIGHT DATA --- # Heartbeat ticks from last night + from datetime import timedelta as _td tick_dir = TIMMY_HOME / "heartbeat" - yesterday = now.strftime("%Y%m%d") + yesterday = (now - _td(days=1)).strftime("%Y%m%d") tick_log = tick_dir / f"ticks_{yesterday}.jsonl" tick_count = 0 alerts = [] diff --git a/tests/test_tasks_bugfixes.py b/tests/test_tasks_bugfixes.py new file mode 100644 index 00000000..f2b86b87 --- /dev/null +++ b/tests/test_tasks_bugfixes.py @@ -0,0 +1,143 @@ +"""Tests for bugfixes in tasks.py from 2026-03-30 audit. + +Covers: + - NET_LINE_LIMIT raised from 10 → 500 to stop false-positive PR rejections + - memory_compress reads actions from tick_record["decision"]["actions"] + - good_morning_report reads yesterday's tick log, not today's +""" + +import json +from datetime import datetime, timezone, timedelta +from pathlib import Path + + +# ── NET_LINE_LIMIT ─────────────────────────────────────────────────── + +def test_net_line_limit_is_sane(): + """NET_LINE_LIMIT = 10 caused every real PR to be spam-rejected. + + Any value below ~200 is dangerously restrictive for a production repo. + 500 is the current target: large enough for feature PRs, small enough + to flag bulk commits. + """ + # Import at top level would pull in huey/orchestration; just grep instead. + tasks_path = Path(__file__).resolve().parent.parent / "tasks.py" + text = tasks_path.read_text() + + # Find the NET_LINE_LIMIT assignment + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("NET_LINE_LIMIT") and "=" in stripped: + value = int(stripped.split("=")[1].strip()) + assert value >= 200, ( + f"NET_LINE_LIMIT = {value} is too low. " + "Any value < 200 will reject most real PRs as over-limit." + ) + assert value <= 2000, ( + f"NET_LINE_LIMIT = {value} is too high — it won't catch bulk commits." + ) + break + else: + raise AssertionError("NET_LINE_LIMIT not found in tasks.py") + + +# ── memory_compress action path ────────────────────────────────────── + +def test_memory_compress_reads_decision_actions(): + """Actions live in tick_record['decision']['actions'], not tick_record['actions']. + + The old code read t.get("actions", []) which always returned [] because + the key is nested inside the decision dict. + """ + tasks_path = Path(__file__).resolve().parent.parent / "tasks.py" + text = tasks_path.read_text() + + # Find the memory_compress function body and verify the action path. + # We look for the specific pattern that reads decision.get("actions") + # within the ticks loop inside memory_compress. + in_memory_compress = False + found_correct_pattern = False + for line in text.splitlines(): + if "def memory_compress" in line or "def _memory_compress" in line: + in_memory_compress = True + elif in_memory_compress and line.strip().startswith("def "): + break + elif in_memory_compress: + # The correct pattern: decision = t.get("decision", {}) + if 'decision' in line and 't.get(' in line and '"decision"' in line: + found_correct_pattern = True + # The OLD bug: directly reading t.get("actions") + if 't.get("actions"' in line and 'decision' not in line: + raise AssertionError( + "Bug: memory_compress reads t.get('actions') directly. " + "Actions are nested under t['decision']['actions']." + ) + + assert found_correct_pattern, ( + "memory_compress does not read decision = t.get('decision', {})" + ) + + +# ── good_morning_report date bug ──────────────────────────────────── + +def test_good_morning_report_reads_yesterday_ticks(): + """good_morning_report runs at 6 AM. It should read YESTERDAY'S tick log, + not today's (which is mostly empty at 6 AM). + + The old code used `now.strftime('%Y%m%d')` which gives today. + The fix uses `(now - timedelta(days=1)).strftime('%Y%m%d')`. + """ + tasks_path = Path(__file__).resolve().parent.parent / "tasks.py" + text = tasks_path.read_text() + + # Find the good_morning_report function and check for the timedelta fix + in_gmr = False + uses_timedelta_for_yesterday = False + old_bug_pattern = False + for line in text.splitlines(): + if "def good_morning_report" in line: + in_gmr = True + elif in_gmr and line.strip().startswith("def "): + break + elif in_gmr: + # Check for the corrected pattern: timedelta subtraction + if "timedelta" in line and "days=1" in line: + uses_timedelta_for_yesterday = True + # Check for the old bug: yesterday = now.strftime(...) + # This is the direct assignment without timedelta + if 'yesterday = now.strftime' in line and 'timedelta' not in line: + old_bug_pattern = True + + assert not old_bug_pattern, ( + "Bug: good_morning_report sets yesterday = now.strftime(...) " + "which gives TODAY's date, not yesterday's." + ) + assert uses_timedelta_for_yesterday, ( + "good_morning_report should use timedelta(days=1) to compute yesterday's date." + ) + + +# ── review_prs includes file list ──────────────────────────────────── + +def test_review_prs_rejection_includes_file_list(): + """When review_prs rejects a PR, the comment should include the file list + so the author knows WHERE the bloat is, not just the net line count. + """ + tasks_path = Path(__file__).resolve().parent.parent / "tasks.py" + text = tasks_path.read_text() + + in_review_prs = False + has_file_list = False + for line in text.splitlines(): + if "def review_prs" in line: + in_review_prs = True + elif in_review_prs and line.strip().startswith("def "): + break + elif in_review_prs: + if "file_list" in line and "filename" in line: + has_file_list = True + + assert has_file_list, ( + "review_prs rejection comment should include a file_list " + "so the author knows which files contribute to the net diff." + )