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
33from __future__ import annotations
44
55import json
6- import logging
76import os
87import sqlite3
98import sys
109import tempfile
10+ import unittest
1111from contextlib import closing
1212
13- import pytest
14-
1513REPO_ROOT = os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
1614if 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 ()
0 commit comments