Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions api/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@
warn_workspace_json_read,
)
from utils.workspace_descriptor import read_json_file
from services.workspace_resolver import (
infer_workspace_name_from_context,
lookup_workspace_display_name,
)
from services.workspace_resolver import infer_workspace_name_from_context
from services.cli_tabs import get_cli_workspace_tabs
from services.workspace_listing import list_workspace_projects
from services.workspace_tabs import (
Expand All @@ -36,13 +33,6 @@
list_workspace_tab_summaries,
)

# Re-exported for tests/test_models_wired_at_read_sites.py — the typed-model
# spy harness patches `workspaces_mod.Bubble` / `.Composer` / `.Workspace` to
# verify that production read paths actually call from_dict. The classes
# themselves are wired inside the services modules now (post-#25 split);
# importing them here keeps the spy resolution stable.
from models import Bubble, Composer, Workspace # noqa: F401

bp = Blueprint("workspaces", __name__)
_logger = logging.getLogger(__name__)

Expand Down
35 changes: 22 additions & 13 deletions tests/test_models_wired_at_read_sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,14 @@ def test_search_endpoint_calls_bubble_from_dict(self):

def test_workspace_tabs_endpoint_calls_bubble_from_dict(self):
from app import create_app
import api.workspaces as workspaces_mod
import services.workspace_db as workspace_db_mod
from models import Bubble
app = create_app()
app.config["TESTING"] = True
app.config["EXCLUSION_RULES"] = []
with patch.object(workspaces_mod.Bubble, "from_dict", wraps=workspaces_mod.Bubble.from_dict) as spy:
with patch.object(
workspace_db_mod.Bubble, "from_dict", wraps=Bubble.from_dict
) as spy:
client = app.test_client()
response = client.get(f"/api/workspaces/{WORKSPACE_ID}/tabs")
self.assertEqual(response.status_code, 200)
Expand All @@ -136,8 +139,6 @@ def test_bubble_schema_drift_is_logged_not_swallowed_silently(self):
# ValueError and skipped silently. Schema drift must now log a
# `Schema drift in bubble <bid>` line so disappearing bubbles can be
# traced. The well-formed row still loads alongside.
import logging

from app import create_app
# Seed a deliberately-malformed bubble row that will trip
# Bubble.from_dict's "expected non-empty str" gate on the bubble_id by
Expand All @@ -163,17 +164,20 @@ def test_bubble_schema_drift_is_logged_not_swallowed_silently(self):
msg="drift log must include the offending bubble id")

def test_workspace_tabs_endpoint_calls_composer_from_dict(self):
# Brad's most-important finding: list_workspaces() at api/workspaces.py:605
# validates each composer with Composer.from_dict, but get_workspace_tabs()
# in the same file used raw json.loads. Schema drift would have been hidden
# from one of the two primary conversation-browsing paths. This test pins
# that BOTH paths must now validate via the model.
# Brad's most-important finding: list_workspaces() validates each composer
# with Composer.from_dict, but get_workspace_tabs() used raw json.loads.
# Schema drift would have been hidden from one of the two primary
# conversation-browsing paths. This test pins that BOTH paths must now
# validate via the model.
from app import create_app
import api.workspaces as workspaces_mod
import services.workspace_tabs as workspace_tabs_mod
from models import Composer
app = create_app()
app.config["TESTING"] = True
app.config["EXCLUSION_RULES"] = []
with patch.object(workspaces_mod.Composer, "from_dict", wraps=workspaces_mod.Composer.from_dict) as spy:
with patch.object(
workspace_tabs_mod.Composer, "from_dict", wraps=Composer.from_dict
) as spy:
client = app.test_client()
response = client.get(f"/api/workspaces/{WORKSPACE_ID}/tabs")
self.assertEqual(response.status_code, 200)
Expand Down Expand Up @@ -220,8 +224,13 @@ def test_composers_endpoint_calls_workspace_from_dict(self):

def test_workspace_display_name_calls_workspace_from_dict(self):
from services.workspace_resolver import lookup_workspace_display_name
import api.workspaces as workspaces_mod
with patch.object(workspaces_mod.Workspace, "from_dict", wraps=workspaces_mod.Workspace.from_dict) as spy:
import services.workspace_resolver as workspace_resolver_mod
from models import Workspace
with patch.object(
workspace_resolver_mod.Workspace,
"from_dict",
wraps=Workspace.from_dict,
) as spy:
name = lookup_workspace_display_name(self.workspace_path, WORKSPACE_ID)
self.assertIsInstance(name, str)
self.assertGreaterEqual(
Expand Down
Loading