From 83c55f6ab656ac3096f95459c37a079604fb0f26 Mon Sep 17 00:00:00 2001 From: Vatche Isahagian Date: Fri, 1 May 2026 15:49:34 -0400 Subject: [PATCH 1/5] fix(e2e): resolve issue #244 test failures - filesystem backend: atomic writes + graceful recovery from corrupt JSON - codex subscribe: warn on audit-append failure instead of rolling back - shared config: invalid-repo-name warning goes to stdout - segmentation test: mark flaky to absorb LLM variance - cross-namespace sharing test: check leaked content, not echoed query - .gitignore: exclude .codex --- .gitignore | 1 + altk_evolve/backend/filesystem.py | 22 ++++++++++++++++--- .../claude/plugins/evolve-lite/lib/config.py | 3 +-- .../plugins/evolve-lite/lib/config.py | 3 +-- .../skills/subscribe/scripts/subscribe.py | 11 +--------- tests/e2e/test_e2e_segmentation.py | 1 + tests/e2e/test_sharing.py | 9 +++++--- 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 872e22e5..f4713cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ dist .secrets event.json site/ +.codex diff --git a/altk_evolve/backend/filesystem.py b/altk_evolve/backend/filesystem.py index a752d67e..48b336a5 100644 --- a/altk_evolve/backend/filesystem.py +++ b/altk_evolve/backend/filesystem.py @@ -1,6 +1,7 @@ import datetime import json import logging +import os import uuid from pathlib import Path from threading import Lock @@ -51,12 +52,27 @@ def _load_namespace_data(self, namespace_id: str) -> FilesystemNamespace: file_path = self._namespace_file(namespace_id) if not file_path.exists(): raise NamespaceNotFoundException(f"Namespace `{namespace_id}` not found") - return FilesystemNamespace.model_validate(json.loads(file_path.read_text())) + raw = file_path.read_text() + if not raw.strip(): + logger.warning("Namespace file %s is empty (likely an interrupted write); treating as missing.", file_path) + raise NamespaceNotFoundException(f"Namespace `{namespace_id}` not found") + try: + return FilesystemNamespace.model_validate(json.loads(raw)) + except json.JSONDecodeError as e: + logger.warning("Namespace file %s is corrupt (%s); treating as missing.", file_path, e) + raise NamespaceNotFoundException(f"Namespace `{namespace_id}` not found") from e def _save_namespace_data(self, namespace_id: str, data: FilesystemNamespace): - """Save namespace data to JSON file.""" + """Save namespace data to JSON file atomically. + + Why: Path.write_text truncates the file immediately and leaves it 0 bytes if the + process is interrupted mid-write (SIGTERM, kill, crash). Write to a sibling tmp + file and os.replace() so readers always see either the old or new complete file. + """ file_path = self._namespace_file(namespace_id) - file_path.write_text(data.model_dump_json(indent=2)) + tmp_path = file_path.with_suffix(file_path.suffix + ".tmp") + tmp_path.write_text(data.model_dump_json(indent=2)) + os.replace(tmp_path, file_path) def ready(self) -> bool: """Check if the backend is healthy.""" diff --git a/platform-integrations/claude/plugins/evolve-lite/lib/config.py b/platform-integrations/claude/plugins/evolve-lite/lib/config.py index 8efdb444..da125ee6 100644 --- a/platform-integrations/claude/plugins/evolve-lite/lib/config.py +++ b/platform-integrations/claude/plugins/evolve-lite/lib/config.py @@ -337,8 +337,7 @@ def _coerce_repo(entry): return None if not is_valid_repo_name(name.strip()): print( - f"evolve-lite: ignoring repo entry {name!r} — invalid name (only A-Z, a-z, 0-9, '.', '_', '-' allowed)", - file=sys.stderr, + f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed" ) return None if not isinstance(remote, str) or not remote.strip(): diff --git a/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py b/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py index 8efdb444..da125ee6 100644 --- a/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py +++ b/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py @@ -337,8 +337,7 @@ def _coerce_repo(entry): return None if not is_valid_repo_name(name.strip()): print( - f"evolve-lite: ignoring repo entry {name!r} — invalid name (only A-Z, a-z, 0-9, '.', '_', '-' allowed)", - file=sys.stderr, + f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed" ) return None if not isinstance(remote, str) or not remote.strip(): diff --git a/platform-integrations/codex/plugins/evolve-lite/skills/subscribe/scripts/subscribe.py b/platform-integrations/codex/plugins/evolve-lite/skills/subscribe/scripts/subscribe.py index 7a50c2e0..26472b97 100644 --- a/platform-integrations/codex/plugins/evolve-lite/skills/subscribe/scripts/subscribe.py +++ b/platform-integrations/codex/plugins/evolve-lite/skills/subscribe/scripts/subscribe.py @@ -113,16 +113,7 @@ def main(): remote=args.remote, ) except Exception as exc: - repos.pop() - set_repos(cfg, repos) - try: - save_config(cfg, project_root) - except Exception: - pass - if dest.exists(): - shutil.rmtree(dest) - print(f"Error: failed to record subscription — clone removed: {exc}", file=sys.stderr) - sys.exit(1) + print(f"Warning: failed to append audit entry for subscribe: {exc}", file=sys.stderr) print(f"Subscribed to '{args.name}' (scope={args.scope}) from {args.remote}") diff --git a/tests/e2e/test_e2e_segmentation.py b/tests/e2e/test_e2e_segmentation.py index d1e29cba..7712ec83 100644 --- a/tests/e2e/test_e2e_segmentation.py +++ b/tests/e2e/test_e2e_segmentation.py @@ -10,6 +10,7 @@ TRAJECTORY = json.loads((Path(__file__).parent.parent / "fixtures" / "appworld_venmo_task_trajectory.json").read_text()) +@pytest.mark.flaky(retries=2, delay=1) def test_segment_trajectory_min_subtasks(): """Real trajectory produces at least 7 subtasks (gold standard has 7).""" subtasks = segment_trajectory(TRAJECTORY) diff --git a/tests/e2e/test_sharing.py b/tests/e2e/test_sharing.py index 4bd8f877..95ab665a 100644 --- a/tests/e2e/test_sharing.py +++ b/tests/e2e/test_sharing.py @@ -122,15 +122,18 @@ async def test_cross_namespace_public_discovery(mcp): "get_entities", {"task": "dependency injection", "entity_type": "guideline", "include_public": True}, ) - assert "dependency injection" in with_public.content[0].text + assert "use dependency injection for testability" in with_public.content[0].text assert "[public: alice]" in with_public.content[0].text - # Without include_public it must NOT appear (it lives in a different namespace) + # Without include_public it must NOT appear (it lives in a different namespace). + # Check for the entity content and owner marker — the query task echoes in + # the response header ("# Guidelines for: dependency injection") regardless of hits. without_public = await client.call_tool_mcp( "get_entities", {"task": "dependency injection", "entity_type": "guideline", "include_public": False}, ) - assert "dependency injection" not in without_public.content[0].text + assert "use dependency injection for testability" not in without_public.content[0].text + assert "[public: alice]" not in without_public.content[0].text finally: try: second_client.delete_namespace(second_ns) From 253338f0f513fe0af413349806a66cc23809c423 Mon Sep 17 00:00:00 2001 From: Vatche Isahagian Date: Fri, 1 May 2026 22:11:06 -0400 Subject: [PATCH 2/5] fix ruff formatting --- .../claude/plugins/evolve-lite/lib/config.py | 4 +--- .../claw-code/plugins/evolve-lite/lib/config.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/platform-integrations/claude/plugins/evolve-lite/lib/config.py b/platform-integrations/claude/plugins/evolve-lite/lib/config.py index da125ee6..2258fbf8 100644 --- a/platform-integrations/claude/plugins/evolve-lite/lib/config.py +++ b/platform-integrations/claude/plugins/evolve-lite/lib/config.py @@ -336,9 +336,7 @@ def _coerce_repo(entry): if not isinstance(name, str) or not name.strip(): return None if not is_valid_repo_name(name.strip()): - print( - f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed" - ) + print(f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed") return None if not isinstance(remote, str) or not remote.strip(): return None diff --git a/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py b/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py index da125ee6..2258fbf8 100644 --- a/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py +++ b/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py @@ -336,9 +336,7 @@ def _coerce_repo(entry): if not isinstance(name, str) or not name.strip(): return None if not is_valid_repo_name(name.strip()): - print( - f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed" - ) + print(f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed") return None if not isinstance(remote, str) or not remote.strip(): return None From 97a3e0223e8348a66bf2dc6cd0f472152cef44e3 Mon Sep 17 00:00:00 2001 From: Vatche Isahagian Date: Mon, 4 May 2026 11:39:20 -0400 Subject: [PATCH 3/5] fix(filesystem): address Codex review on interrupted-write recovery - Unique tmp filenames for namespace saves: concurrent CLI/MCP writers sharing data_dir no longer clobber each other's .json.tmp, which caused FileNotFoundError at os.replace time. - Unlink empty/corrupt namespace files when classifying as missing, so the create_namespace() call inside ensure_namespace() doesn't trip on the stale file and raise NamespaceAlreadyExistsException, which left startup wedged despite the "treat as missing" recovery branch. Adds tests/unit/test_filesystem_backend.py covering both paths. --- altk_evolve/backend/filesystem.py | 30 +++++++++++---- tests/unit/test_filesystem_backend.py | 53 +++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 tests/unit/test_filesystem_backend.py diff --git a/altk_evolve/backend/filesystem.py b/altk_evolve/backend/filesystem.py index 48b336a5..28f55ac6 100644 --- a/altk_evolve/backend/filesystem.py +++ b/altk_evolve/backend/filesystem.py @@ -48,31 +48,45 @@ def _namespace_file(self, namespace_id: str) -> Path: return self.data_dir / f"{namespace_id}.json" def _load_namespace_data(self, namespace_id: str) -> FilesystemNamespace: - """Load namespace data from JSON file.""" + """Load namespace data from JSON file. + + Empty or corrupt files are treated as missing AND unlinked, so that a subsequent + create_namespace() call does not trip on the stale file and raise + NamespaceAlreadyExistsException, which would leave ensure_namespace() stuck. + """ file_path = self._namespace_file(namespace_id) if not file_path.exists(): raise NamespaceNotFoundException(f"Namespace `{namespace_id}` not found") raw = file_path.read_text() if not raw.strip(): - logger.warning("Namespace file %s is empty (likely an interrupted write); treating as missing.", file_path) + logger.warning("Namespace file %s is empty (likely an interrupted write); removing and treating as missing.", file_path) + file_path.unlink(missing_ok=True) raise NamespaceNotFoundException(f"Namespace `{namespace_id}` not found") try: return FilesystemNamespace.model_validate(json.loads(raw)) except json.JSONDecodeError as e: - logger.warning("Namespace file %s is corrupt (%s); treating as missing.", file_path, e) + logger.warning("Namespace file %s is corrupt (%s); removing and treating as missing.", file_path, e) + file_path.unlink(missing_ok=True) raise NamespaceNotFoundException(f"Namespace `{namespace_id}` not found") from e def _save_namespace_data(self, namespace_id: str, data: FilesystemNamespace): """Save namespace data to JSON file atomically. Why: Path.write_text truncates the file immediately and leaves it 0 bytes if the - process is interrupted mid-write (SIGTERM, kill, crash). Write to a sibling tmp - file and os.replace() so readers always see either the old or new complete file. + process is interrupted mid-write (SIGTERM, kill, crash). Write to a uniquely-named + sibling tmp file and os.replace() so readers always see either the old or new + complete file. The uuid suffix keeps concurrent writers from the same data_dir + (e.g. CLI + MCP server) from clobbering each other's tmp file, which would cause + FileNotFoundError at os.replace time. """ file_path = self._namespace_file(namespace_id) - tmp_path = file_path.with_suffix(file_path.suffix + ".tmp") - tmp_path.write_text(data.model_dump_json(indent=2)) - os.replace(tmp_path, file_path) + tmp_path = file_path.with_suffix(f"{file_path.suffix}.tmp.{uuid.uuid4().hex}") + try: + tmp_path.write_text(data.model_dump_json(indent=2)) + os.replace(tmp_path, file_path) + except Exception: + tmp_path.unlink(missing_ok=True) + raise def ready(self) -> bool: """Check if the backend is healthy.""" diff --git a/tests/unit/test_filesystem_backend.py b/tests/unit/test_filesystem_backend.py new file mode 100644 index 00000000..913264bc --- /dev/null +++ b/tests/unit/test_filesystem_backend.py @@ -0,0 +1,53 @@ +from pathlib import Path + +import pytest + +from altk_evolve.backend.filesystem import FilesystemEntityBackend +from altk_evolve.config.evolve import EvolveConfig +from altk_evolve.config.filesystem import FilesystemSettings +from altk_evolve.frontend.client.evolve_client import EvolveClient + + +@pytest.fixture +def client(tmp_path: Path) -> EvolveClient: + return EvolveClient(config=EvolveConfig(backend="filesystem", settings=FilesystemSettings(data_dir=str(tmp_path)))) + + +@pytest.fixture +def backend(tmp_path: Path) -> FilesystemEntityBackend: + return FilesystemEntityBackend(config=FilesystemSettings(data_dir=str(tmp_path))) + + +def test_ensure_namespace_recovers_from_zero_byte_file(client: EvolveClient, tmp_path: Path): + stale = tmp_path / "ns_stale.json" + stale.write_text("") + assert stale.exists() and stale.stat().st_size == 0 + + ns = client.ensure_namespace("ns_stale") + + assert ns.id == "ns_stale" + assert ns.num_entities == 0 + assert stale.exists() and stale.stat().st_size > 0 + + +def test_ensure_namespace_recovers_from_corrupt_json(client: EvolveClient, tmp_path: Path): + corrupt = tmp_path / "ns_corrupt.json" + corrupt.write_text("{not json") + + ns = client.ensure_namespace("ns_corrupt") + + assert ns.id == "ns_corrupt" + assert ns.num_entities == 0 + + +def test_save_tolerates_stale_shared_tmp(backend: FilesystemEntityBackend, tmp_path: Path): + """Stale .json.tmp from an interrupted write must not block subsequent saves. + + Regression guard for concurrent CLI+MCP writers that used to share the tmp name. + """ + (tmp_path / "ns_busy.json.tmp").write_text("leftover") + + backend.create_namespace("ns_busy") + + target = tmp_path / "ns_busy.json" + assert target.exists() and target.stat().st_size > 0 From da2344069ef812b0b63bb1378e06424a69544b64 Mon Sep 17 00:00:00 2001 From: Vatche Isahagian Date: Mon, 4 May 2026 13:13:12 -0400 Subject: [PATCH 4/5] addressing comments --- .../claude/plugins/evolve-lite/lib/config.py | 5 ++++- .../claw-code/plugins/evolve-lite/lib/config.py | 5 ++++- tests/platform_integrations/test_bob_sharing.py | 6 +++--- tests/platform_integrations/test_codex_sharing.py | 2 +- tests/platform_integrations/test_sync.py | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/platform-integrations/claude/plugins/evolve-lite/lib/config.py b/platform-integrations/claude/plugins/evolve-lite/lib/config.py index 2258fbf8..9414862f 100644 --- a/platform-integrations/claude/plugins/evolve-lite/lib/config.py +++ b/platform-integrations/claude/plugins/evolve-lite/lib/config.py @@ -336,7 +336,10 @@ def _coerce_repo(entry): if not isinstance(name, str) or not name.strip(): return None if not is_valid_repo_name(name.strip()): - print(f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed") + print( + f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed", + file=sys.stderr, + ) return None if not isinstance(remote, str) or not remote.strip(): return None diff --git a/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py b/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py index 2258fbf8..9414862f 100644 --- a/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py +++ b/platform-integrations/claw-code/plugins/evolve-lite/lib/config.py @@ -336,7 +336,10 @@ def _coerce_repo(entry): if not isinstance(name, str) or not name.strip(): return None if not is_valid_repo_name(name.strip()): - print(f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed") + print( + f"evolve-lite: {name!r} (skipped - invalid subscription name) — only A-Z, a-z, 0-9, '.', '_', '-' allowed", + file=sys.stderr, + ) return None if not isinstance(remote, str) or not remote.strip(): return None diff --git a/tests/platform_integrations/test_bob_sharing.py b/tests/platform_integrations/test_bob_sharing.py index f5063a78..f9108352 100644 --- a/tests/platform_integrations/test_bob_sharing.py +++ b/tests/platform_integrations/test_bob_sharing.py @@ -375,7 +375,7 @@ def test_skips_invalid_subscription_name(self, temp_project_dir): cfg_path.write_text('repos:\n - name: "../outside"\n scope: "read"\n remote: "git@github.com:x/y.git"\n branch: "main"\n') result = run_script(SYNC_SCRIPT, temp_project_dir, evolve_dir=evolve_dir) assert result.returncode == 0 - assert "invalid subscription name" in result.stdout + assert "invalid subscription name" in result.stderr assert not (evolve_dir / "entities" / "subscribed" / "outside").exists() def test_rejects_dot_and_double_dot_names(self, temp_project_dir): @@ -386,12 +386,12 @@ def test_rejects_dot_and_double_dot_names(self, temp_project_dir): cfg_path.write_text('repos:\n - name: "."\n scope: "read"\n remote: "git@github.com:x/y.git"\n branch: "main"\n') result = run_script(SYNC_SCRIPT, temp_project_dir, evolve_dir=evolve_dir) assert result.returncode == 0 - assert "invalid subscription name" in result.stdout + assert "invalid subscription name" in result.stderr cfg_path.write_text('repos:\n - name: ".."\n scope: "read"\n remote: "git@github.com:x/y.git"\n branch: "main"\n') result = run_script(SYNC_SCRIPT, temp_project_dir, evolve_dir=evolve_dir) assert result.returncode == 0 - assert "invalid subscription name" in result.stdout + assert "invalid subscription name" in result.stderr def test_removed_entity_disappears_after_sync(self, subscribed_project): """Entities deleted from a read-scope remote are removed from the mirror on next sync.""" diff --git a/tests/platform_integrations/test_codex_sharing.py b/tests/platform_integrations/test_codex_sharing.py index b1ec5e73..cca75053 100644 --- a/tests/platform_integrations/test_codex_sharing.py +++ b/tests/platform_integrations/test_codex_sharing.py @@ -515,7 +515,7 @@ def test_sync_skips_invalid_subscription_name(self, temp_project_dir): ) assert result.returncode == 0 - assert "'.' (skipped - invalid subscription name)" in result.stdout + assert "'.' (skipped - invalid subscription name)" in result.stderr assert not (evolve_dir / "entities" / "subscribed").exists() def test_sync_uses_workspace_config_with_custom_evolve_dir(self, temp_project_dir, local_repo): diff --git a/tests/platform_integrations/test_sync.py b/tests/platform_integrations/test_sync.py index 5a77f21d..00f4d754 100644 --- a/tests/platform_integrations/test_sync.py +++ b/tests/platform_integrations/test_sync.py @@ -190,7 +190,7 @@ def test_skips_invalid_subscription_name(self, temp_project_dir): cfg_path.write_text("repos:\n - name: ../evil\n scope: read\n remote: git@github.com:x/y.git\n branch: main\n") result = run_script(SYNC_SCRIPT, temp_project_dir, evolve_dir=evolve_dir) assert result.returncode == 0 - assert "invalid subscription name" in result.stdout + assert "invalid subscription name" in result.stderr assert not (evolve_dir / "entities" / "evil").exists() def test_manual_run_ignores_on_session_start_false(self, subscribed_project): From c2397e3e36bdfcb4088b08fd53aebc754fa4860d Mon Sep 17 00:00:00 2001 From: Vatche Isahagian Date: Mon, 4 May 2026 13:30:07 -0400 Subject: [PATCH 5/5] addressing code rabbit issues --- altk_evolve/backend/filesystem.py | 4 ++-- tests/unit/test_filesystem_backend.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/altk_evolve/backend/filesystem.py b/altk_evolve/backend/filesystem.py index 28f55ac6..9f6599c4 100644 --- a/altk_evolve/backend/filesystem.py +++ b/altk_evolve/backend/filesystem.py @@ -6,7 +6,7 @@ from pathlib import Path from threading import Lock -from pydantic import Field +from pydantic import Field, ValidationError from altk_evolve.backend.base import BaseEntityBackend from altk_evolve.config.filesystem import FilesystemSettings, filesystem_settings @@ -64,7 +64,7 @@ def _load_namespace_data(self, namespace_id: str) -> FilesystemNamespace: raise NamespaceNotFoundException(f"Namespace `{namespace_id}` not found") try: return FilesystemNamespace.model_validate(json.loads(raw)) - except json.JSONDecodeError as e: + except (json.JSONDecodeError, ValidationError) as e: logger.warning("Namespace file %s is corrupt (%s); removing and treating as missing.", file_path, e) file_path.unlink(missing_ok=True) raise NamespaceNotFoundException(f"Namespace `{namespace_id}` not found") from e diff --git a/tests/unit/test_filesystem_backend.py b/tests/unit/test_filesystem_backend.py index 913264bc..44398c0b 100644 --- a/tests/unit/test_filesystem_backend.py +++ b/tests/unit/test_filesystem_backend.py @@ -18,6 +18,7 @@ def backend(tmp_path: Path) -> FilesystemEntityBackend: return FilesystemEntityBackend(config=FilesystemSettings(data_dir=str(tmp_path))) +@pytest.mark.unit def test_ensure_namespace_recovers_from_zero_byte_file(client: EvolveClient, tmp_path: Path): stale = tmp_path / "ns_stale.json" stale.write_text("") @@ -30,6 +31,7 @@ def test_ensure_namespace_recovers_from_zero_byte_file(client: EvolveClient, tmp assert stale.exists() and stale.stat().st_size > 0 +@pytest.mark.unit def test_ensure_namespace_recovers_from_corrupt_json(client: EvolveClient, tmp_path: Path): corrupt = tmp_path / "ns_corrupt.json" corrupt.write_text("{not json") @@ -40,6 +42,20 @@ def test_ensure_namespace_recovers_from_corrupt_json(client: EvolveClient, tmp_p assert ns.num_entities == 0 +@pytest.mark.unit +def test_ensure_namespace_recovers_from_schema_invalid_json(client: EvolveClient, tmp_path: Path): + """Valid JSON that doesn't match FilesystemNamespace must also trigger recovery, + not propagate pydantic.ValidationError and wedge startup.""" + bogus = tmp_path / "ns_bogus.json" + bogus.write_text('{"not_a_namespace": true}') + + ns = client.ensure_namespace("ns_bogus") + + assert ns.id == "ns_bogus" + assert ns.num_entities == 0 + + +@pytest.mark.unit def test_save_tolerates_stale_shared_tmp(backend: FilesystemEntityBackend, tmp_path: Path): """Stale .json.tmp from an interrupted write must not block subsequent saves.