test(workspace): cover Windows backslash artifact open paths
openArtifactPath/_workspacePathExists split on '/' but Windows artifact paths arrive as backslash absolute paths. Regression tests drive the real functions from static/workspace.js via node (mirroring test_issue3262_artifact_path_canonicalization.py): backslash subdir and root artifacts must open, POSIX relative clicks must keep working, and a genuinely missing artifact must still surface file_open_failed.
This commit is contained in:
1 parent
71ee457d7d
commit
5a01ce11cd
1 file changed
+126
@@ -0,0 +1,126 @@
|
||||
"""Regression: artifact clicks with Windows backslash paths open correctly.
|
||||
|
||||
Windows artifact paths are absolute backslash paths, e.g.
|
||||
``D:\\proj\\dir\\file.md``, while ``openArtifactPath()`` /
|
||||
``_workspacePathExists()`` split on ``/``. Before the fix:
|
||||
|
||||
* the workspace-prefix strip (``rel.startsWith(normWs)``) never matched, so the
|
||||
full absolute path was passed down, and
|
||||
* ``_workspacePathExists()``'s ``split('/')`` treated the whole backslash path
|
||||
as a single filename, so ``/api/list`` never matched and the UI reported
|
||||
"cannot open file" (``file_open_failed``).
|
||||
|
||||
The fix normalizes both ``rel`` and the session workspace to ``/`` separators
|
||||
before stripping. This drives the ACTUAL functions from static/workspace.js via
|
||||
node (same pattern as test_issue3262_artifact_path_canonicalization.py) so the
|
||||
test cannot drift from the real frontend code.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
WORKSPACE_JS = (REPO / "static" / "workspace.js").read_text(encoding="utf-8")
|
||||
NODE = shutil.which("node")
|
||||
|
||||
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
|
||||
|
||||
def _extract_function(source: str, name: str) -> str:
|
||||
start = source.index(f"async function {name}(" if f"async function {name}(" in source else f"function {name}(")
|
||||
brace = source.index("{", start)
|
||||
depth = 0
|
||||
for pos in range(brace, len(source)):
|
||||
if source[pos] == "{":
|
||||
depth += 1
|
||||
elif source[pos] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : pos + 1]
|
||||
raise AssertionError(f"could not extract {name}")
|
||||
|
||||
|
||||
def _run_artifact_open(path: str, workspace: str, entries_by_dir: dict) -> dict:
|
||||
"""Run the real openArtifactPath + _workspacePathExists against stubbed deps.
|
||||
|
||||
``entries_by_dir`` maps a workspace-relative dir ('.' for root) to the list
|
||||
of entry names /api/list would return, so the existence check is exercised
|
||||
with realistic data. Returns the recorded calls (openFile / setStatus).
|
||||
"""
|
||||
open_artifact = _extract_function(WORKSPACE_JS, "openArtifactPath")
|
||||
path_exists = _extract_function(WORKSPACE_JS, "_workspacePathExists")
|
||||
entries_json = json.dumps(entries_by_dir)
|
||||
driver = f"""
|
||||
const S = {{ session: {{ session_id: 't', workspace: {json.dumps(workspace)} }} }};
|
||||
const calls = [];
|
||||
function switchWorkspacePanelTab() {{}}
|
||||
function t(key) {{ return key; }}
|
||||
function setStatus(msg) {{ calls.push({{status: msg}}); }}
|
||||
function openFile(rel) {{ calls.push({{open: rel}}); }}
|
||||
const ENTRIES_BY_DIR = {entries_json};
|
||||
async function api(url) {{
|
||||
const m = url.match(/[?&]path=([^&]*)/);
|
||||
const dir = m ? decodeURIComponent(m[1]) : '.';
|
||||
calls.push({{api: dir}});
|
||||
return {{ entries: (ENTRIES_BY_DIR[dir] || []).map(name => ({{name, path: name}})) }};
|
||||
}}
|
||||
{path_exists}
|
||||
{open_artifact}
|
||||
(async () => {{
|
||||
await openArtifactPath({json.dumps(path)});
|
||||
process.stdout.write(JSON.stringify(calls));
|
||||
}})();
|
||||
"""
|
||||
r = subprocess.run(
|
||||
[NODE, "-e", driver],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
)
|
||||
assert r.returncode == 0, f"node failed: {r.stderr}"
|
||||
return json.loads(r.stdout)
|
||||
|
||||
|
||||
WS = r"D:\proj"
|
||||
|
||||
|
||||
def test_windows_backslash_subdir_artifact_opens():
|
||||
"""Subdir artifact with backslash absolute path strips to rel path and opens."""
|
||||
calls = _run_artifact_open(
|
||||
r"D:\proj\src\report.pdf",
|
||||
WS,
|
||||
{".": ["readme.md"], "src": ["report.pdf"]},
|
||||
)
|
||||
assert calls[-1] == {"open": "src/report.pdf"}, calls
|
||||
|
||||
|
||||
def test_windows_backslash_root_artifact_opens():
|
||||
"""Root-level artifact with backslash absolute path opens."""
|
||||
calls = _run_artifact_open(
|
||||
r"D:\proj\readme.md",
|
||||
WS,
|
||||
{".": ["readme.md"], "src": ["report.pdf"]},
|
||||
)
|
||||
assert calls[-1] == {"open": "readme.md"}, calls
|
||||
|
||||
|
||||
def test_forward_slash_relative_behavior_unchanged():
|
||||
"""Existing POSIX-style relative artifact clicks keep working (no regression)."""
|
||||
calls = _run_artifact_open(
|
||||
"./src/report.pdf",
|
||||
WS,
|
||||
{".": ["readme.md"], "src": ["report.pdf"]},
|
||||
)
|
||||
assert calls[-1] == {"open": "src/report.pdf"}, calls
|
||||
|
||||
|
||||
def test_missing_artifact_still_reports_failure():
|
||||
"""A genuinely missing artifact still surfaces file_open_failed."""
|
||||
calls = _run_artifact_open(
|
||||
r"D:\proj\src\missing.md",
|
||||
WS,
|
||||
{".": ["readme.md"], "src": ["report.pdf"]},
|
||||
)
|
||||
assert calls[-1] == {"status": "file_open_failed"}, calls
|
||||
Reference in new issue
Block a user