Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4a8ba6f
test(durable): reject impossible lifecycle progress
seonghobae Aug 12, 2026
c0f278d
fix(durable): preserve lifecycle progress invariant
seonghobae Aug 12, 2026
d282b4a
Merge protected main into durable progress invariants
seonghobae Aug 12, 2026
b95dea3
Merge branch 'main' into fix/durable-progress-invariants
opencode-agent[bot] Aug 12, 2026
a9bb139
test(durable): prove request total knownness boundary
seonghobae Aug 12, 2026
ad5626e
test(durable): exercise progress upsert against PostgreSQL
seonghobae Aug 12, 2026
887cc75
fix(durable): preserve request total knownness
seonghobae Aug 12, 2026
8c211d4
fix(schema): persist lifecycle total knownness
seonghobae Aug 12, 2026
3250bc7
fix(schema): mirror lifecycle total knownness
seonghobae Aug 12, 2026
660677c
fix(db): preserve model metadata helper during lifecycle reconciliation
seonghobae Aug 12, 2026
4dea49e
fix(durable): preserve lifecycle persistence compatibility
seonghobae Aug 12, 2026
3bc3264
noop
seonghobae Aug 12, 2026
32b28cb
chore: remove accidental branch placeholder
seonghobae Aug 12, 2026
ca2e02d
noop
seonghobae Aug 12, 2026
3f55f99
chore: remove accidental branch placeholder
seonghobae Aug 12, 2026
7b9f287
merge(main): reconcile durable invariant with current protected head
seonghobae Aug 12, 2026
94512e0
fix(stack): preserve current protected-main tree
seonghobae Aug 12, 2026
4f1c2a1
test(durable): verify internal total-known persistence
seonghobae Aug 12, 2026
ddc783f
merge(main): compose durable progress invariants with current protect…
seonghobae Aug 12, 2026
c4d2528
test(durable): require persisted state on rejected progress
seonghobae Aug 12, 2026
ce4097c
test(durable): expose rejected-upsert and migration evidence gaps
seonghobae Aug 12, 2026
e4c1fea
fix(durable): return persisted state after rejected progress merge
seonghobae Aug 12, 2026
aebf237
fix(schema): fail closed on historical progress corruption
seonghobae Aug 12, 2026
43ccbb9
fix(schema): preserve endpoint definition while hardening migration
seonghobae Aug 12, 2026
803494a
fix(schema): mirror fail-closed progress migration
seonghobae Aug 12, 2026
3140ee3
test(durable): cover rejected progress without persisted row
seonghobae Aug 12, 2026
9116637
Merge branch 'main' into fix/durable-progress-invariants
opencode-agent[bot] Aug 12, 2026
186f7da
fix(durable): tolerate minimal cursor doubles
seonghobae Aug 12, 2026
ac1157c
Merge branch 'main' into fix/durable-progress-invariants
opencode-agent[bot] Aug 12, 2026
40c29e6
test(db): reject caller-selected schema authority
seonghobae Aug 12, 2026
01d9dac
fix(db): remove caller-selected schema execution
seonghobae Aug 12, 2026
2291d4f
test(db): bind schema application to package asset
seonghobae Aug 12, 2026
7667078
merge(main): refresh durable invariants on protected head
seonghobae Aug 12, 2026
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
24 changes: 21 additions & 3 deletions pg_llm_batch/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,14 @@ def _normalize_remote_batch_snapshot(
)
counts_value = provider_batch.get("request_counts")
request_counts = counts_value if isinstance(counts_value, Mapping) else {}
total_requests = _provider_count(request_counts.get("total"))
completed_requests = _provider_count(request_counts.get("completed"))
failed_requests = _provider_count(request_counts.get("failed"))
if (
"total" in request_counts
and completed_requests + failed_requests > total_requests
):
raise ValueError("request_counts progress is inconsistent")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
provider_metadata, metadata_json = _provider_metadata(
provider_batch.get("metadata")
)
Expand All @@ -400,9 +408,9 @@ def _normalize_remote_batch_snapshot(
"batch_status": batch_status,
"output_file_id": output_file_id,
"error_file_id": error_file_id,
"total_requests": _provider_count(request_counts.get("total")),
"completed_requests": _provider_count(request_counts.get("completed")),
"failed_requests": _provider_count(request_counts.get("failed")),
"total_requests": total_requests,
"completed_requests": completed_requests,
"failed_requests": failed_requests,
"provider_metadata": provider_metadata,
"observed_at": observed,
"terminal_at": terminal_at,
Expand Down Expand Up @@ -496,6 +504,16 @@ def _persist_remote_batch_state(
),
updated_at = EXCLUDED.updated_at
WHERE EXCLUDED.observation_order > llm_remote_batch_jobs.observation_order
AND GREATEST(
llm_remote_batch_jobs.completed_requests,
EXCLUDED.completed_requests
) + GREATEST(
llm_remote_batch_jobs.failed_requests,
EXCLUDED.failed_requests
) <= GREATEST(
llm_remote_batch_jobs.total_requests,
EXCLUDED.total_requests
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
AND (
llm_remote_batch_jobs.batch_status NOT IN (
'completed', 'failed', 'expired', 'cancelled'
Expand Down
118 changes: 118 additions & 0 deletions tests/test_remote_batch_progress_invariant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# SPDX-License-Identifier: Apache-2.0
"""Regression tests for durable lifecycle progress invariants."""

from __future__ import annotations

from typing import Any

import pytest

from pg_llm_batch import db


class _Cursor:
"""Capture lifecycle SQL without contacting PostgreSQL."""

def __init__(self, driver: "_Psycopg") -> None:
self.driver = driver

def __enter__(self) -> "_Cursor":
return self

def __exit__(self, *_exc: Any) -> None:
return None

def execute(self, sql: str, params: Any = None) -> None:
"""Record one SQL statement and its bound parameters."""
self.driver.executions.append((sql, params))


class _Connection:
"""Expose the minimal connection surface used by lifecycle persistence."""

def __init__(self, driver: "_Psycopg") -> None:
self.driver = driver

def __enter__(self) -> "_Connection":
return self

def __exit__(self, *_exc: Any) -> None:
return None

def cursor(self) -> _Cursor:
"""Return one SQL-capturing cursor."""
return _Cursor(self.driver)

def commit(self) -> None:
"""Record an explicit transaction commit."""
self.driver.commits += 1


class _Psycopg:
"""Minimal deterministic psycopg replacement for boundary tests."""

def __init__(self) -> None:
self.executions: list[tuple[str, Any]] = []
self.connections: list[str] = []
self.commits = 0

def connect(self, dsn: str) -> _Connection:
"""Record the selected target before returning a fake connection."""
self.connections.append(dsn)
return _Connection(self)


def _compact_sql(sql: str) -> str:
"""Normalize SQL whitespace for semantic contract assertions."""
return " ".join(sql.split())


def test_persistence_rejects_impossible_same_observation_before_database(
monkeypatch: Any,
) -> None:
"""Completed plus failed requests cannot exceed the observed total."""
driver = _Psycopg()
monkeypatch.setattr(db, "psycopg", driver)

with pytest.raises(ValueError, match="request_counts progress is inconsistent"):
db.persist_remote_batch_state(
"postgresql://example",
"primary",
{
"id": "batch-1",
"status": "in_progress",
"request_counts": {"total": 1, "completed": 1, "failed": 1},
},
observation_order=1,
)

assert driver.connections == []
assert driver.executions == []
assert driver.commits == 0


def test_upsert_guards_combined_monotonic_progress_invariant(monkeypatch: Any) -> None:
"""Independent monotonic counter updates cannot create impossible progress."""
driver = _Psycopg()
monkeypatch.setattr(db, "psycopg", driver)

db.persist_remote_batch_state(
"postgresql://example",
"primary",
{
"id": "batch-1",
"status": "in_progress",
"request_counts": {"total": 10, "completed": 9, "failed": 0},
},
observation_order=1,
)

sql = _compact_sql(driver.executions[1][0])
expected_guard = (
"GREATEST( llm_remote_batch_jobs.completed_requests, "
"EXCLUDED.completed_requests ) + GREATEST( "
"llm_remote_batch_jobs.failed_requests, EXCLUDED.failed_requests ) "
"<= GREATEST( llm_remote_batch_jobs.total_requests, "
"EXCLUDED.total_requests )"
)
assert expected_guard in sql
Loading