feat(workspaces): autocomplete trusted workspace paths — v0.50.162 (PR #880 by @franksong2702, closes #616)

Adds GET /api/workspaces/suggest endpoint and autocomplete dropdown in the Spaces panel. Suggestions limited to trusted roots (home, saved workspaces, boot default). Keyboard nav, Tab completion, hidden dir support. Symlink-escape and dotdot-escape invariants locked by regression tests.
This commit is contained in:
Frank Song
2026-04-23 10:35:58 +08:00
committed by GitHub
parent 0f1b232c12
commit 62c56175b7
7 changed files with 365 additions and 10 deletions

17
tests/test_issue616.py Normal file
View File

@@ -0,0 +1,17 @@
import pathlib
def test_workspace_suggest_endpoint_is_wired():
src = pathlib.Path("api/routes.py").read_text(encoding="utf-8")
assert '"/api/workspaces/suggest"' in src
def test_spaces_panel_uses_workspace_suggest_autocomplete():
src = pathlib.Path("static/panels.js").read_text(encoding="utf-8")
assert "/api/workspaces/suggest" in src
assert "wsAddSuggestions" in src
assert "scheduleWorkspacePathSuggestions" in src
assert "if(!prefix)" in src
assert "dataset.path" in src
assert "scrollIntoView" in src
assert "_wsSuggestIndex=0" in src

View File

@@ -1,5 +1,5 @@
"""Sprint 5 tests: workspace CRUD, file save, session index, JS serving."""
import json, pathlib, uuid, urllib.request, urllib.error
import json, pathlib, uuid, urllib.request, urllib.error, urllib.parse
import os
from tests._pytest_port import BASE
@@ -80,6 +80,34 @@ def test_workspace_add_requires_path():
result, status = post("/api/workspaces/add", {})
assert status == 400
def test_workspace_suggest_returns_trusted_directories(cleanup_test_sessions):
_, ws = make_session_tracked(cleanup_test_sessions)
child = make_workspace_child(ws, f"workspace-suggest-{uuid.uuid4().hex[:6]}")
nested = make_workspace_child(child, "nested")
prefix = str(child.parent / child.name[:12])
data, status = get(f"/api/workspaces/suggest?prefix={urllib.parse.quote(prefix)}")
assert status == 200
assert str(child) in data["suggestions"]
assert all(not pathlib.Path(p).name.startswith('.') for p in data["suggestions"])
def test_workspace_suggest_hides_untrusted_system_prefix():
data, status = get("/api/workspaces/suggest?prefix=/etc")
assert status == 200
assert data["suggestions"] == []
def test_workspace_suggest_hidden_dirs_only_when_requested(cleanup_test_sessions):
_, ws = make_session_tracked(cleanup_test_sessions)
hidden = make_workspace_child(ws, ".workspace-hidden")
visible = make_workspace_child(ws, "workspace-visible")
base = str(ws) + "/"
data, status = get(f"/api/workspaces/suggest?prefix={urllib.parse.quote(base)}")
assert status == 200
assert str(visible) in data["suggestions"]
assert str(hidden) not in data["suggestions"]
data2, status2 = get(f"/api/workspaces/suggest?prefix={urllib.parse.quote(base + '.w')}")
assert status2 == 200
assert str(hidden) in data2["suggestions"]
def test_workspace_remove(cleanup_test_sessions):
_, ws = make_session_tracked(cleanup_test_sessions)
child = make_workspace_child(ws, f"workspace-remove-{uuid.uuid4().hex[:6]}")