"""M10 §8, §19 and §20: the storyteller does not know the media layer is there. Three claims, and the first is the milestone's central acceptance condition: * **§20 — ordinary play is unchanged** with no media configuration of any kind. Not "works with a warning", not "works once you dismiss something": unchanged. * **§19 — nothing is contacted**, nothing is required at startup, and no provider setting exists to be got wrong. * **§8 — the hidden-information boundary.** A future provider must not receive narrator-only material merely because the storyteller knows it. The §8 tests use a **hidden M7 knowledge source**, which is this product's real narrator-only mechanism, rather than an invented marker — so what is tested is the boundary that exists. Each carries a **positive control**: the sentinel is shown to reach the narrator's own prompt in the same campaign, so a passing test cannot be one where the secret was never established. python -m pytest tests/test_m10_no_media.py -v """ import pytest from fastapi import Depends from fastapi.testclient import TestClient from app import auth, limits, memorybank, models from app.database import Base, SessionLocal, engine, get_db from app.knowledge import embeddings from app.main import app from app.media import providers from app.routers import adventures import m10_fixture from fakes import ScriptedProvider class StubDerived: async def complete(self, system, prompt, **kwargs): return "A memory of the meeting." async def embed(self, texts): out = [] for text in texts: lowered = text.lower() out.append([ 1.0, 1.0 if "observer" in lowered or "panelling" in lowered else 0.0, 1.0 if "office" in lowered or "meeting" in lowered else 0.0, ]) return out @pytest.fixture() def client(monkeypatch): Base.metadata.create_all(bind=engine) memorybank._vector_cache.clear() embeddings._cache.clear() setup = SessionLocal() user = models.User(is_guest=False, email="m10nm@example.com") setup.add(user) setup.flush() setup.add(models.Settings( user_id=user.id, model="test-model", embedding_model="", context_token_budget=4000, max_output_tokens=400, memory_top_k=3, )) adventure = models.Adventure(user_id=user.id, title="No media") setup.add(adventure) setup.flush() setup.add(models.Action( adventure_id=adventure.id, type="start", text="Bill badges in on a Tuesday morning.", )) setup.commit() adv_id, user_id = adventure.id, user.id setup.close() monkeypatch.setattr(limits, "check_row_cap", lambda *a, **k: None) monkeypatch.setattr(adventures.turns, "OpenAICompatibleProvider", ScriptedProvider) monkeypatch.setattr(memorybank, "embedding_provider", lambda s: StubDerived()) monkeypatch.setattr(memorybank, "summary_provider", lambda s: StubDerived()) app.dependency_overrides[auth.get_current_user] = ( lambda db=Depends(get_db): db.get(models.User, user_id) ) test_client = TestClient(app) test_client.adv_id = adv_id try: yield test_client finally: app.dependency_overrides.clear() adventures.turns._active_turns.clear() memorybank._vector_cache.clear() embeddings._cache.clear() Base.metadata.drop_all(bind=engine) # ---------------------------------------------------- §20: unchanged play def test_a_whole_campaign_plays_with_no_media_configuration(client): """§20's list, in one campaign, with no media anything. Turns, state extraction, memory and summary activity, knowledge retrieval, Undo, Redo, Retry, a Save Point restore, and a fresh read of what was written — all of it while no provider is registered, no media endpoint is configured, and no media table holds a row. The genuine process restarts live in `test_m10_lineage.py`. """ adv = client.adv_id assert providers.registered() == {} m10_fixture.upload_handbook(client, adv) m10_fixture.build(client, adv) for i in range(3): m10_fixture.play(client, adv, f"discuss item {i}", []) point = client.post(f"/api/adventures/{adv}/checkpoints", json={"name": "Mid-meeting", "note": ""}) assert point.status_code == 201, point.text[:300] m10_fixture.play(client, adv, "the meeting runs long", []) assert client.post(f"/api/adventures/{adv}/undo").status_code == 200 assert client.post(f"/api/adventures/{adv}/redo").status_code == 200 retried = client.post(f"/api/adventures/{adv}/retry") assert retried.status_code == 200, retried.text[:300] restored = client.post( f"/api/adventures/{adv}/checkpoints/{point.json()['id']}/restore") assert restored.status_code == 200, restored.text[:300] import asyncio asyncio.run(memorybank.run_post_turn(adv)) # Retrieval still works, and the state is intact. report = client.get(f"/api/adventures/{adv}/context").json() assert report["prompt"]["system"] assert client.get(f"/api/adventures/{adv}/state").json()["document"]["entities"] # Read back through a fresh session — the state is on disk, not in the # request that wrote it. This is *not* a process restart: the genuine # spawned-process restarts are in `test_m10_lineage.py`, which runs them # with profiles written and packets built. with SessionLocal() as db: assert db.get(models.Adventure, adv).narrative_state["scene"]["summary"] def test_no_media_row_exists_after_ordinary_play(client): """Media readiness is inert until something uses it.""" m10_fixture.build(client, client.adv_id) for i in range(3): m10_fixture.play(client, client.adv_id, f"turn {i}", []) with SessionLocal() as db: # The fixture writes two profiles deliberately; ordinary *play* writes # none, which is the claim. Counting after a campaign built without the # fixture's profile step would be the same assertion said less clearly. played_only = models.Adventure(user_id=None, title="untouched") db.add(played_only) db.flush() assert db.query(models.VisualProfile).filter( models.VisualProfile.adventure_id == played_only.id).count() == 0 def test_the_prompt_is_unchanged_by_media_readiness(client): """M10 touches no prompt path, and the assembled prompt shows it. The context builder is the one place a new subsystem would leak into every turn. No section M10 could have added appears, and the packet's own vocabulary is absent. """ m10_fixture.build(client, client.adv_id) report = client.get(f"/api/adventures/{client.adv_id}/context").json() labels = {section["label"] for section in report["sections"]} for absent in ("scene_packet", "visual_profile", "visual_profiles", "media"): assert absent not in labels blob = report["prompt"]["system"] + report["prompt"]["story"] assert "visual_profile" not in blob assert "scene_id" not in blob def test_the_turn_path_does_not_import_the_media_package(client): """Structural: a turn cannot reach the media layer even by accident. Checked on the modules' import statements rather than on their text, so the test says "does not import the media package" and not "does not contain the letters m-e-d-i-a" — which `immediately` would fail. """ import ast import pathlib root = pathlib.Path(__file__).resolve().parent.parent / "app" for name in ("routers/adventures/turns.py", "context/builder.py", "narrative/apply.py", "narrative/store.py", "tree.py", "head.py", "memorybank.py"): for node in ast.walk(ast.parse((root / name).read_text())): if isinstance(node, ast.Import): names = [a.name for a in node.names] elif isinstance(node, ast.ImportFrom): names = [node.module or ""] + [a.name for a in node.names] else: continue assert not any( n == "media" or n.endswith(".media") or n.startswith("media.") for n in names ), f"{name} imports the media package" # ----------------------------------------------------- §19: nothing outbound def test_no_media_provider_is_required_at_startup(client): """The application imports, serves and plays with an empty registry.""" assert providers.registered() == {} assert client.get("/api/health").json() == {"ok": True} m10_fixture.play(client, client.adv_id, "play a turn", []) def test_no_media_setting_exists_to_be_misconfigured(client): """§11's last clause: if no provider configuration is needed, none exists. M10 invents no media endpoint setting, so there is nothing to point at a cloud by mistake. The endpoint *policy* exists and is tested; a stored endpoint does not. """ settings = client.get("/api/settings").json() assert not any( "media" in key or "image" in key or "video" in key or "tts" in key or "stt" in key for key in settings ), settings.keys() assert not any( "media" in column.name for column in models.Settings.__table__.columns ) def test_the_media_package_opens_no_socket(client): """§19: no new required outbound connection, checked by import. `test_egress.py` owns the general no-outbound guarantee; this is the narrow M10 claim that the new package could not participate in one. """ import pathlib seam = pathlib.Path(__file__).resolve().parent.parent / "app" / "media" for path in seam.rglob("*.py"): body = path.read_text() for forbidden in ("httpx", "requests.", "urlopen", "socket.socket", "aiohttp", "subprocess"): assert forbidden not in body, f"{path.name} references {forbidden}" def test_a_media_endpoint_cannot_be_pointed_at_a_cloud(client): """The policy, applied where a future coordinator would apply it.""" for url in ("https://api.openai.com/v1", "http://8.8.8.8:8188", "https://replicate.com", "http://example.com"): assert providers.endpoint_rejection_reason(url) is not None # ------------------------------------------- §8: the hidden-information line def test_a_narrator_only_secret_does_not_reach_the_scene_packet(client): """§8, with a positive control. The sentinel lives in a **hidden** imported source, which is the product's narrator-only mechanism. The control proves it genuinely reaches the narrator's prompt in this very campaign — so the packet's silence is a boundary rather than an accident of the source never being retrieved. """ adv = client.adv_id m10_fixture.upload_secret(client, adv) m10_fixture.build(client, adv) m10_fixture.play(client, adv, "look at the north wall panelling of the office", []) report = client.get(f"/api/adventures/{adv}/context").json() narrator_prompt = report["prompt"]["system"] + report["prompt"]["story"] assert m10_fixture.SECRET_SENTINEL in narrator_prompt, ( "the control failed: the narrator was never told the secret, so the " "packet's not containing it proves nothing" ) packet = client.get(f"/api/adventures/{adv}/scene-packet").json() assert m10_fixture.SECRET_SENTINEL not in repr(packet) assert "concealed observer" not in repr(packet).lower() def test_the_packet_carries_no_imported_source_even_when_visible(client): """The boundary is drawn by class, not by filtering secrets one at a time. A *visible* reference source is excluded too, which is what makes the rule hold for a secret nobody thought to mark: the packet never reads imported knowledge at all, so there is no filter to forget to apply. """ adv = client.adv_id m10_fixture.upload_handbook(client, adv) m10_fixture.build(client, adv) m10_fixture.play(client, adv, "ask about the north wall panelling", []) report = client.get(f"/api/adventures/{adv}/context").json() assert any("handbook" in r["filename"] for r in report["knowledge"]["used"]), ( "the control failed: the handbook never reached the narrator" ) assert "refurbished" not in repr( client.get(f"/api/adventures/{adv}/scene-packet").json()) def test_a_secret_the_story_accepted_does_reach_the_packet(client): """The other side of the line, and the reason the rule is the right one. Once the *story* establishes something through a validated event, it is no longer narrator-only knowledge — it is something that happened, at a position, in the accepted state. A picture of that scene should show it, and a packet that hid it would be hiding the story from itself. """ adv = client.adv_id m10_fixture.upload_secret(client, adv) m10_fixture.build(client, adv) m10_fixture.play(client, adv, "the panel swings open", [ m10_fixture.entity("observer", "character", "The observer"), {"type": "set_scene", "summary": "The panel swings open and the observer steps out.", "location": "office", "present": ["bill", "alice", "roger", "observer"]}, ]) packet = client.get(f"/api/adventures/{adv}/scene-packet").json() assert "The observer" in [c["name"] for c in packet["characters"]] # And still not the sentinel, which the story never said aloud. assert m10_fixture.SECRET_SENTINEL not in repr(packet) def test_memories_and_summaries_stay_out_of_the_packet(client): """§7's bound: derived narrative text about the past is not depiction input.""" import asyncio adv = client.adv_id m10_fixture.build(client, adv) for i in range(8): m10_fixture.play(client, adv, f"talk {i}", [], prose=f"Roger recounts the printer incident again {i}.") asyncio.run(memorybank.run_post_turn(adv)) packet = client.get(f"/api/adventures/{adv}/scene-packet").json() assert "printer" not in repr(packet) assert "memor" not in repr(packet).lower()