128 lines
5.3 KiB
Python
128 lines
5.3 KiB
Python
# #region TestDemoModeIsolation [C:2] [TYPE Module]
|
|
# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository
|
|
# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.
|
|
# @LAYER Tests
|
|
# @RELATION DEPENDS_ON -> [DemoDataService]
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
from src.models.clean_release import ReleaseCandidate
|
|
from src.services.clean_release.demo_data_service import (
|
|
build_namespaced_id,
|
|
create_isolated_repository,
|
|
resolve_namespace,
|
|
)
|
|
|
|
|
|
# #region test_resolve_namespace_separates_demo_and_real [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO -> TestDemoModeIsolation
|
|
# @PURPOSE: Ensure namespace resolver returns deterministic and distinct namespaces.
|
|
# @PRE: Mode names are provided as user/runtime strings.
|
|
# @POST: Demo and real namespaces are different and stable.
|
|
def test_resolve_namespace_separates_demo_and_real() -> None:
|
|
demo = resolve_namespace("demo")
|
|
real = resolve_namespace("real")
|
|
|
|
assert demo == "clean-release:demo"
|
|
assert real == "clean-release:real"
|
|
assert demo != real
|
|
# #endregion test_resolve_namespace_separates_demo_and_real
|
|
|
|
|
|
# #region test_build_namespaced_id_prevents_cross_mode_collisions [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO -> TestDemoModeIsolation
|
|
# @PURPOSE: Ensure ID generation prevents demo/real collisions for identical logical IDs.
|
|
# @PRE: Same logical candidate id is used in two different namespaces.
|
|
# @POST: Produced physical IDs differ by namespace prefix.
|
|
def test_build_namespaced_id_prevents_cross_mode_collisions() -> None:
|
|
logical_id = "2026.03.09-rc1"
|
|
demo_id = build_namespaced_id(resolve_namespace("demo"), logical_id)
|
|
real_id = build_namespaced_id(resolve_namespace("real"), logical_id)
|
|
|
|
assert demo_id != real_id
|
|
assert demo_id.startswith("clean-release:demo::")
|
|
assert real_id.startswith("clean-release:real::")
|
|
# #endregion test_build_namespaced_id_prevents_cross_mode_collisions
|
|
|
|
|
|
# #region test_create_isolated_repository_keeps_mode_data_separate [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO -> TestDemoModeIsolation
|
|
# @PURPOSE: Verify demo and real repositories do not leak state across mode boundaries.
|
|
# @PRE: Two repositories are created for distinct modes.
|
|
# @POST: Candidate mutations in one mode are not visible in the other mode.
|
|
def test_create_isolated_repository_keeps_mode_data_separate() -> None:
|
|
demo_repo = create_isolated_repository("demo")
|
|
real_repo = create_isolated_repository("real")
|
|
|
|
demo_candidate_id = build_namespaced_id(resolve_namespace("demo"), "candidate-1")
|
|
real_candidate_id = build_namespaced_id(resolve_namespace("real"), "candidate-1")
|
|
|
|
demo_repo.save_candidate(
|
|
ReleaseCandidate(
|
|
id=demo_candidate_id,
|
|
version="1.0.0",
|
|
source_snapshot_ref="git:sha-demo",
|
|
created_by="demo-operator",
|
|
created_at=datetime.now(UTC),
|
|
status="DRAFT",
|
|
)
|
|
)
|
|
real_repo.save_candidate(
|
|
ReleaseCandidate(
|
|
id=real_candidate_id,
|
|
version="1.0.0",
|
|
source_snapshot_ref="git:sha-real",
|
|
created_by="real-operator",
|
|
created_at=datetime.now(UTC),
|
|
status="DRAFT",
|
|
)
|
|
)
|
|
|
|
assert demo_repo.get_candidate(demo_candidate_id) is not None
|
|
assert demo_repo.get_candidate(real_candidate_id) is None
|
|
assert real_repo.get_candidate(real_candidate_id) is not None
|
|
assert real_repo.get_candidate(demo_candidate_id) is None
|
|
# #endregion test_create_isolated_repository_keeps_mode_data_separate
|
|
|
|
|
|
# #region test_build_namespaced_id_rejects_empty_namespace [C:2] [TYPE Function]
|
|
def test_build_namespaced_id_rejects_empty_namespace() -> None:
|
|
"""build_namespaced_id with empty namespace raises ValueError."""
|
|
from src.services.clean_release.demo_data_service import build_namespaced_id
|
|
import pytest
|
|
|
|
with pytest.raises(ValueError, match="namespace must be non-empty"):
|
|
build_namespaced_id("", "logical-1")
|
|
with pytest.raises(ValueError, match="namespace must be non-empty"):
|
|
build_namespaced_id(" ", "logical-1")
|
|
# #endregion test_build_namespaced_id_rejects_empty_namespace
|
|
|
|
|
|
# #region test_build_namespaced_id_rejects_empty_logical_id [C:2] [TYPE Function]
|
|
def test_build_namespaced_id_rejects_empty_logical_id() -> None:
|
|
"""build_namespaced_id with empty logical_id raises ValueError."""
|
|
from src.services.clean_release.demo_data_service import build_namespaced_id
|
|
import pytest
|
|
|
|
with pytest.raises(ValueError, match="logical_id must be non-empty"):
|
|
build_namespaced_id("demo", "")
|
|
with pytest.raises(ValueError, match="logical_id must be non-empty"):
|
|
build_namespaced_id("demo", " ")
|
|
# #endregion test_build_namespaced_id_rejects_empty_logical_id
|
|
|
|
|
|
# #region test_resolve_namespace_defaults_to_real [C:2] [TYPE Function]
|
|
def test_resolve_namespace_defaults_to_real() -> None:
|
|
"""Non-demo mode defaults to real namespace."""
|
|
from src.services.clean_release.demo_data_service import resolve_namespace
|
|
|
|
assert resolve_namespace("") == "clean-release:real"
|
|
assert resolve_namespace(None) == "clean-release:real"
|
|
assert resolve_namespace("production") == "clean-release:real"
|
|
# #endregion test_resolve_namespace_defaults_to_real
|
|
|
|
|
|
# #endregion TestDemoModeIsolation
|