feat(release): bound durable run retention
This commit is contained in:
@@ -792,6 +792,97 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
self.assertEqual("unavailable", listed[-1]["status"])
|
||||
self.assertEqual(older["run_id"], self.store.list(limit=1)[0]["run_id"])
|
||||
|
||||
def test_cursor_pagination_is_stable_and_keeps_unavailable_records_visible(
|
||||
self,
|
||||
) -> None:
|
||||
created: list[dict[str, object]] = []
|
||||
for index in range(3):
|
||||
with patch(
|
||||
"govoplan_release.release_run._timestamp",
|
||||
return_value=f"2026-07-22T0{index}:00:00Z",
|
||||
):
|
||||
created.append(
|
||||
create_run(
|
||||
self.store,
|
||||
request_id=f"cursor-page-create-request-{index:04d}",
|
||||
)
|
||||
)
|
||||
corrupt_path = self.root / f"{created[0]['run_id']}.json"
|
||||
corrupt_path.write_text("{}", encoding="utf-8")
|
||||
corrupt_path.chmod(0o600)
|
||||
|
||||
first = self.store.list_page(limit=1)
|
||||
second = self.store.list_page(limit=1, cursor=first["next_cursor"])
|
||||
third = self.store.list_page(limit=1, cursor=second["next_cursor"])
|
||||
|
||||
self.assertEqual(created[2]["run_id"], first["runs"][0]["run_id"])
|
||||
self.assertEqual(created[1]["run_id"], second["runs"][0]["run_id"])
|
||||
self.assertEqual("unavailable", third["runs"][0]["status"])
|
||||
self.assertIsNone(third["next_cursor"])
|
||||
with self.assertRaisesRegex(ReleaseRunConflict, "cursor is invalid"):
|
||||
self.store.list_page(limit=1, cursor=f"{first['next_cursor']}tampered")
|
||||
|
||||
def test_retention_prunes_only_oldest_completed_records_and_fails_closed(
|
||||
self,
|
||||
) -> None:
|
||||
with patch("govoplan_release.release_run.MAX_RUN_RECORDS", 3):
|
||||
completed: list[dict[str, object]] = []
|
||||
for index in range(2):
|
||||
with patch(
|
||||
"govoplan_release.release_run._timestamp",
|
||||
return_value=f"2026-07-22T0{index}:00:00Z",
|
||||
):
|
||||
run = create_run(
|
||||
self.store,
|
||||
request_id=f"retention-completed-create-{index:04d}",
|
||||
plan_snapshot=mutating_first_plan(),
|
||||
)
|
||||
attempt_id = f"retention-completed-attempt-{index:04d}"
|
||||
self.store.start_step(
|
||||
run["run_id"], "core:tag", attempt_id=attempt_id
|
||||
)
|
||||
self.store.finish_step(
|
||||
run["run_id"],
|
||||
"core:tag",
|
||||
attempt_id=attempt_id,
|
||||
succeeded=True,
|
||||
result_code="tag_created",
|
||||
)
|
||||
completed.append(run)
|
||||
active = create_run(
|
||||
self.store,
|
||||
request_id="retention-active-create-0001",
|
||||
)
|
||||
replacement = create_run(
|
||||
self.store,
|
||||
request_id="retention-replacement-create-0001",
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
(self.root / f"{completed[0]['run_id']}.json").exists()
|
||||
)
|
||||
self.assertTrue((self.root / f"{completed[1]['run_id']}.json").exists())
|
||||
self.assertTrue((self.root / f"{active['run_id']}.json").exists())
|
||||
self.assertTrue((self.root / f"{replacement['run_id']}.json").exists())
|
||||
|
||||
corrupt_path = self.root / f"{completed[1]['run_id']}.json"
|
||||
corrupt_path.write_text("{}", encoding="utf-8")
|
||||
corrupt_path.chmod(0o600)
|
||||
with self.assertRaisesRegex(ReleaseRunConflict, "retention limit"):
|
||||
create_run(
|
||||
self.store,
|
||||
request_id="retention-refused-create-0001",
|
||||
)
|
||||
self.assertEqual(3, len(list(self.root.glob("*.json"))))
|
||||
self.assertIn(
|
||||
completed[1]["run_id"],
|
||||
{
|
||||
item["run_id"]
|
||||
for item in self.store.list_page(limit=3)["runs"]
|
||||
if item["status"] == "unavailable"
|
||||
},
|
||||
)
|
||||
|
||||
def test_list_enumeration_oserror_is_normalized(self) -> None:
|
||||
self.store.list()
|
||||
with patch.object(
|
||||
@@ -872,6 +963,51 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
|
||||
|
||||
class ReleaseRunApiTests(unittest.TestCase):
|
||||
def test_api_exposes_and_validates_cursor_pagination(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
app = create_app(
|
||||
workspace_root=Path(temp_dir),
|
||||
run_state_root=Path(temp_dir) / "runs",
|
||||
token="token",
|
||||
)
|
||||
headers = {"X-Release-Console-Token": "token"}
|
||||
with (
|
||||
patch("server.app.build_dashboard", return_value=object()),
|
||||
patch(
|
||||
"server.app.build_selective_release_plan",
|
||||
return_value=release_plan(),
|
||||
),
|
||||
TestClient(app) as client,
|
||||
):
|
||||
for index in range(2):
|
||||
response = client.post(
|
||||
"/api/release-runs",
|
||||
headers=headers,
|
||||
json=api_run_input(
|
||||
request_id=f"api-page-create-request-{index:04d}"
|
||||
),
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
first = client.get(
|
||||
"/api/release-runs?limit=1", headers=headers
|
||||
).json()
|
||||
second = client.get(
|
||||
"/api/release-runs",
|
||||
headers=headers,
|
||||
params={"limit": 1, "cursor": first["next_cursor"]},
|
||||
).json()
|
||||
invalid = client.get(
|
||||
"/api/release-runs?cursor=not-a-cursor", headers=headers
|
||||
)
|
||||
|
||||
self.assertEqual(1, len(first["runs"]))
|
||||
self.assertEqual(1, len(second["runs"]))
|
||||
self.assertNotEqual(
|
||||
first["runs"][0]["run_id"], second["runs"][0]["run_id"]
|
||||
)
|
||||
self.assertIsNone(second["next_cursor"])
|
||||
self.assertEqual(409, invalid.status_code)
|
||||
|
||||
def test_token_guarded_create_list_read_resume_and_retry(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir) / "runs"
|
||||
|
||||
Reference in New Issue
Block a user