Skip to content

Commit b7ffbf9

Browse files
committed
fix: added None check to row["value"]
1 parent 91a8d6e commit b7ffbf9

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

services/workspace_tabs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
9797
parts = row["key"].split(":")
9898
if len(parts) >= 3:
9999
bid = parts[2]
100+
if row["value"] is None:
101+
continue
100102
try:
101103
bubble_obj = Bubble.from_dict(json.loads(row["value"]), bubble_id=bid)
102104
bubble_map[bid] = bubble_obj.raw
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Regression test for issue #50: NULL bubble value crashes GET /tabs.
2+
3+
A cursorDiskKV row with a NULL value column previously caused
4+
json.loads(None) → TypeError, which propagated as a 500 response.
5+
The fix adds an explicit None-guard before json.loads in the bubble
6+
loading loop of services/workspace_tabs.py.
7+
"""
8+
9+
import json
10+
import os
11+
import sqlite3
12+
import tempfile
13+
import unittest
14+
15+
16+
class TestNullBubbleValueDoesNotCrashTabs(unittest.TestCase):
17+
def setUp(self):
18+
self.tmp = tempfile.TemporaryDirectory()
19+
base = self.tmp.name
20+
21+
# Minimal workspaceStorage layout expected by assemble_workspace_tabs.
22+
ws_dir = os.path.join(base, "workspaceStorage")
23+
os.makedirs(ws_dir)
24+
25+
# Global storage with a cursorDiskKV table containing a NULL-value bubble row.
26+
global_dir = os.path.join(base, "globalStorage")
27+
os.makedirs(global_dir)
28+
db_path = os.path.join(global_dir, "state.vscdb")
29+
conn = sqlite3.connect(db_path)
30+
conn.execute("CREATE TABLE cursorDiskKV ([key] TEXT PRIMARY KEY, value TEXT)")
31+
conn.execute(
32+
"INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)",
33+
("bubbleId:composer-abc:bubble-null", None), # NULL value — the crash case
34+
)
35+
# Also insert a healthy bubble so we verify good rows still load.
36+
conn.execute(
37+
"INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)",
38+
(
39+
"bubbleId:composer-abc:bubble-ok",
40+
json.dumps({"type": 1, "text": "hello", "createdAt": 0}),
41+
),
42+
)
43+
conn.commit()
44+
conn.close()
45+
46+
self.workspace_path = ws_dir
47+
48+
def tearDown(self):
49+
self.tmp.cleanup()
50+
51+
def test_null_bubble_row_is_skipped_without_exception(self):
52+
"""assemble_workspace_tabs must not raise when a bubble row has NULL value."""
53+
from services.workspace_tabs import assemble_workspace_tabs
54+
55+
# Should complete without TypeError (or any exception).
56+
try:
57+
payload, status = assemble_workspace_tabs(
58+
workspace_id="global",
59+
workspace_path=self.workspace_path,
60+
rules=[],
61+
)
62+
except TypeError as exc:
63+
self.fail(f"NULL bubble row raised TypeError: {exc}")
64+
65+
# The endpoint must return 200 (or 404 only if global storage is absent —
66+
# our setup provides it, so 200 is expected).
67+
self.assertEqual(status, 200)
68+
69+
def test_healthy_bubbles_still_load_when_null_row_present(self):
70+
"""Healthy bubble rows in the same table are not dropped by the None-guard."""
71+
from services.workspace_tabs import assemble_workspace_tabs
72+
73+
payload, status = assemble_workspace_tabs(
74+
workspace_id="global",
75+
workspace_path=self.workspace_path,
76+
rules=[],
77+
)
78+
self.assertEqual(status, 200)
79+
# The response payload must be a dict (no 500 error shape).
80+
self.assertIsInstance(payload, dict)
81+
self.assertIn("tabs", payload)
82+
83+
84+
if __name__ == "__main__":
85+
unittest.main()

0 commit comments

Comments
 (0)