Skip to content

Commit ab8a3fa

Browse files
fix(#125): harden alias cache validation and honor nocache fast path
1 parent 0f896e4 commit ab8a3fa

6 files changed

Lines changed: 72 additions & 7 deletions

File tree

services/summary_cache.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,12 @@ def get_cached_invalid_workspace_aliases(
258258
aliases = data.get("invalid_workspace_aliases")
259259
if not isinstance(aliases, dict):
260260
return None
261-
return {str(k): str(v) for k, v in aliases.items()}
261+
validated: dict[str, str] = {}
262+
for key, value in aliases.items():
263+
if not isinstance(key, str) or not isinstance(value, str):
264+
return None
265+
validated[key] = value
266+
return validated
262267

263268

264269
def set_cached_invalid_workspace_aliases(

services/workspace_context.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import os
66
import sqlite3
7-
from dataclasses import dataclass, field, replace
7+
from dataclasses import dataclass, replace
88
from typing import Any
99

1010
from models import Bubble
@@ -37,7 +37,7 @@ class WorkspaceContext:
3737
workspace_path_to_id: dict[str, str]
3838
project_layouts_map: dict[str, list[str]]
3939
bubble_map: dict[str, Bubble]
40-
invalid_workspace_aliases: dict[str, str] = field(default_factory=dict)
40+
invalid_workspace_aliases: dict[str, str] | None = None
4141

4242

4343
def _entries(
@@ -171,6 +171,8 @@ def resolve_invalid_workspace_aliases_cached(
171171
Returns:
172172
``{invalid_id: replacement_id}``, or ``{}`` when every workspace is valid.
173173
"""
174+
if ctx.invalid_workspace_aliases is not None:
175+
return ctx.invalid_workspace_aliases
174176
if not ctx.invalid_workspace_ids:
175177
return {}
176178

@@ -227,6 +229,8 @@ def with_invalid_workspace_aliases(
227229
project_layouts_map: dict[str, list[str]] | None = None,
228230
) -> WorkspaceContext:
229231
"""Return *ctx* with ``invalid_workspace_aliases`` populated from cache."""
232+
if ctx.invalid_workspace_aliases is not None:
233+
return ctx
230234
aliases = resolve_invalid_workspace_aliases_cached(
231235
ctx,
232236
global_db,
@@ -235,6 +239,4 @@ def with_invalid_workspace_aliases(
235239
nocache=nocache,
236240
project_layouts_map=project_layouts_map,
237241
)
238-
if aliases is ctx.invalid_workspace_aliases:
239-
return ctx
240242
return replace(ctx, invalid_workspace_aliases=aliases)

services/workspace_listing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def list_workspace_projects(
9393
)
9494

9595
projects, warnings = _build_workspace_projects_uncached(
96-
workspace_path, rules, orch,
96+
workspace_path, rules, orch, nocache=effective_nocache,
9797
)
9898
if not effective_nocache:
9999
set_cached_projects(orch.fingerprint, projects, warnings)
@@ -104,6 +104,8 @@ def _build_workspace_projects_uncached(
104104
workspace_path: str,
105105
rules: list[Any],
106106
orch: WorkspaceOrchestration,
107+
*,
108+
nocache: bool = False,
107109
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
108110
parse_warnings = ParseWarningCollector()
109111
ctx = orch.ctx
@@ -129,6 +131,7 @@ def _build_workspace_projects_uncached(
129131
global_db,
130132
workspace_path,
131133
rules,
134+
nocache=nocache,
132135
project_layouts_map=project_layouts_map,
133136
)
134137

tests/test_summary_cache.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ def test_invalid_workspace_aliases_cache_miss_on_fingerprint_change(self):
102102
set_cached_invalid_workspace_aliases(fp1, {"broken-ws": "good-ws"})
103103
self.assertIsNone(get_cached_invalid_workspace_aliases(fp2))
104104

105+
def test_invalid_workspace_aliases_rejects_non_string_entries(self):
106+
fp = {"version": 1, "workspace_path": "/ws", "global_db_mtime_ns": 100}
107+
summary_cache._write_cache_file(
108+
summary_cache.INVALID_WORKSPACE_ALIASES_CACHE_FILE,
109+
{
110+
"fingerprint": fp,
111+
"invalid_workspace_aliases": {"broken-ws": 123},
112+
},
113+
)
114+
self.assertIsNone(get_cached_invalid_workspace_aliases(fp))
115+
105116

106117
if __name__ == "__main__":
107118
unittest.main()

tests/test_workspace_context.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,4 +362,30 @@ def test_with_invalid_workspace_aliases_attaches_to_context():
362362
assert enriched.invalid_workspace_aliases == {
363363
"invalidws": "abc123workspace",
364364
}
365-
assert ctx.invalid_workspace_aliases == {}
365+
assert ctx.invalid_workspace_aliases is None
366+
367+
368+
def test_resolve_invalid_workspace_aliases_uses_ctx_fast_path():
369+
from dataclasses import replace
370+
371+
with tempfile.TemporaryDirectory() as tmp:
372+
ws_root = _make_workspace_root(tmp)
373+
_add_workspace_without_folders(ws_root, "invalidws")
374+
ctx = resolve_workspace_context(ws_root)
375+
enriched = replace(
376+
ctx,
377+
invalid_workspace_aliases={"invalidws": "abc123workspace"},
378+
)
379+
conn = _open_workspace_global_db(ws_root)
380+
conn.commit()
381+
try:
382+
with patch(
383+
"services.workspace_context.infer_invalid_workspace_aliases",
384+
) as mock_infer:
385+
aliases = resolve_invalid_workspace_aliases_cached(
386+
enriched, conn, ws_root, [],
387+
)
388+
assert aliases == {"invalidws": "abc123workspace"}
389+
mock_infer.assert_not_called()
390+
finally:
391+
conn.close()

tests/test_workspace_listing_performance.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ def _make_fixture(base: str) -> str:
6565
return ws_path
6666

6767

68+
def _make_fixture_with_invalid_workspace(base: str) -> str:
69+
ws_path = _make_fixture(base)
70+
invalid_dir = os.path.join(ws_path, "invalid-ws")
71+
os.makedirs(invalid_dir)
72+
with open(os.path.join(invalid_dir, "workspace.json"), "w", encoding="utf-8") as f:
73+
json.dump({"folders": []}, f)
74+
return ws_path
75+
76+
6877
class TestListWorkspaceProjectsNoBubbleScan(unittest.TestCase):
6978
"""list_workspace_projects must not query bubbleId rows from global storage."""
7079

@@ -126,6 +135,15 @@ def test_output_shape_preserved(self):
126135
self.assertIn("conversationCount", p)
127136
self.assertIn("lastModified", p)
128137

138+
def test_nocache_bypasses_alias_disk_cache(self):
139+
ws_path = _make_fixture_with_invalid_workspace(self.tmp.name)
140+
with patch(
141+
"services.summary_cache.get_cached_invalid_workspace_aliases",
142+
) as mock_get:
143+
mock_get.return_value = {"invalid-ws": "global"}
144+
list_workspace_projects(ws_path, rules=[], nocache=True)
145+
mock_get.assert_not_called()
146+
129147

130148
if __name__ == "__main__":
131149
unittest.main()

0 commit comments

Comments
 (0)