diff --git a/chat/runtime.py b/chat/runtime.py index 6db8a24f..dd04c3af 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -795,12 +795,20 @@ class ChatRuntime: # single source of truth shared with the workbench continuity reader. self._loaded_engine_identity = str(manifest.get("engine_identity", "")) if self._loaded_engine_identity: + # A malformed identity_scheme (non-int) falls back to the legacy + # scheme — the verifying migration then reconciles it safely rather + # than crashing the load. A null/absent revision becomes "" so the + # legacy branch treats it as unverifiable (conservative DIVERGED). + try: + stored_scheme = int(manifest.get("identity_scheme", 1)) + except (TypeError, ValueError): + stored_scheme = 1 reconciliation = reconcile_loaded_identity( self.config, self._engine_identity, stored_identity=self._loaded_engine_identity, - stored_scheme=int(manifest.get("identity_scheme", 1)), - stored_revision=str(manifest.get("written_at_revision", "")), + stored_scheme=stored_scheme, + stored_revision=str(manifest.get("written_at_revision") or ""), ) if reconciliation is IdentityReconciliation.DIVERGED: self.identity_continuity_break = True diff --git a/tests/test_identity_provenance_split.py b/tests/test_identity_provenance_split.py index 78c610f5..08f88fba 100644 --- a/tests/test_identity_provenance_split.py +++ b/tests/test_identity_provenance_split.py @@ -180,3 +180,71 @@ def test_legacy_checkpoint_with_different_packs_breaks_without_strict( with pytest.warns(RuntimeWarning, match="identity continuity break"): runtime = ChatRuntime(config=RuntimeConfig(), engine_state_path=state_dir) assert runtime.identity_continuity_break is True + + +# --- malformed / missing manifest fields must not crash the guard or reader --- + + +def test_malformed_identity_scheme_does_not_crash_the_load_guard(tmp_path: Path) -> None: + # A non-int identity_scheme must fall back to the legacy scheme (verifying + # migration), not raise on int() while loading the checkpoint. + state_dir = tmp_path / "es" + state_dir.mkdir(parents=True) + legacy_id = compute_legacy_engine_identity(_DEFAULT_PACKS, "old-rev") + (state_dir / "manifest.json").write_text( + json.dumps( + { + "schema_version": 2, + "turn_count": 4, + "written_at_revision": "old-rev", + "engine_identity": legacy_id, + "identity_scheme": "not-an-int", # malformed + }, + sort_keys=True, + indent=2, + ), + encoding="utf-8", + ) + with pytest.warns(RuntimeWarning, match="migrating engine-identity stamp scheme"): + runtime = ChatRuntime( + config=RuntimeConfig(strict_identity_continuity=True), + engine_state_path=state_dir, + ) + assert runtime.identity_continuity_break is False + + +def test_reader_malformed_identity_scheme_falls_back_to_legacy() -> None: + from workbench.readers import _identity_continuity_from_manifest + + # A legacy stamp of the CURRENT default packs, but identity_scheme is garbage: + # the reader must fall back to legacy and verify-migrate -> "verified", not crash. + legacy = compute_legacy_engine_identity(_DEFAULT_PACKS, "r1") + result = _identity_continuity_from_manifest( + { + "schema_version": 2, + "turn_count": 1, + "written_at_revision": "r1", + "engine_identity": legacy, + "identity_scheme": "not-an-int", + } + ) + assert result is not None + assert result.status == "verified" + + +def test_reader_missing_revision_is_a_conservative_break() -> None: + from workbench.readers import _identity_continuity_from_manifest + + # A legacy stamp with NO written_at_revision can't be verified -> DIVERGED -> + # "break" (never blindly "verified"); and it must not crash on a None revision. + legacy = compute_legacy_engine_identity(_DEFAULT_PACKS, "r1") + result = _identity_continuity_from_manifest( + { + "schema_version": 2, + "turn_count": 1, + "engine_identity": legacy, + # no written_at_revision, no identity_scheme + } + ) + assert result is not None + assert result.status == "break" diff --git a/workbench/readers.py b/workbench/readers.py index adde8780..2d151175 100644 --- a/workbench/readers.py +++ b/workbench/readers.py @@ -1739,7 +1739,7 @@ def _identity_continuity_from_manifest( current_engine_identity, stored_identity=engine_identity, stored_scheme=stored_scheme, - stored_revision=written_at_revision, + stored_revision=written_at_revision or "", ) if reconciliation is IdentityReconciliation.DIVERGED: summary = "checkpoint identity differs from the current ratified substrate"