22
33A cursorDiskKV row with a NULL value column previously caused
44json.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 .
5+ The fix uses ``_loads_disk_kv_value`` in ``services/workspace_tabs.py`` so
6+ NULL / unparseable cursorDiskKV values are skipped without raising .
77"""
88
99import json
1212import tempfile
1313import unittest
1414
15+ from services .workspace_tabs import assemble_workspace_tabs
16+
17+ # cursorDiskKV keys use typed prefixes; tabs[].id is the bare suffix only
18+ # (assemble_workspace_tabs: composer_id = row["key"].split(":")[1]).
19+ COMPOSER_ID = "composer-abc"
20+ COMPOSER_KV_KEY = f"composerData:{ COMPOSER_ID } "
21+
1522
1623class TestNullBubbleValueDoesNotCrashTabs (unittest .TestCase ):
1724 def setUp (self ):
@@ -30,21 +37,21 @@ def setUp(self):
3037 conn .execute ("CREATE TABLE cursorDiskKV ([key] TEXT PRIMARY KEY, value TEXT)" )
3138 conn .execute (
3239 "INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)" ,
33- ("bubbleId:composer-abc :bubble-null" , None ), # NULL value — the crash case
40+ (f "bubbleId:{ COMPOSER_ID } :bubble-null" , None ), # NULL value — the crash case
3441 )
3542 # Healthy bubble that should surface in the assembled tab.
3643 conn .execute (
3744 "INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)" ,
3845 (
39- "bubbleId:composer-abc :bubble-ok" ,
46+ f "bubbleId:{ COMPOSER_ID } :bubble-ok" ,
4047 json .dumps ({"type" : 1 , "text" : "hello world" , "createdAt" : 1739200000000 }),
4148 ),
4249 )
4350 # Composer referencing the healthy bubble — required for a tab to be built.
4451 conn .execute (
4552 "INSERT INTO cursorDiskKV ([key], value) VALUES (?, ?)" ,
4653 (
47- "composerData:composer-abc" ,
54+ COMPOSER_KV_KEY ,
4855 json .dumps ({
4956 "name" : "Test Chat" ,
5057 "modelConfig" : {"modelName" : "gpt-4o" },
@@ -64,8 +71,6 @@ def tearDown(self):
6471
6572 def test_null_bubble_row_is_skipped_without_exception (self ):
6673 """assemble_workspace_tabs must not raise when a bubble row has NULL value."""
67- from services .workspace_tabs import assemble_workspace_tabs
68-
6974 try :
7075 _payload , status = assemble_workspace_tabs (
7176 workspace_id = "global" ,
@@ -75,34 +80,34 @@ def test_null_bubble_row_is_skipped_without_exception(self):
7580 except TypeError as exc :
7681 self .fail (f"NULL bubble row raised TypeError: { exc } " )
7782
78- self .assertEqual (status , 200 )
83+ self .assertEqual (status , 200 , "NULL bubble row must not turn tabs load into an error response" )
7984
8085 def test_healthy_bubbles_still_load_when_null_row_present (self ):
8186 """The healthy bubble surfaces in a tab even when a NULL row is present."""
82- from services .workspace_tabs import assemble_workspace_tabs
83-
8487 payload , status = assemble_workspace_tabs (
8588 workspace_id = "global" ,
8689 workspace_path = self .workspace_path ,
8790 rules = [],
8891 )
89- self .assertEqual (status , 200 )
90- self .assertIsInstance (payload , dict )
92+ self .assertEqual (status , 200 , "tabs endpoint must succeed when only the null bubble row is bad" )
93+ self .assertIsInstance (payload , dict , "tabs response must be a JSON object envelope" )
9194 tabs = payload .get ("tabs" , [])
92- self .assertEqual (len (tabs ), 1 , "Expected exactly one tab for composer-abc " )
95+ self .assertEqual (len (tabs ), 1 , f "Expected exactly one tab for { COMPOSER_ID } " )
9396
9497 tab = tabs [0 ]
95- self .assertEqual (tab ["id" ], "composer-abc" )
96- self .assertEqual (tab ["title" ], "Test Chat" )
97- self .assertIn ("bubbles" , tab )
98- self .assertIn ("codeBlockDiffs" , tab )
98+ # GET /tabs and workspace.html ?tab= use bare composer id, not the KV key.
99+ self .assertEqual (tab ["id" ], COMPOSER_ID , "tab id must be bare composer id (KV key suffix only)" )
100+ self .assertNotEqual (tab ["id" ], COMPOSER_KV_KEY , "tab id must not include composerData: prefix" )
101+ self .assertEqual (tab ["title" ], "Test Chat" , "composer name from seeded cursorDiskKV row" )
102+ self .assertIn ("bubbles" , tab , "tab payload must include bubbles for the conversation view" )
103+ self .assertIn ("codeBlockDiffs" , tab , "tab payload must include codeBlockDiffs field (may be empty)" )
99104
100105 bubbles = tab ["bubbles" ]
101106 self .assertEqual (len (bubbles ), 1 , "Expected exactly one bubble (null row skipped)" )
102107 bubble = bubbles [0 ]
103- self .assertEqual (bubble ["type" ], "user" )
104- self .assertEqual (bubble ["text" ], "hello world" )
105- self .assertIn ("timestamp" , bubble )
108+ self .assertEqual (bubble ["type" ], "user" , "header type 1 maps to user bubble" )
109+ self .assertEqual (bubble ["text" ], "hello world" , "healthy bubble text must surface in the tab" )
110+ self .assertIn ("timestamp" , bubble , "bubble must carry a timestamp for ordering/display" )
106111
107112
108113if __name__ == "__main__" :
0 commit comments