Repository navigation
fix(durable): enforce lifecycle progress invariants #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 c0f278d
fix(durable): preserve lifecycle progress invariant
seonghobae d282b4a
Merge protected main into durable progress invariants
seonghobae b95dea3
Merge branch 'main' into fix/durable-progress-invariants
opencode-agent[bot] a9bb139
test(durable): prove request total knownness boundary
seonghobae ad5626e
test(durable): exercise progress upsert against PostgreSQL
seonghobae 887cc75
fix(durable): preserve request total knownness
seonghobae 8c211d4
fix(schema): persist lifecycle total knownness
seonghobae 3250bc7
fix(schema): mirror lifecycle total knownness
seonghobae 660677c
fix(db): preserve model metadata helper during lifecycle reconciliation
seonghobae 4dea49e
fix(durable): preserve lifecycle persistence compatibility
seonghobae 3bc3264
noop
seonghobae 32b28cb
chore: remove accidental branch placeholder
seonghobae ca2e02d
noop
seonghobae 3f55f99
chore: remove accidental branch placeholder
seonghobae 7b9f287
merge(main): reconcile durable invariant with current protected head
seonghobae 94512e0
fix(stack): preserve current protected-main tree
seonghobae 4f1c2a1
test(durable): verify internal total-known persistence
seonghobae ddc783f
merge(main): compose durable progress invariants with current protect…
seonghobae c4d2528
test(durable): require persisted state on rejected progress
seonghobae ce4097c
test(durable): expose rejected-upsert and migration evidence gaps
seonghobae e4c1fea
fix(durable): return persisted state after rejected progress merge
seonghobae aebf237
fix(schema): fail closed on historical progress corruption
seonghobae 43ccbb9
fix(schema): preserve endpoint definition while hardening migration
seonghobae 803494a
fix(schema): mirror fail-closed progress migration
seonghobae 3140ee3
test(durable): cover rejected progress without persisted row
seonghobae 9116637
Merge branch 'main' into fix/durable-progress-invariants
opencode-agent[bot] 186f7da
fix(durable): tolerate minimal cursor doubles
seonghobae ac1157c
Merge branch 'main' into fix/durable-progress-invariants
opencode-agent[bot] 40c29e6
test(db): reject caller-selected schema authority
seonghobae 01d9dac
fix(db): remove caller-selected schema execution
seonghobae 2291d4f
test(db): bind schema application to package asset
seonghobae 7667078
merge(main): refresh durable invariants on protected head
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.