Skip to content

Commit 709f838

Browse files
committed
fix: remaining issues
1 parent a6d3545 commit 709f838

3 files changed

Lines changed: 54 additions & 5 deletions

File tree

models/conversation.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,18 @@ def model_info(self) -> dict[str, Any]:
288288
return value
289289

290290
@property
291-
def thinking(self) -> Any | None:
292-
if "thinking" not in self.raw:
291+
def thinking(self) -> str | dict[str, Any] | None:
292+
value = self.raw.get("thinking")
293+
if value is None:
293294
return None
294-
return self.raw.get("thinking")
295+
if isinstance(value, (str, dict)):
296+
return value
297+
_logger.warning(
298+
"Schema drift in Bubble %s: invalid type for thinking (expected str or dict, got %s)",
299+
self.bubble_id,
300+
type(value).__name__,
301+
)
302+
return None
295303

296304
@property
297305
def thinking_duration_ms(self) -> int | float | None:

services/workspace_resolver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ def infer_invalid_workspace_aliases(
376376
project_name_map: dict,
377377
workspace_path_map: dict,
378378
workspace_entries: list,
379-
bubble_map: dict,
379+
bubble_map: Mapping[str, Bubble | dict[str, Any]],
380380
composer_id_to_ws: dict,
381381
invalid_workspace_ids: set[str],
382382
) -> dict[str, str]:
@@ -392,7 +392,7 @@ def infer_invalid_workspace_aliases(
392392
project_name_map: Basename map for path resolution.
393393
workspace_path_map: Normalized path map for path resolution.
394394
workspace_entries: Workspace folder entries from storage scan.
395-
bubble_map: Bubble KV map for path resolution.
395+
bubble_map: ``{bubble_id: Bubble | bubble_dict}`` for path resolution.
396396
composer_id_to_ws: Composer-to-workspace map (may point at invalid IDs).
397397
invalid_workspace_ids: Workspace folder names to reassign.
398398

tests/test_raw_accessors.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
import os
77
import sys
88
import unittest
9+
from unittest.mock import patch
910

1011
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1112
if REPO_ROOT not in sys.path:
1213
sys.path.insert(0, REPO_ROOT)
1314

1415
from models.conversation import Bubble, Composer
16+
from services.workspace_tabs import _bubble_entry_timestamp_ms
1517
from models.raw_access import (
1618
composer_newly_created_files,
1719
conversation_header_bubble_id,
@@ -86,6 +88,45 @@ def test_bubble_timestamp_ms_prefers_created_at_and_handles_zero(self) -> None:
8688
with self.assertNoLogs("models.conversation", level="WARNING"):
8789
self.assertIsNone(bad_bool.bubble_timestamp_ms())
8890

91+
def test_bubble_thinking_silent_when_missing(self) -> None:
92+
bubble = Bubble.from_dict({"text": "hi"}, bubble_id="b-1")
93+
with self.assertNoLogs("models.conversation", level="WARNING"):
94+
self.assertIsNone(bubble.thinking)
95+
96+
def test_bubble_thinking_warns_on_wrong_type(self) -> None:
97+
bubble = Bubble.from_dict({"thinking": 42}, bubble_id="b-bad")
98+
with self.assertLogs("models.conversation", level="WARNING") as logs:
99+
self.assertIsNone(bubble.thinking)
100+
self.assertTrue(any("thinking" in m for m in logs.output), logs.output)
101+
102+
def test_bubble_thinking_accepts_str_or_dict(self) -> None:
103+
s = Bubble.from_dict({"thinking": "trace"}, bubble_id="b-str")
104+
self.assertEqual(s.thinking, "trace")
105+
d = Bubble.from_dict({"thinking": {"text": "nested"}}, bubble_id="b-dict")
106+
self.assertEqual(d.thinking, {"text": "nested"})
107+
108+
def test_bubble_entry_timestamp_ms_preserves_epoch_zero(self) -> None:
109+
"""Regression: falsy ``or now()`` must not replace a valid ``createdAt`` of 0."""
110+
bubble = Bubble.from_dict({"createdAt": 0}, bubble_id="b-zero")
111+
sentinel_now_ms = 9_999_000_000_000
112+
with patch(
113+
"services.workspace_tabs.datetime",
114+
) as mock_datetime:
115+
mock_datetime.now.return_value.timestamp.return_value = sentinel_now_ms / 1000
116+
ts = _bubble_entry_timestamp_ms(bubble)
117+
self.assertEqual(ts, 0)
118+
self.assertNotEqual(ts, sentinel_now_ms)
119+
120+
def test_bubble_entry_timestamp_ms_falls_back_when_no_timestamp(self) -> None:
121+
bubble = Bubble.from_dict({"text": "hi"}, bubble_id="b-none")
122+
sentinel_now_ms = 1_700_000_000_000
123+
with patch(
124+
"services.workspace_tabs.datetime",
125+
) as mock_datetime:
126+
mock_datetime.now.return_value.timestamp.return_value = sentinel_now_ms / 1000
127+
ts = _bubble_entry_timestamp_ms(bubble)
128+
self.assertEqual(ts, sentinel_now_ms)
129+
89130
def test_dict_bridge_newly_created_files_matches_composer_property(self) -> None:
90131
data = {**GOOD_COMPOSER_RAW, "newlyCreatedFiles": [{"uri": {"path": "/a"}}]}
91132
composer = Composer.from_dict(data, composer_id="cid-bridge")

0 commit comments

Comments
 (0)