fix: canonicalize ./ and ~/ prefixes in _normalizeArtifactPath (#3262)
Pre-release Codex regression gate caught that _normalizeArtifactPath() did not strip ./ or ~/ prefixes, so a tool arg recorded as ./foo.md did not match a file-tree-opened foo.md in _turnMutatedPreviewPaths — the open preview was left stale after an agent edit via a ./-prefixed path. Strip ~/ and leading ./ before ignore/membership checks. Node-driven regression test pins foo.md == ./foo.md == ~/foo.md and confirms the existing ignore-dir / URL / empty rejections still hold. Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
This commit is contained in:
@@ -170,6 +170,12 @@ function _normalizeArtifactPath(path){
|
||||
if(!path) return '';
|
||||
path = String(path).trim().replace(/[\`"'<>),.;:]+$/g,'').replace(/^[\`"'(<]+/g,'');
|
||||
if(!path || path.length > 240 || path.includes('://')) return '';
|
||||
// Canonicalize workspace-relative prefixes so a file-tree open ("foo.md") and a
|
||||
// tool arg recorded as "./foo.md" or "~/foo.md" compare equal for mutation
|
||||
// tracking; otherwise an agent edit via a ./-prefixed path leaves the open
|
||||
// preview stale (#3262 / pre-release regression-gate finding).
|
||||
path = path.replace(/^~\//,'').replace(/^(?:\.\/)+/,'');
|
||||
if(!path) return '';
|
||||
if(ARTIFACT_IGNORE_RE.test(path)) return '';
|
||||
if(!/[./]/.test(path)) return '';
|
||||
return path;
|
||||
|
||||
87
tests/test_issue3262_artifact_path_canonicalization.py
Normal file
87
tests/test_issue3262_artifact_path_canonicalization.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Regression: _normalizeArtifactPath() canonicalizes ./ and ~/ prefixes (#3262).
|
||||
|
||||
The workspace preview reload-on-mutation tracking (#3262) compares the open
|
||||
preview path against the set of paths the agent's tools touched during the
|
||||
turn. File-tree opens record a bare workspace-relative path ("foo.md"), but a
|
||||
tool argument can arrive as "./foo.md" or "~/foo.md". Before the fix,
|
||||
_normalizeArtifactPath() did not strip those prefixes, so "./foo.md" != "foo.md"
|
||||
in _turnMutatedPreviewPaths and an agent edit via a ./-prefixed path left the
|
||||
open preview stale (pre-release Codex regression-gate finding).
|
||||
|
||||
This drives the ACTUAL _normalizeArtifactPath() from static/workspace.js via
|
||||
node so it can't drift from a Python mirror.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
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(decl_regex: str) -> str:
|
||||
m = re.search(decl_regex, WORKSPACE_JS)
|
||||
assert m, f"definition not found: {decl_regex}"
|
||||
return m.group(0)
|
||||
|
||||
|
||||
def _normalize_via_node(paths):
|
||||
ignore_re = _extract(r"const ARTIFACT_IGNORE_RE = /.*?/;")
|
||||
# Extract the full function body by brace-matching.
|
||||
start = WORKSPACE_JS.index("function _normalizeArtifactPath(")
|
||||
brace = WORKSPACE_JS.index("{", start)
|
||||
depth = 0
|
||||
end = None
|
||||
for i in range(brace, len(WORKSPACE_JS)):
|
||||
c = WORKSPACE_JS[i]
|
||||
if c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
end = i + 1
|
||||
break
|
||||
fn = WORKSPACE_JS[start:end]
|
||||
driver = (
|
||||
ignore_re + "\n" + fn + "\n"
|
||||
+ "const out = JSON.parse(process.argv[1]).map(_normalizeArtifactPath);\n"
|
||||
+ "process.stdout.write(JSON.stringify(out));\n"
|
||||
)
|
||||
r = subprocess.run(
|
||||
[NODE, "-e", driver, json.dumps(paths)],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
)
|
||||
assert r.returncode == 0, f"node failed: {r.stderr}"
|
||||
return json.loads(r.stdout)
|
||||
|
||||
|
||||
def test_dot_slash_and_tilde_prefixes_canonicalize_to_bare_path():
|
||||
out = _normalize_via_node(["foo.md", "./foo.md", "~/foo.md", "././foo.md"])
|
||||
assert out == ["foo.md", "foo.md", "foo.md", "foo.md"], (
|
||||
f"./ and ~/ prefixes must canonicalize to the bare workspace-relative "
|
||||
f"path so mutation tracking matches a file-tree open (#3262); got {out}"
|
||||
)
|
||||
|
||||
|
||||
def test_nested_relative_path_prefix_canonicalizes():
|
||||
out = _normalize_via_node(["sub/dir/x.py", "./sub/dir/x.py", "~/sub/dir/x.py"])
|
||||
assert out == ["sub/dir/x.py", "sub/dir/x.py", "sub/dir/x.py"], (
|
||||
f"prefix canonicalization must apply to nested paths too (#3262); got {out}"
|
||||
)
|
||||
|
||||
|
||||
def test_canonicalization_preserves_ignore_and_url_rejection():
|
||||
# Canonicalization must not weaken the existing rejections.
|
||||
out = _normalize_via_node(["./node_modules/x.js", "https://e.com/a", "./"])
|
||||
assert out == ["", "", ""], (
|
||||
f"ignore-dir, URL, and empty-after-strip rejections must still hold "
|
||||
f"after prefix canonicalization (#3262); got {out}"
|
||||
)
|
||||
Reference in New Issue
Block a user