Skip to content

Commit d23fe71

Browse files
committed
fix: test failure with bubble none and pytest missing
1 parent 4e7a23b commit d23fe71

4 files changed

Lines changed: 93 additions & 98 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ jobs:
116116
# new endpoint file because `pytest tests/` would also re-collect the
117117
# 178 unittest.TestCase subclasses already run in the step above —
118118
# ~2× the CI minutes for zero extra signal.
119-
run: python -m pytest tests/test_api_endpoints.py tests/test_parse_failure_logging.py -v --tb=short
119+
run: python -m pytest tests/test_api_endpoints.py -v --tb=short
120120

121121
# ── PyInstaller desktop build (Windows only, once per workflow) ────────
122122
# Closes #44. Builds the onedir bundle and smoke-tests --help so the

services/workspace_tabs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
114114
parts = row["key"].split(":")
115115
if len(parts) >= 3:
116116
bid = parts[2]
117+
if row["value"] is None:
118+
continue
117119
try:
118120
parsed = json.loads(row["value"])
119121
except json.JSONDecodeError as e:
@@ -192,6 +194,8 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
192194

193195
for row in composer_rows:
194196
composer_id = row["key"].split(":")[1]
197+
if row["value"] is None:
198+
continue
195199
try:
196200
parsed = json.loads(row["value"])
197201
except json.JSONDecodeError as e:
Lines changed: 86 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
"""pytest caplog tests for structured logging at model parse sites (issue #66)."""
1+
"""Tests for structured logging at model parse sites (issue #66)."""
22

33
from __future__ import annotations
44

55
import json
6-
import logging
76
import os
87
import sqlite3
98
import sys
109
import tempfile
10+
import unittest
1111
from contextlib import closing
1212

13-
import pytest
14-
1513
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1614
if REPO_ROOT not in sys.path:
1715
sys.path.insert(0, REPO_ROOT)
@@ -36,7 +34,6 @@ def _seed_listing_with_drifted_composer(parent: str) -> str:
3634

