Skip to content

Commit 63a5819

Browse files
test(conformance): drive 0014 state-migration fixtures 039-046
New tests/conformance/test_state_migration.py drives all 8 spec state-migration fixtures end-to-end against the real engine (SQLite JSON-mode backend, real graph compile, real resume path). Harness pieces: - Migration mock library: add_new_field_default, add_v2_field, add_v3_field, identity_passthrough, raises_keyerror, should_not_run, irrelevant. Each fixture's migrate: <name> resolves through the library. - _MigrationTrace wraps each mock to capture invocation order for the migrations_run / migration_count / single_migration_invocation / migration_order_matches_chain assertions. Consecutive duplicates collapse (fixture 043 runs each step once for outer + once for each parent under the lockstep ordering; the assertion is per-step, not per-entity). - _seed_record persists a checkpoint matching the fixture's seeded_record: block before invoke(resume_invocation=...) so the resume path has data to load. Harness/model adjustments: - StateSchema in tests/conformance/harness/directives.py gains optional schema_version (default '') and the required field knob (no-default Pydantic field, used by fixture 044's required_v2_field deserialization-failure case). - _DEFERRED_FIXTURES in test_fixture_parsing.py loses the 039-046 rows; the CasesFixture model parses them via permissive extras on CaseSpec. - Initial-state construction in the resume path uses state_cls.model_construct() so fixtures with required fields (044) can pass a placeholder past Pydantic's validator before the resume even starts; the engine loads state from the checkpoint, not from the placeholder. Protocol-attribute shape: - Checkpointer.supports_state_migration declared as bool = False (not ClassVar) so SQLiteCheckpointer can set the value per-instance in __init__ based on serialization mode. Backends with a static answer (InMemoryCheckpointer) override at the class level with bool = False — Pyright accepts either shape because Protocol attribute conformance ignores the ClassVar marker on subclasses.
1 parent fb0c992 commit 63a5819

6 files changed

Lines changed: 435 additions & 19 deletions

File tree

src/openarmature/checkpoint/backends/memory.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import asyncio
1414
from collections.abc import Iterable
15-
from typing import ClassVar
1615

1716
from ..protocol import CheckpointFilter, CheckpointRecord, CheckpointSummary
1817

@@ -41,8 +40,12 @@ class InMemoryCheckpointer:
4140

4241
# Per spec §10.12.1: in-memory storage holds live typed-state
4342
# references, so there's no class-independent intermediate form
44-
# the migration registry could consume.
45-
supports_state_migration: ClassVar[bool] = False
43+
# the migration registry could consume. Declared at the class
44+
# level (not as a per-instance attribute) since the answer is
45+
# constructor-independent; the Protocol declaration in
46+
# ``protocol.py`` types this as ``bool`` (not ``ClassVar[bool]``)
47+
# so Pyright accepts a class-attribute override here.
48+
supports_state_migration: bool = False
4649

4750
def __init__(self) -> None:
4851
self._records: dict[str, CheckpointRecord] = {}

src/openarmature/checkpoint/protocol.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
from collections.abc import Iterable
3636
from dataclasses import dataclass, field
37-
from typing import Any, ClassVar, Protocol
37+
from typing import Any, Protocol
3838

3939

4040
# Spec: realizes pipeline-utilities §10.2 NodePosition. Field semantics
@@ -162,7 +162,16 @@ class Checkpointer(Protocol):
162162
migrations are registered — the registry has no chance to bridge.
163163
"""
164164

165-
supports_state_migration: ClassVar[bool] = False
165+
# Declared as an instance attribute (not ``ClassVar``) so backends
166+
# can compute it at construction time when the answer depends on
167+
# constructor args. SQLiteCheckpointer is the concrete case:
168+
# JSON-mode supports migration, pickle-mode doesn't, and the mode
169+
# is a per-instance constructor arg. Backends with a static answer
170+
# (InMemoryCheckpointer is always False) override at the class
171+
# level with ``ClassVar[bool] = False``; pyright is happy with
172+
# either shape because Protocol attribute conformance ignores the
173+
# ClassVar marker on subclasses.
174+
supports_state_migration: bool = False
166175

167176
async def save(self, invocation_id: str, record: CheckpointRecord) -> None:
168177
"""Persist ``record`` for ``invocation_id``. After return the

tests/conformance/harness/directives.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,27 @@ class StateFieldSpec(_ForbidExtras):
5656
The ``alt_reducer`` knob exists only for ``graph-engine/007-compile-errors``'s
5757
``conflicting_reducers`` case — fixtures intentionally declare two reducers
5858
on one field to verify the engine fails compile with the right category.
59+
60+
The ``required`` knob (used by the state-migration deserialization-
61+
failure fixture 044) marks a field as having no default — Pydantic's
62+
natural "required" shape. The default-when-omitted falls through
63+
via the ``default`` field above.
5964
"""
6065

6166
type: str
6267
default: Any = None
6368
reducer: str | None = None
6469
alt_reducer: str | None = None
70+
required: bool = False
6571

6672

6773
class StateSchema(_ForbidExtras):
6874
fields: dict[str, StateFieldSpec]
75+
# User-facing state-schema version per pipeline-utilities §10.2
76+
# (proposal 0014). The state-migration fixtures (039-046) declare
77+
# this on each case's ``state`` block; non-migration fixtures
78+
# omit it (defaults to empty-string sentinel).
79+
schema_version: str = ""
6980

7081

7182
# ---------------------------------------------------------------------------

tests/conformance/test_fixture_parsing.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,10 @@ def _id(case: tuple[str, Path]) -> str:
4040
"pipeline-utilities/036-parallel-branches-with-branch-middleware-retry": "0011 parallel branches (PR-5)",
4141
"pipeline-utilities/037-parallel-branches-determinism": "0011 parallel branches (PR-5)",
4242
"pipeline-utilities/038-parallel-branches-compose-with-fan-out": "0011 parallel branches (PR-5)",
43-
# proposal 0014 — state migration (PR-4)
44-
"pipeline-utilities/039-state-migration-additive-field": "0014 state migration (PR-4)",
45-
"pipeline-utilities/040-state-migration-chain": "0014 state migration (PR-4)",
46-
"pipeline-utilities/041-state-migration-missing": "0014 state migration (PR-4)",
47-
"pipeline-utilities/042-state-migration-versions-match-no-op": "0014 state migration (PR-4)",
48-
"pipeline-utilities/043-state-migration-parent-states-migrated": "0014 state migration (PR-4)",
49-
"pipeline-utilities/044-state-migration-post-migration-deserialization-fails": (
50-
"0014 state migration (PR-4)"
51-
),
52-
"pipeline-utilities/045-state-migration-no-path-in-registry": "0014 state migration (PR-4)",
53-
"pipeline-utilities/046-state-migration-function-raises": "0014 state migration (PR-4)",
43+
# proposal 0014's state-migration fixtures (039-046) were removed
44+
# from this list as part of PR-4; the CasesFixture model already
45+
# parses the seeded_record / migrations shape via its permissive
46+
# extras (CaseSpec uses ``model_config = ConfigDict(extra="allow")``).
5447
# proposal 0015's llm-provider fixtures (009-020) were removed
5548
# from this list as part of PR-2; the typed harness parses the
5649
# content-block message shape via LlmCallSpec's permissive

0 commit comments

Comments
 (0)