מוסיף get_predecessor_context(issue_id, limit) ב-paperclip_client — read-only,
מחזיר ריצות-heartbeat שהסתיימו על אותו issue (newest-first) עם ה-summary שכל
heartbeat משאיר ב-result_json, כך ש-wake טרי קורא מה קודמיו הסיקו במקום לגלות-מחדש
(ה-generic blind heartbeat). קשירת run→issue דרך wakeup_request_id→payload.issueId;
limit מוגבל 1..10.
- Port: pc_get_predecessor_context (read-only, לא עטוף-טלמטריה).
- endpoint: GET /api/operations/issues/{issue_id}/predecessor?limit=3.
- 3 בדיקות (fake-asyncpg: shaping+isoformat, clamp, empty). אומת חי read-only על
issue בן 11 ריצות → מוחזרות מסקנות-דיספוזיציה אמיתיות.
Invariants: מקיים G12 (fetch מהמעטפת, המגע מהשער; אין REST/כלי-פלטפורמה בליבה),
G2 (מקור-predecessor יחיד). follow-up: MCP tool + HEARTBEAT (wake קורא בתחילת ריצה).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
2.4 KiB
Python
85 lines
2.4 KiB
Python
"""Tests for #220 — paperclip_client.get_predecessor_context ("seance").
|
|
|
|
Fakes an asyncpg connection so it never touches the live DB. Verifies the
|
|
limit clamp, row shaping (timestamps → isoformat), and the empty case.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("PAPERCLIP_DB_URL", "postgres://x:x@127.0.0.1:54329/paperclip")
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # web/
|
|
|
|
pc = pytest.importorskip("paperclip_client", reason="web deps unavailable")
|
|
|
|
|
|
class _FakeConn:
|
|
def __init__(self, rows):
|
|
self._rows = rows
|
|
self.calls: list[tuple] = []
|
|
|
|
async def fetch(self, query, *args):
|
|
self.calls.append((query, args))
|
|
return self._rows
|
|
|
|
async def close(self):
|
|
pass
|
|
|
|
|
|
def _row():
|
|
return {
|
|
"id": "run-1",
|
|
"status": "succeeded",
|
|
"started_at": datetime(2026, 7, 5, 10, 0, 0),
|
|
"finished_at": datetime(2026, 7, 5, 10, 5, 0),
|
|
"summary": "כתבתי בלוק ז — 3,206 מילים",
|
|
"error_code": None,
|
|
"session_id_after": "sess-abc",
|
|
"agent_name": "writer",
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def fake(monkeypatch):
|
|
holder = {"rows": [_row()], "conn": None}
|
|
|
|
async def _connect(_url):
|
|
holder["conn"] = _FakeConn(holder["rows"])
|
|
return holder["conn"]
|
|
|
|
monkeypatch.setattr(pc.asyncpg, "connect", _connect)
|
|
return holder
|
|
|
|
|
|
def test_shapes_runs_newest_first(fake):
|
|
result = asyncio.run(pc.get_predecessor_context("iss-1"))
|
|
assert result["ok"] is True and result["issue_id"] == "iss-1"
|
|
(run,) = result["runs"]
|
|
assert run["run_id"] == "run-1"
|
|
assert run["status"] == "succeeded"
|
|
assert run["started_at"] == "2026-07-05T10:00:00"
|
|
assert run["finished_at"] == "2026-07-05T10:05:00"
|
|
assert "בלוק ז" in run["summary"]
|
|
assert run["session_id"] == "sess-abc"
|
|
assert run["agent_name"] == "writer"
|
|
|
|
|
|
def test_limit_is_clamped(fake):
|
|
asyncio.run(pc.get_predecessor_context("iss-1", limit=99))
|
|
assert fake["conn"].calls[0][1] == ("iss-1", 10) # clamped high
|
|
asyncio.run(pc.get_predecessor_context("iss-1", limit=0))
|
|
assert fake["conn"].calls[0][1] == ("iss-1", 1) # clamped low
|
|
|
|
|
|
def test_empty(fake):
|
|
fake["rows"] = []
|
|
result = asyncio.run(pc.get_predecessor_context("ghost"))
|
|
assert result == {"ok": True, "issue_id": "ghost", "runs": []}
|