3735
conn = sqlite3.connect(os.path.join(global_root, "state.vscdb"))
3836
conn.execute("CREATE TABLE cursorDiskKV ([key] TEXT PRIMARY KEY, value TEXT)")
39-
# Missing createdAt — Composer.from_dict raises SchemaError.
4037
conn.execute(
4138
"INSERT INTO cursorDiskKV VALUES (?, ?)",
4239
(
@@ -85,7 +82,6 @@ def _seed_tabs_with_drifted_bubble(parent: str) -> str:
8582
}),
8683
),
8784
)
88-
# Non-dict bubble value trips Bubble.from_dict schema gate.
8985
conn.execute(
9086
"INSERT INTO cursorDiskKV VALUES (?, ?)",
9187
("bubbleId:cmp-ok:b-bad", json.dumps("not-a-dict")),
@@ -99,102 +95,97 @@ def _seed_tabs_with_drifted_bubble(parent: str) -> str:
9995
return ws_root
10096

10197

102-
@pytest.fixture
103-
def caplog_at_warning(caplog: pytest.LogCaptureFixture) -> pytest.LogCaptureFixture:
104-
caplog.set_level(logging.WARNING)
105-
return caplog
106-
107-
108-
def test_listing_logs_composer_schema_drift(caplog_at_warning: pytest.LogCaptureFixture) -> None:
109-
with tempfile.TemporaryDirectory() as tmp:
110-
ws_root = _seed_listing_with_drifted_composer(tmp)
111-
with caplog_at_warning.at_level(logging.WARNING, logger="services.workspace_listing"):
112-
list_workspace_projects(ws_root, rules=[])
98+
class TestParseFailureLogging(unittest.TestCase):
99+
def test_listing_logs_composer_schema_drift(self) -> None:
100+
with tempfile.TemporaryDirectory() as tmp:
101+
ws_root = _seed_listing_with_drifted_composer(tmp)
102+
with self.assertLogs("services.workspace_listing", level="WARNING") as cm:
103+
list_workspace_projects(ws_root, rules=[])
113104

114-
messages = [r.getMessage() for r in caplog_at_warning.records]
115-
assert any("Composer" in m and "cmp-drift" in m for m in messages), (
116-
f"expected Composer parse warning for cmp-drift, got: {messages}"
117-
)
105+
messages = [r.getMessage() for r in cm.records]
106+
self.assertTrue(
107+
any("Composer" in m and "cmp-drift" in m for m in messages),
108+
f"expected Composer parse warning for cmp-drift, got: {messages}",
109+
)
118110

111+
def test_workspace_tabs_logs_bubble_json_decode_failure(self) -> None:
112+
from flask import Flask
113+
114+
app = Flask(__name__)
115+
app.config["TESTING"] = True
116+
app.config["EXCLUSION_RULES"] = []
117+
118+
with tempfile.TemporaryDirectory() as tmp:
119+
ws_root = _seed_tabs_with_drifted_bubble(tmp)
120+
global_db = os.path.join(tmp, "globalStorage", "state.vscdb")
121+
with closing(sqlite3.connect(global_db)) as conn:
122+
conn.execute(
123+
"INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)",
124+
("bubbleId:cmp-ok:b-json", "{not valid json"),
125+
)
126+
conn.commit()
127+
with self.assertLogs("services.workspace_tabs", level="WARNING") as cm:
128+
with app.test_request_context("/api/workspaces/global/tabs"):
129+
payload, status = assemble_workspace_tabs("global", ws_root, rules=[])
130+
131+
self.assertEqual(status, 200)
132+
messages = [r.getMessage() for r in cm.records]
133+
self.assertTrue(
134+
any("decode Bubble" in m and "b-json" in m for m in messages),
135+
f"expected JSON decode warning for b-json, got: {messages}",
136+
)
119137

120-
def test_workspace_tabs_logs_bubble_json_decode_failure(
121-
caplog_at_warning: pytest.LogCaptureFixture,
122-
) -> None:
123-
from flask import Flask
138+
def test_workspace_tabs_logs_composer_json_decode_failure(self) -> None:
139+
from flask import Flask
124140

125-
app = Flask(__name__)
126-
app.config["TESTING"] = True
127-
app.config["EXCLUSION_RULES"] = []
141+
app = Flask(__name__)
142+
app.config["TESTING"] = True
143+
app.config["EXCLUSION_RULES"] = []
128144

129-
with tempfile.TemporaryDirectory() as tmp:
130-
ws_root = _seed_tabs_with_drifted_bubble(tmp)
131-
global_db = os.path.join(tmp, "globalStorage", "state.vscdb")
132-
with closing(sqlite3.connect(global_db)) as conn:
133-
conn.execute(
134-
"INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)",
135-
("bubbleId:cmp-ok:b-json", "{not valid json"),
145+
with tempfile.TemporaryDirectory() as tmp:
146+
ws_root = _seed_tabs_with_drifted_bubble(tmp)
147+
global_db = os.path.join(tmp, "globalStorage", "state.vscdb")
148+
bad_composer_value = (
149+
'{"fullConversationHeadersOnly": [{"bubbleId": "b1"}], "createdAt":'
136150
)
137-
conn.commit()
138-
with caplog_at_warning.at_level(logging.WARNING, logger="services.workspace_tabs"):
139-
with app.test_request_context("/api/workspaces/global/tabs"):
140-
payload, status = assemble_workspace_tabs("global", ws_root, rules=[])
141-
142-
assert status == 200
143-
messages = [r.getMessage() for r in caplog_at_warning.records]
144-
assert any("decode Bubble" in m and "b-json" in m for m in messages), (
145-
f"expected JSON decode warning for b-json, got: {messages}"
146-
)
147-
148-
149-
def test_workspace_tabs_logs_composer_json_decode_failure(
150-
caplog_at_warning: pytest.LogCaptureFixture,
151-
) -> None:
152-
from flask import Flask
153-
154-
app = Flask(__name__)
155-
app.config["TESTING"] = True
156-
app.config["EXCLUSION_RULES"] = []
157-
158-
with tempfile.TemporaryDirectory() as tmp:
159-
ws_root = _seed_tabs_with_drifted_bubble(tmp)
160-
global_db = os.path.join(tmp, "globalStorage", "state.vscdb")
161-
# Value must match composer_rows LIKE '%fullConversationHeadersOnly%' to reach parse.
162-
bad_composer_value = (
163-
'{"fullConversationHeadersOnly": [{"bubbleId": "b1"}], "createdAt":'
151+
with closing(sqlite3.connect(global_db)) as conn:
152+
conn.execute(
153+
"INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)",
154+
("composerData:cmp-json", bad_composer_value),
155+
)
156+
conn.commit()
157+
with self.assertLogs("services.workspace_tabs", level="WARNING") as cm:
158+
with app.test_request_context("/api/workspaces/global/tabs"):
159+
payload, status = assemble_workspace_tabs("global", ws_root, rules=[])
160+
161+
self.assertEqual(status, 200)
162+
messages = [r.getMessage() for r in cm.records]
163+
self.assertTrue(
164+
any("decode Composer" in m and "cmp-json" in m for m in messages),
165+
f"expected JSON decode warning for cmp-json, got: {messages}",
164166
)
165-
with closing(sqlite3.connect(global_db)) as conn:
166-
conn.execute(
167-
"INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)",
168-
("composerData:cmp-json", bad_composer_value),
169-
)
170-
conn.commit()
171-
with caplog_at_warning.at_level(logging.WARNING, logger="services.workspace_tabs"):
172-
with app.test_request_context("/api/workspaces/global/tabs"):
173-
payload, status = assemble_workspace_tabs("global", ws_root, rules=[])
174-
175-
assert status == 200
176-
messages = [r.getMessage() for r in caplog_at_warning.records]
177-
assert any("decode Composer" in m and "cmp-json" in m for m in messages), (
178-
f"expected JSON decode warning for cmp-json, got: {messages}"
179-
)
180167

168+
def test_workspace_tabs_logs_bubble_schema_drift(self) -> None:
169+
from flask import Flask
170+
171+
app = Flask(__name__)
172+
app.config["TESTING"] = True
173+
app.config["EXCLUSION_RULES"] = []
174+
175+
with tempfile.TemporaryDirectory() as tmp:
176+
ws_root = _seed_tabs_with_drifted_bubble(tmp)
177+
with self.assertLogs("services.workspace_tabs", level="WARNING") as cm:
178+
with app.test_request_context("/api/workspaces/global/tabs"):
179+
payload, status = assemble_workspace_tabs("global", ws_root, rules=[])
180+
181+
self.assertEqual(status, 200)
182+
self.assertIn("cmp-ok", [t["id"] for t in payload.get("tabs", [])])
183+
messages = [r.getMessage() for r in cm.records]
184+
self.assertTrue(
185+
any("Bubble" in m and "b-bad" in m for m in messages),
186+
f"expected Bubble parse warning for b-bad, got: {messages}",
187+
)
181188

182-
def test_workspace_tabs_logs_bubble_schema_drift(caplog_at_warning: pytest.LogCaptureFixture) -> None:
183-
from flask import Flask
184-
185-
app = Flask(__name__)
186-
app.config["TESTING"] = True
187-
app.config["EXCLUSION_RULES"] = []
188-
189-
with tempfile.TemporaryDirectory() as tmp:
190-
ws_root = _seed_tabs_with_drifted_bubble(tmp)
191-
with caplog_at_warning.at_level(logging.WARNING, logger="services.workspace_tabs"):
192-
with app.test_request_context("/api/workspaces/global/tabs"):
193-
payload, status = assemble_workspace_tabs("global", ws_root, rules=[])
194189

195-
assert status == 200
196-
assert "cmp-ok" in [t["id"] for t in payload.get("tabs", [])]
197-
messages = [r.getMessage() for r in caplog_at_warning.records]
198-
assert any("Bubble" in m and "b-bad" in m for m in messages), (
199-
f"expected Bubble parse warning for b-bad, got: {messages}"
200-
)
190+
if __name__ == "__main__":
191+
unittest.main()

tests/test_workspace_tabs_null_bubble.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
33
A cursorDiskKV row with a NULL value column previously caused
44
json.loads(None) -> TypeError, which propagated as a 500 response.
5-
The fix uses ``_try_loads_kv_value`` in ``services/workspace_tabs.py`` so
6-
NULL / unparseable cursorDiskKV values are skipped without raising.
5+
Bubble rows with NULL or invalid JSON values are skipped in
6+
``services/workspace_tabs.py`` without raising.
77
"""
88

99
import json

0 commit comments

Comments
 (0)