Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions services/workspace_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
parts = row["key"].split(":")
if len(parts) >= 3:
bid = parts[2]
if row["value"] is None:
Comment thread
bradjin8 marked this conversation as resolved.
Outdated
continue
try:
bubble_obj = Bubble.from_dict(json.loads(row["value"]), bubble_id=bid)
bubble_map[bid] = bubble_obj.raw
Expand Down
81 changes: 81 additions & 0 deletions tests/test_workspace_tabs_null_bubble.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Regression test for issue #50: NULL bubble value crashes GET /tabs.

A cursorDiskKV row with a NULL value column previously caused
json.loads(None) -> TypeError, which propagated as a 500 response.
The fix adds an explicit None-guard before json.loads in the bubble
loading loop of services/workspace_tabs.py.
"""

import json
import os
import sqlite3
import tempfile
import unittest


class TestNullBubbleValueDoesNotCrashTabs(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
base = self.tmp.name

# Minimal workspaceStorage layout expected by assemble_workspace_tabs.
ws_dir = os.path.join(base, "workspaceStorage")
os.makedirs(ws_dir)

# Global storage with a cursorDiskKV table containing a NULL-value bubble row.
global_dir = os.path.join(base, "globalStorage")
os.makedirs(global_dir)
db_path = os.path.join(global_dir, "state.vscdb")
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE cursorDiskKV ([key] TEXT PRIMARY KEY, value TEXT)")
conn.execute(
"INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)",
("bubbleId:composer-abc:bubble-null", None), # NULL value — the crash case
)
# Also insert a healthy bubble so we verify good rows still load.
conn.execute(
"INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)",
(
"bubbleId:composer-abc:bubble-ok",
json.dumps({"type": 1, "text": "hello", "createdAt": 0}),
),
)
conn.commit()
conn.close()

self.workspace_path = ws_dir

def tearDown(self):
self.tmp.cleanup()

def test_null_bubble_row_is_skipped_without_exception(self):
"""assemble_workspace_tabs must not raise when a bubble row has NULL value."""
from services.workspace_tabs import assemble_workspace_tabs
Comment thread
bradjin8 marked this conversation as resolved.
Outdated

try:
payload, status = assemble_workspace_tabs(
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
workspace_id="global",
workspace_path=self.workspace_path,
rules=[],
)
except TypeError as exc:
self.fail(f"NULL bubble row raised TypeError: {exc}")

self.assertEqual(status, 200)

def test_healthy_bubbles_still_load_when_null_row_present(self):
"""Healthy bubble rows in the same table are not dropped by the None-guard."""
from services.workspace_tabs import assemble_workspace_tabs
Comment thread
bradjin8 marked this conversation as resolved.
Outdated

payload, status = assemble_workspace_tabs(
workspace_id="global",
workspace_path=self.workspace_path,
rules=[],
)
self.assertEqual(status, 200)
self.assertIsInstance(payload, dict)
self.assertIn("tabs", payload)

Comment thread
coderabbitai[bot] marked this conversation as resolved.

if __name__ == "__main__":
unittest.main()
Loading