gabriel / musehub public
test_canary.py python
159 lines 6.8 KB
Raw
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago
1 """Canary tests — run these first to rule out infra before debugging test failures.
2
3 Layer 1: DB reachable
4 Layer 2: All ORM tables exist in the test schema
5 Layer 3: TRUNCATE isolation works (no state bleeds between tests)
6 Layer 4: Session factory is wired to the test DB (not prod)
7 Layer 5: Unique constraints fire correctly (proving TRUNCATE reset identity)
8 """
9 from __future__ import annotations
10
11 import pytest
12 import pytest_asyncio
13 from sqlalchemy import text
14 from sqlalchemy.ext.asyncio import AsyncSession
15
16 from musehub.db.database import Base
17
18
19 def test_hello() -> None:
20 print("hello world")
21
22
23 class TestDBReachable:
24 """Layer 1 — can we talk to Postgres at all?"""
25
26 async def test_connection(self, db_session: AsyncSession) -> None:
27 result = await db_session.execute(text("SELECT 1"))
28 assert result.scalar() == 1
29
30 async def test_postgres_version(self, db_session: AsyncSession) -> None:
31 result = await db_session.execute(text("SELECT version()"))
32 version = result.scalar()
33 assert version is not None
34 assert "PostgreSQL" in version
35
36
37 class TestSchemaComplete:
38 """Layer 2 — every ORM model has a matching table in the test DB."""
39
40 async def test_all_orm_tables_exist(self, db_session: AsyncSession) -> None:
41 result = await db_session.execute(
42 text(
43 "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
44 )
45 )
46 live_tables = {row[0] for row in result.fetchall()}
47 orm_tables = {t.name for t in Base.metadata.tables.values()}
48 missing = orm_tables - live_tables
49 assert not missing, (
50 f"ORM models have no matching DB table — run create_all or add a migration:\n"
51 f"{'\n'.join(f' {t}' for t in sorted(missing))}"
52 )
53
54 async def test_no_orphan_db_tables(self, db_session: AsyncSession) -> None:
55 """Tables in DB but not in ORM — usually a dropped model without a migration."""
56 result = await db_session.execute(
57 text(
58 "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
59 )
60 )
61 live_tables = {row[0] for row in result.fetchall()}
62 orm_tables = {t.name for t in Base.metadata.tables.values()}
63 # alembic_version is managed by Alembic, not our ORM — exclude it.
64 orphans = live_tables - orm_tables - {"alembic_version"}
65 assert not orphans, (
66 f"DB has tables with no ORM model — dead schema, needs a DROP migration:\n"
67 f"{'\n'.join(f' {t}' for t in sorted(orphans))}"
68 )
69
70
71 class TestTruncateIsolation:
72 """Layer 3 — each test starts with a clean slate."""
73
74 async def test_insert_is_visible_within_test(self, db_session: AsyncSession) -> None:
75 from datetime import datetime, timezone
76 from musehub.core.genesis import compute_identity_id, compute_repo_id
77 from musehub.db.musehub_repo_models import MusehubRepo
78 _created_at = datetime.now(tz=timezone.utc)
79 _owner_id = compute_identity_id(b"canary")
80 r = MusehubRepo(
81 repo_id=compute_repo_id(_owner_id, "canary", "code", _created_at.isoformat()),
82 name="canary", owner="canary",
83 slug="canary", visibility="public", owner_user_id=_owner_id,
84 created_at=_created_at, updated_at=_created_at,
85 )
86 db_session.add(r)
87 await db_session.flush()
88 result = await db_session.execute(
89 text("SELECT COUNT(*) FROM musehub_repos WHERE owner = 'canary'")
90 )
91 assert result.scalar() == 1
92
93 async def test_previous_test_data_is_gone(self, db_session: AsyncSession) -> None:
94 """This runs after test_insert_is_visible_within_test — canary row must be gone."""
95 result = await db_session.execute(
96 text("SELECT COUNT(*) FROM musehub_repos WHERE owner = 'canary'")
97 )
98 assert result.scalar() == 0, (
99 "Canary row from previous test is still present — TRUNCATE isolation broken"
100 )
101
102
103 class TestTruncateCoverage:
104 """Layer 3b — TRUNCATE SQL covers every table in Base.metadata.
105
106 This catches the recurring "model registered after TRUNCATE SQL was
107 pre-computed" bug: conftest builds _TRUNCATE_SQL at import time from
108 Base.metadata.sorted_tables. If a DB model module is imported for the
109 first time inside a test (after that point), its table is NOT in the
110 TRUNCATE — so data from that test leaks into later tests.
111
112 Fix: ensure every model module is imported in conftest BEFORE
113 _TRUNCATE_SQL is computed.
114 """
115
116 def test_truncate_sql_covers_all_orm_tables(self) -> None:
117 import tests.conftest as cf
118 from musehub.db.database import Base
119
120 orm_tables = {t.name for t in Base.metadata.tables.values()}
121 # Parse the table names out of the pre-computed TRUNCATE statement.
122 # Format: "TRUNCATE table1, table2, ... RESTART IDENTITY CASCADE"
123 truncate_body = cf._TRUNCATE_SQL.split("TRUNCATE ", 1)[1]
124 truncate_body = truncate_body.split(" RESTART ")[0]
125 truncated = {t.strip() for t in truncate_body.split(",")}
126 missing = orm_tables - truncated
127 assert not missing, (
128 "These ORM tables are NOT in conftest._TRUNCATE_SQL — data from "
129 "tests that use them will leak into later tests.\n"
130 "Fix: import the model module in conftest.py BEFORE _TRUNCATE_SQL "
131 f"is computed (i.e. before line ~168):\n"
132 f"{'\n'.join(f' {t}' for t in sorted(missing))}"
133 )
134
135
136 class TestSessionWiring:
137 """Layer 4 — db_session fixture is pointing at the test DB, not prod."""
138
139 async def test_connected_to_test_database(self, db_session: AsyncSession) -> None:
140 result = await db_session.execute(text("SELECT current_database()"))
141 db_name = result.scalar()
142 assert "test" in db_name, (
143 f"Tests are running against '{db_name}', not the test DB — "
144 "check TEST_DATABASE_URL and the db_session fixture"
145 )
146
147 async def test_asyncsessionlocal_uses_test_engine(self, db_session: AsyncSession) -> None:
148 """AsyncSessionLocal() (used by executors) must point at the test DB."""
149 from musehub.db import database
150 result = await db_session.execute(text("SELECT current_database()"))
151 test_db = result.scalar()
152 # The conftest swaps database._async_session_factory; verify it's active.
153 async with database._async_session_factory() as s:
154 r2 = await s.execute(text("SELECT current_database()"))
155 executor_db = r2.scalar()
156 assert executor_db == test_db, (
157 f"database._async_session_factory points at '{executor_db}' "
158 f"but db_session is on '{test_db}' — fixture swap broken"
159 )
File History 14 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 8 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454 chore: bump version to 0.2.0.dev2 — nightly.2, matching muse Sonnet 4.6 patch 11 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 14 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 29 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 33 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 35 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 35 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago