Skip to content

Commit f6d3ff9

Browse files
committed
fix: review findings
1 parent 14e9f54 commit f6d3ff9

6 files changed

Lines changed: 58 additions & 29 deletions

File tree

services/summary_cache.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def fingerprint_workspace_storage(
6161
cli_chats_path: str | None = None,
6262
) -> dict[str, Any]:
6363
"""Build a fingerprint dict for cache invalidation."""
64-
ws_mt: list[tuple[str, int]] = []
64+
ws_mt: list[list[str | int]] = []
6565
for entry in workspace_entries:
6666
name = entry.get("name")
6767
if not isinstance(name, str):
@@ -71,8 +71,8 @@ def fingerprint_workspace_storage(
7171
p = os.path.join(base, rel)
7272
mtime = _file_mtime_ns(p)
7373
if mtime is not None:
74-
ws_mt.append((f"{name}/{rel}", mtime))
75-
ws_mt.sort(key=lambda x: x[0])
74+
ws_mt.append([f"{name}/{rel}", mtime])
75+
ws_mt.sort(key=lambda row: row[0])
7676

7777
return {
7878
"version": CACHE_VERSION,
@@ -84,10 +84,22 @@ def fingerprint_workspace_storage(
8484
}
8585

8686

87+
def _normalize_fingerprint(fp: dict[str, Any]) -> dict[str, Any]:
88+
"""Normalize fingerprint for comparison (JSON round-trip uses lists, not tuples)."""
89+
normalized = dict(fp)
90+
wf = fp.get("workspace_files")
91+
if isinstance(wf, list):
92+
normalized["workspace_files"] = [
93+
[row[0], row[1]] if isinstance(row, (list, tuple)) and len(row) == 2 else row
94+
for row in wf
95+
]
96+
return normalized
97+
98+
8799
def _fingerprint_equal(a: object, b: dict[str, Any]) -> bool:
88100
if not isinstance(a, dict):
89101
return False
90-
return a == b
102+
return _normalize_fingerprint(a) == _normalize_fingerprint(b)
91103

92104

93105
def _read_cache_file(path: Path | str) -> dict[str, Any] | None:

services/workspace_db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ def collect_invalid_workspace_ids(workspace_entries: list[dict]) -> set[str]:
296296
" AND LENGTH(value) > 10"
297297
" AND value LIKE '%fullConversationHeadersOnly%'"
298298
" AND value NOT LIKE '%fullConversationHeadersOnly\":[]%'"
299+
" AND value NOT LIKE '%fullConversationHeadersOnly\": []%'"
299300
)
300301

301302

templates/workspace.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ <h2 id="project-name">Loading...</h2>
122122
}
123123

124124
async function selectTab(id) {
125+
selectTab._latest = id;
125126
const summary = allTabs.find(t => t.id === id);
126127
if (!summary) return;
127128

@@ -139,9 +140,11 @@ <h2 id="project-name">Loading...</h2>
139140

140141
// Resolve full tab: use cache if available, lazy-fetch if not
141142
if (tabCache[id]) {
143+
if (selectTab._latest !== id) return;
142144
selectedTab = tabCache[id];
143145
} else if (summary.bubbles) {
144146
// Fallback: summary endpoint returned a full tab (e.g. old server version)
147+
if (selectTab._latest !== id) return;
145148
selectedTab = summary;
146149
tabCache[id] = summary;
147150
} else {
@@ -151,13 +154,16 @@ <h2 id="project-name">Loading...</h2>
151154
'<div class="loading-center" style="min-height:60vh"><div class="spinner"></div><p>Loading conversation…</p></div>';
152155
try {
153156
const res = await fetch(`/api/workspaces/${WORKSPACE_ID}/tabs/${id}`);
157+
if (selectTab._latest !== id) return;
154158
if (!res.ok) throw new Error(`HTTP ${res.status}`);
155159
const data = await res.json();
160+
if (selectTab._latest !== id) return;
156161
if (data.error) throw new Error(data.error);
157162
showIncompleteResultsBanner('parse-warnings-host', data.warnings);
158163
tabCache[id] = data.tab;
159164
selectedTab = data.tab;
160165
} catch (e) {
166+
if (selectTab._latest !== id) return;
161167
const main = document.getElementById('main-content');
162168
main.classList.remove('is-loading');
163169
main.innerHTML =
@@ -166,6 +172,8 @@ <h2 id="project-name">Loading...</h2>
166172
}
167173
}
168174

175+
if (selectTab._latest !== id) return;
176+
169177
document.getElementById('action-buttons').style.display = 'flex';
170178

171179
const codeEditsBtn = document.getElementById('dl-code-edits');

tests/test_summary_cache.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,25 @@ def test_fingerprint_includes_workspace_files(self):
6565
fp = fingerprint_workspace_storage(ws, entries, global_db_path=None, rules=[])
6666
self.assertTrue(fp["workspace_files"])
6767

68+
def test_workspace_files_fingerprint_round_trip(self):
69+
"""JSON cache round-trip must match freshly computed workspace_files fingerprints."""
70+
with tempfile.TemporaryDirectory() as ws:
71+
entry_dir = os.path.join(ws, "entry1")
72+
os.makedirs(entry_dir)
73+
db = os.path.join(entry_dir, "state.vscdb")
74+
with open(db, "wb") as f:
75+
f.write(b"x")
76+
entries = [{"name": "entry1", "workspaceJsonPath": os.path.join(entry_dir, "workspace.json")}]
77+
fp = fingerprint_workspace_storage(ws, entries, global_db_path=None, rules=[])
78+
projects = [{"id": "a", "name": "A", "conversationCount": 1, "lastModified": "x"}]
79+
warnings: list = []
80+
set_cached_projects(fp, projects, warnings)
81+
fp2 = fingerprint_workspace_storage(ws, entries, global_db_path=None, rules=[])
82+
hit = get_cached_projects(fp2)
83+
self.assertIsNotNone(hit, msg="cache miss after JSON round-trip of workspace_files")
84+
assert hit is not None
85+
self.assertEqual(hit[0], projects)
86+
6887

6988
if __name__ == "__main__":
7089
unittest.main()

tests/test_workspace_listing_performance.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
import sys
1414
import tempfile
1515
import unittest
16-
from unittest.mock import call, patch
16+
from unittest.mock import patch
1717

1818
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1919
if REPO_ROOT not in sys.path:
2020
sys.path.insert(0, REPO_ROOT)
2121

22+
import services.workspace_listing as _ws_listing_mod
2223
from services.workspace_listing import list_workspace_projects
2324

2425
COMPOSER_ID = "composer-list-perf"
@@ -78,29 +79,21 @@ def test_no_global_bubble_id_query(self):
7879

7980
executed_queries: list[str] = []
8081

81-
original_open_global_db = None
82-
import services.workspace_db as _ws_db_mod
83-
84-
original_open_global_db = _ws_db_mod.open_global_db
82+
original_open_global_db = _ws_listing_mod.open_global_db
8583

8684
from contextlib import contextmanager
8785

8886
@contextmanager
8987
def _spying_open_global_db(workspace_path):
9088
with original_open_global_db(workspace_path) as (conn, path):
9189
if conn is not None:
92-
original_execute = conn.execute
93-
94-
def _spying_execute(query, params=()):
95-
executed_queries.append(query)
96-
return original_execute(query, params)
97-
98-
conn.execute = _spying_execute # type: ignore[method-assign]
90+
conn.set_trace_callback(executed_queries.append)
9991
yield conn, path
10092

101-
with patch.object(_ws_db_mod, "open_global_db", _spying_open_global_db):
102-
projects, warnings = list_workspace_projects(ws_path, rules=[])
93+
with patch.object(_ws_listing_mod, "open_global_db", _spying_open_global_db):
94+
list_workspace_projects(ws_path, rules=[], nocache=True)
10395

96+
self.assertTrue(executed_queries, msg="expected SQL queries to be recorded")
10497
bubble_scans = [q for q in executed_queries if "bubbleId:%" in q]
10598
self.assertEqual(
10699
bubble_scans,

tests/test_workspace_tabs_summary.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,25 +110,19 @@ def _make_fixture(base: str) -> str:
110110

111111
def _collect_queries(ws_path, fn):
112112
"""Call fn(ws_path) while recording every SQL query; return (result, queries)."""
113-
import services.workspace_db as _ws_db_mod
113+
import services.workspace_tabs as _ws_tabs_mod
114114

115-
orig = _ws_db_mod.open_global_db
115+
orig = _ws_tabs_mod.open_global_db
116116
executed: list[str] = []
117117

118118
@contextmanager
119119
def _spy(workspace_path):
120120
with orig(workspace_path) as (conn, path):
121121
if conn is not None:
122-
orig_exec = conn.execute
123-
124-
def _tracking_execute(query, params=()):
125-
executed.append(query)
126-
return orig_exec(query, params)
127-
128-
conn.execute = _tracking_execute # type: ignore[method-assign]
122+
conn.set_trace_callback(executed.append)
129123
yield conn, path
130124

131-
with patch.object(_ws_db_mod, "open_global_db", _spy):
125+
with patch.object(_ws_tabs_mod, "open_global_db", _spy):
132126
result = fn(ws_path)
133127
return result, executed
134128

@@ -145,8 +139,9 @@ def test_no_global_bubble_scan(self):
145139
"""list_workspace_tab_summaries must not issue a bubbleId:% LIKE query."""
146140
(payload, status), queries = _collect_queries(
147141
self.ws_path,
148-
lambda p: list_workspace_tab_summaries("global", p, rules=[]),
142+
lambda p: list_workspace_tab_summaries("global", p, rules=[], nocache=True),
149143
)
144+
self.assertTrue(queries, msg="expected SQL queries to be recorded")
150145
bubble_scans = [q for q in queries if "bubbleId:%" in q]
151146
self.assertEqual(
152147
bubble_scans,
@@ -198,6 +193,7 @@ def test_scoped_bubble_query_only(self):
198193
self.ws_path,
199194
lambda p: assemble_single_tab("global", COMPOSER_ID, p, rules=[]),
200195
)
196+
self.assertTrue(queries, msg="expected SQL queries to be recorded")
201197
global_bubble_scans = [
202198
q for q in queries if "bubbleId:%" in q and f"bubbleId:{COMPOSER_ID}:%" not in q
203199
]

0 commit comments

Comments
 (0)