Skip to content

Commit 789648f

Browse files
test(agent-context): cover symlink containment in the mtime fallback
The recursive fallback resolves each candidate before the relative_to() containment check, but nothing exercised that path. Add a parity test for a plan reachable only through a specs/ symlink pointing outside the project: relative_to() is lexical and would accept it, emitting an in-project-looking path for an out-of-project file. Both the bash twin and the Python port skip it, so the "at <plan>" line is omitted. Also correct the module docstring, which still described the fallback as scanning specs/*/plan.md one level deep.
1 parent 0f95e09 commit 789648f

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

‎extensions/agent-context/scripts/python/update_agent_context.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
1212
When ``plan_path`` is omitted, the script derives it from
1313
``.specify/feature.json`` (written by /speckit-specify). Falls back to the most
14-
recently modified ``specs/*/plan.md`` only when feature.json is absent or its
15-
plan does not exist yet.
14+
recently modified ``plan.md`` found anywhere under ``specs/`` — scoped layouts
15+
nest it as ``specs/<scope>/<feature>/plan.md`` — only when feature.json is
16+
absent or its plan does not exist yet.
1617
"""
1718

1819
from __future__ import annotations

‎tests/extensions/test_update_agent_context_python_parity.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,38 @@ def test_python_mtime_fallback_finds_nested_plan_matching_bash(
343343
assert b"at specs/backend/001-nested/plan.md" in content
344344

345345

346+
@requires_posix_bash
347+
def test_python_mtime_fallback_skips_plan_reached_through_escaping_symlink(
348+
tmp_path: Path,
349+
) -> None:
350+
"""A plan reached via a specs/ symlink out of the project is not selected.
351+
352+
``relative_to()`` is lexical, so ``specs/linked/001-x/plan.md`` looks
353+
in-project even when ``specs/linked`` points outside the tree. Resolving
354+
before the containment check rejects it, so the fallback finds nothing and
355+
the ``at <plan>`` line is omitted rather than naming an out-of-project file
356+
with an in-project-looking path. Mirrors the bash twin's ``_resolved_rel``.
357+
"""
358+
repo_a, repo_b = twin_projects(tmp_path, context_file="AGENTS.md")
359+
for repo in (repo_a, repo_b):
360+
outside = repo.parent / f"outside-{repo.name}" / "001-x"
361+
outside.mkdir(parents=True, exist_ok=True)
362+
(outside / "plan.md").write_text("# plan\n", encoding="utf-8")
363+
specs = repo / "specs"
364+
specs.mkdir(parents=True, exist_ok=True)
365+
(specs / "linked").symlink_to(outside.parent, target_is_directory=True)
366+
# Sanity: the plan really is reachable through the symlink.
367+
assert (specs / "linked" / "001-x" / "plan.md").is_file()
368+
369+
bash = run_bash(repo_a)
370+
py = run_python(repo_b)
371+
372+
assert_parity(bash, py, repo_a, repo_b)
373+
content = (repo_b / "AGENTS.md").read_bytes()
374+
assert content == (repo_a / "AGENTS.md").read_bytes()
375+
assert b"\nat " not in content
376+
377+
346378
@requires_posix_bash
347379
def test_python_prefers_feature_json_over_mtime_matching_bash(tmp_path: Path) -> None:
348380
repo_a, repo_b = twin_projects(tmp_path, context_file="AGENTS.md")

0 commit comments

Comments
 (0)