mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
205e7818f8
* fix: replace empty except blocks with diagnostic logging
Add log.debug/warning to 7 bare except-pass blocks that silenced
failures in security-relevant or operationally-important paths:
- Channel route lookup, CLI policy evaluation, OIDC JWKS fetch,
prompt policy loading, plan file write, routing override, username
resolution.
Plan write now reports failure to user instead of falsely claiming
"Plan saved."
* fix: replace assert-with-side-effect and narrow BaseException catch
- Convert 4 assert isinstance() to explicit TypeError raises — assertions
are stripped under python -O, removing runtime type checks
- Narrow except BaseException to except Exception in fallback handler —
KeyboardInterrupt/SystemExit should not record as health failures
- Plan write failure now reports error to user instead of "Plan saved"
* fix: wire up toast error type and remove useless conditional
- showToast() now accepts optional type param ("error") with red border
styling — 3 call sites were passing "error" that was silently ignored
- Remove always-true if (q) guard after early-return on empty query
* fix: remove unreachable return None after return self._judge
* fix: parenthesize multi-line string concatenations in dev_parts list
Explicit parens make intentional concatenation unambiguous to static
analysis (CodeQL implicit-string-concatenation-in-list rule).
* fix: remove constant-true filter in test mock — return list directly
* fix: extract side-effecting calls from assert in tests
store.delete() and mgr.close() have side effects that would be
stripped under python -O. Assign to variable first, then assert.
* fix: remove unused local variables in tests
Drop assignments to unused workstream/variable references created
solely for side effects. Use _ for unused tuple unpacking.
* fix: use admin.prompt_policies permission for prompt policy endpoints
All 5 prompt-policy endpoints (list, create, get, update, delete)
were checking admin.policies (the tool-policy permission) instead of
admin.prompt_policies. This caused a mismatch with the admin UI which
gates the tab on admin.prompt_policies — users could see the tab but
get 403, or reach the endpoint but never see the tab.
* fix: use caplog instead of capsys for structlog warning assertion
structlog output goes through the logging system, not stdout/stderr.
* fix: address review — remove dead isinstance, module-level import, unnecessary lambdas
- session.py: remove unreachable isinstance check (has_batch already
validates raw_edits is a list)
- cli.py: move logging import to module level
- test_workstream.py: replace lambda wid: FakeUI(wid) with FakeUI
129 lines
4.5 KiB
Python
129 lines
4.5 KiB
Python
"""Tests for the storage backend registry."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
import sqlalchemy as sa
|
|
|
|
from turnstone.core.storage import (
|
|
StorageUnavailableError,
|
|
get_storage,
|
|
init_storage,
|
|
reset_storage,
|
|
)
|
|
from turnstone.core.storage._postgresql import PostgreSQLBackend
|
|
from turnstone.core.storage._sqlite import SQLiteBackend
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_registry():
|
|
"""Reset the storage registry before and after each test."""
|
|
reset_storage()
|
|
yield
|
|
reset_storage()
|
|
|
|
|
|
class TestInitStorage:
|
|
def test_sqlite_default(self, tmp_path):
|
|
backend = init_storage("sqlite", path=str(tmp_path / "test.db"), run_migrations=False)
|
|
assert isinstance(backend, SQLiteBackend)
|
|
|
|
def test_unknown_backend_raises(self):
|
|
with pytest.raises(ValueError, match="Unknown storage backend"):
|
|
init_storage("mongodb")
|
|
|
|
def test_postgresql_requires_url(self):
|
|
with pytest.raises(ValueError, match="requires a connection URL"):
|
|
init_storage("postgresql")
|
|
|
|
|
|
class TestGetStorage:
|
|
def test_auto_init(self, tmp_path, monkeypatch):
|
|
"""get_storage() auto-initializes with SQLite if not yet initialized."""
|
|
monkeypatch.chdir(tmp_path)
|
|
storage = get_storage()
|
|
assert isinstance(storage, SQLiteBackend)
|
|
|
|
def test_returns_same_instance(self, tmp_path):
|
|
init_storage("sqlite", path=str(tmp_path / "test.db"), run_migrations=False)
|
|
s1 = get_storage()
|
|
s2 = get_storage()
|
|
assert s1 is s2
|
|
|
|
|
|
class TestResetStorage:
|
|
def test_reset_clears_singleton(self, tmp_path):
|
|
init_storage("sqlite", path=str(tmp_path / "test.db"), run_migrations=False)
|
|
s1 = get_storage()
|
|
reset_storage()
|
|
# After reset, get_storage() auto-inits a new instance
|
|
init_storage("sqlite", path=str(tmp_path / "test2.db"), run_migrations=False)
|
|
s2 = get_storage()
|
|
assert s1 is not s2
|
|
|
|
|
|
class TestConnUnavailableLogging:
|
|
"""Test that _conn() deduplicates DB unavailable/restored logging."""
|
|
|
|
def _make_backend(self, tmp_path):
|
|
"""Create a minimal SQLite backend for testing _conn()."""
|
|
from turnstone.core.storage._sqlite import SQLiteBackend
|
|
|
|
return SQLiteBackend(str(tmp_path / "test.db"), create_tables=True)
|
|
|
|
def test_logs_unavailable_once(self, tmp_path, caplog: pytest.LogCaptureFixture) -> None:
|
|
backend = self._make_backend(tmp_path)
|
|
with patch.object(backend, "_engine") as mock_engine:
|
|
mock_engine.connect.side_effect = sa.exc.OperationalError(
|
|
"conn", {}, Exception("refused")
|
|
)
|
|
for _ in range(3):
|
|
with pytest.raises(StorageUnavailableError), backend._conn():
|
|
pass # pragma: no cover
|
|
unavailable_msgs = [r for r in caplog.records if "database.unavailable" in r.message]
|
|
assert len(unavailable_msgs) == 1
|
|
|
|
def test_logs_restored_on_recovery(self, tmp_path, caplog: pytest.LogCaptureFixture) -> None:
|
|
import logging
|
|
|
|
caplog.set_level(logging.INFO)
|
|
backend = self._make_backend(tmp_path)
|
|
# Simulate outage
|
|
with patch.object(backend, "_engine") as mock_engine:
|
|
mock_engine.connect.side_effect = sa.exc.OperationalError(
|
|
"conn", {}, Exception("refused")
|
|
)
|
|
with pytest.raises(StorageUnavailableError), backend._conn():
|
|
pass # pragma: no cover
|
|
assert backend._db_unavailable is True
|
|
# Real connection — should log restored
|
|
caplog.clear()
|
|
with backend._conn():
|
|
pass
|
|
restored_msgs = [r for r in caplog.records if "database.connection_restored" in r.message]
|
|
assert len(restored_msgs) == 1
|
|
assert backend._db_unavailable is False
|
|
|
|
def test_postgresql_conn_raises_storage_unavailable(self) -> None:
|
|
import threading
|
|
|
|
backend = PostgreSQLBackend.__new__(PostgreSQLBackend)
|
|
backend._db_unavailable = False
|
|
backend._db_unavailable_lock = threading.Lock()
|
|
|
|
def _raise_op_error():
|
|
raise sa.exc.OperationalError("conn", {}, Exception("refused"))
|
|
|
|
mock_engine = type(
|
|
"E",
|
|
(),
|
|
{
|
|
"connect": staticmethod(_raise_op_error),
|
|
"url": sa.engine.make_url("postgresql://user:pass@localhost/db"),
|
|
},
|
|
)()
|
|
backend._engine = mock_engine
|
|
with pytest.raises(StorageUnavailableError), backend._conn():
|
|
pass # pragma: no cover
|
|
assert backend._db_unavailable is True
|