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.
126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
"""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 |