Skip to content

Fix Jest runner discovery for deeply nested / dynamic-route TS tests#2003

Open
Serhan-Asad wants to merge 26 commits into
mainfrom
fix/jest-runner-discovery-dynamic-routes
Open

Fix Jest runner discovery for deeply nested / dynamic-route TS tests#2003
Serhan-Asad wants to merge 26 commits into
mainfrom
fix/jest-runner-discovery-dynamic-routes

Conversation

@Serhan-Asad

Copy link
Copy Markdown
Collaborator

Problem

get_test_command.py's TypeScript runner detector (_detect_ts_test_runner) left colocated Next.js app-page suites unexecutable through PDD, discovered while addressing the review on promptdriven/pdd_cloud#3251 (issue promptdriven/pdd_cloud#3024). Two independent defects:

  1. Shallow discovery. It walked up only 5 parent directories looking for a jest/vitest/playwright config. A colocated page test such as frontend/src/app/hackathon/[eventId]/team/__tests__/test_page.tsx sits 6–8 directories below frontend/jest.config.js, so the config was never found and resolution fell back to npx tsx <file> — which has no describe/it globals and runs from the wrong cwd.

  2. Regex path targeting. Jest was invoked as npx jest --no-coverage -- <abs path>. Jest treats the trailing path as a regex; Next.js dynamic-route segments ([eventId], [slug]) are regex character classes, so the literal bracketed path matched nothing (No tests found, exiting with code 1).

Net effect: a regenerating pdd change/sync could not execute these generated suites, preserving a false-green split (generator reports its suite passing while CI's own runner never executed it).

Fix

  • Walk up until a runner config is found or the JS project root (nearest package.json) is reached, with a defensive iteration cap. The nearest ancestor config still wins, and the search never escapes above the project root.
  • Invoke Jest with --runTestsByPath so the resolved absolute path is matched literally, regardless of bracketed dynamic-route segments.

Verification

Against promptdriven/pdd_cloud#3251's six migrated app-page suites, all six now resolve to the frontend Jest runner (cwd = frontend/) and execute through PDD's real command path: 339/339 tests pass (previously: 2/6 executed, 4/6 returned "No tests found").

New regression tests cover both defects (deep discovery, --runTestsByPath, literal bracketed path, and the package.json project-root boundary). Prompt (get_test_command_python.prompt) and fingerprint (.pdd/meta/...) updated to match.

Downstream

Unblocks the complete fix for promptdriven/pdd_cloud#3251 (coordinated with #1984). On release, pdd_cloud re-pins pdd-cli and its guarded --runTestsByPath regression activates.

🤖 Generated with Claude Code

Serhan-Asad and others added 5 commits July 11, 2026 14:46
get_test_command's TypeScript runner detector had two defects that left
colocated Next.js app-page suites unexecutable through PDD:

1. It walked up only 5 parent directories looking for a jest/vitest/
   playwright config, so a page test at e.g.
   frontend/src/app/hackathon/[eventId]/team/__tests__/ never reached
   frontend/jest.config.js and fell back to `npx tsx` (no describe/it,
   wrong cwd). The walk now continues to the JS project root (nearest
   package.json), with a defensive iteration cap, and never escapes above
   the project root.

2. Jest was invoked with a trailing bare path that Jest treats as a
   regex. Next.js dynamic-route segments ([eventId]/[slug]) are regex
   character classes, so the literal bracketed path matched nothing
   ("No tests found"). Jest is now invoked with `--runTestsByPath` so the
   resolved absolute path is matched literally.

Verified against promptdriven/pdd_cloud#3251's six migrated app-page
suites: all six now resolve to the frontend Jest runner and execute
(339/339 tests) through PDD's real command path. Prompt and fingerprint
updated to match; adds regression tests for both defects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-1 review fixes for the TS test-runner detector:

- Boundary regression (major): stopping the upward walk at the nearest
  package.json broke workspace monorepos, where a leaf package has its
  own manifest but inherits the Jest/Vitest/Playwright config from the
  workspace root. Walk instead to the repository root (nearest ancestor
  holding .git) — through intermediate package.json files — so the
  workspace-root config is still found; never escape above the repo root.

- Playwright bracket handling (major): Playwright treats its positional
  argument as a regex, so a literal `.spec` path under a dynamic route
  ([slug]) never matched. Regex-escape the path for Playwright (Jest
  --runTestsByPath / Vitest keep it literal).

- Shell safety (major): the resolved path was concatenated unquoted into a
  command string that callers run with shell=True, so spaces / bracket
  globs / $() could be re-split or reinterpreted. Shell-quote the path for
  every runner (also makes POSIX shlex.split round-trip cleanly).

Prompt and fingerprint updated to match; adds regressions for
workspace-root inheritance, the repo-root no-escape boundary, Playwright
bracket escaping, and shell-quoted paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-1 fixed a boundary that stopped at the nearest package.json (which
missed workspace-root configs), but switching to a plain .git boundary
over-corrected: an independent package inside a git repo would wrongly
adopt the repository-root config, and a non-git tree could walk to the
filesystem root.

Boundary is now the JS project root (nearest package.json), crossed only
when the package is a member of an ancestor workspace (a `workspaces`
field, `pnpm-workspace.yaml`, or `lerna.json`), and never above the
repository root (.git). Adds `_belongs_to_ancestor_workspace` plus
regressions for an independent leaf package and a non-git stray-ancestor
config; prompt + fingerprint updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clarify that TestCommand.command is a POSIX-shell string (matching how all
pdd callers execute verify commands) so shlex.quote is the correct quoting
here; making runner execution safe under Windows cmd.exe would require
moving all callers to argv + shell=False, a pre-existing cross-cutting
change out of scope for runner detection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Adversarial review loop (gpt-5.6-codex, xhigh) — resolved

Two rounds of independent review; all correctness findings fixed:

Round 1

  • Deep discovery past the 5-parent cap; --runTestsByPath so bracketed dynamic-route paths match literally; regressions + prompt/fingerprint aligned.

Round 2

  • Boundary (fixed): the interim .git-only boundary over-corrected — an independent package inside a git repo would adopt the repo-root config. Now: stop at the JS project root (nearest package.json), cross it only when the package is a member of an ancestor workspace (workspaces field / pnpm-workspace.yaml / lerna.json), and never cross the repository root. Added _belongs_to_ancestor_workspace + negative tests (workspace-root inheritance, independent leaf, non-git stray ancestor).
  • Shell quoting (scoped): the resolved path is shlex.quoted — correct for pdd's POSIX-shell command convention (all callers run shell=True / shlex.split). Full Windows-cmd.exe safety would require moving every caller to an argv list + shell=False, a pre-existing cross-cutting change out of scope for runner detection; documented in-code.

Verified: the six pdd_cloud app-page suites all resolve to the frontend Jest runner and execute 339/339 through PDD's real command path; existing runner-detection tests green.

…iew)

_belongs_to_ancestor_workspace treated any ancestor `workspaces` (or
pnpm/lerna) declaration as membership, so an unrelated package (e.g.
`vendor/tool`) beneath a workspace root would wrongly adopt the root
config. Now read the ancestor's declared package globs and require the
leaf package's path (relative to the declaring ancestor) to actually
match one, with segment-wise `*`/`**` semantics; unparseable pnpm YAML is
treated conservatively as non-member. Adds a negative test for an
unrelated package under a workspace root; prompt + fingerprint updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Review round 3 — resolved

  • Workspace membership (fixed): the round-2 workspace-aware boundary treated any ancestor workspaces/pnpm/lerna declaration as membership, so an unrelated package (e.g. vendor/tool) beneath a workspace root could wrongly adopt the root config. Membership is now proven — the ancestor's declared package globs must actually match the leaf's path relative to that ancestor (segment-wise * = one segment, ** = any depth); unparseable pnpm YAML is treated conservatively as non-member. Added a negative test (vendor/tool under workspaces: ["packages/*"] → no match) plus glob unit coverage.

All correctness findings across three review rounds are addressed. The only intentionally-scoped item is Windows cmd.exe quoting, which requires moving pdd's codebase-wide command-as-string convention to argv/shell=False (documented, out of scope for runner detection).

@Serhan-Asad Serhan-Asad requested a review from gltanaka July 12, 2026 02:42
@Serhan-Asad Serhan-Asad self-assigned this Jul 12, 2026

@gltanaka gltanaka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review verdict: changes requested

This PR is needed. The original failure is real: deeply nested TypeScript suites can miss the runner config and fall back to npx tsx, while Jest interprets bracketed dynamic-route paths as regexes without --runTestsByPath. The deep walk, correct cwd, literal Jest targeting, and shell quoting solve that concrete pdd_cloud failure. I also independently confirmed the six downstream suites select cwd=frontend and execute 339/339 through the returned TestCommand at this PR head.

One correctness issue still blocks merge:

  • Workspace exclusions are ignoredpdd/get_test_command.py:120-127. _workspace_globs_for() returns positive and negative pnpm patterns as indistinguishable strings, then _belongs_to_ancestor_workspace() uses any(...). With the officially supported pnpm shape packages: ['packages/**', '!**/test/**'], a package under packages/app/test/fixture matches the positive pattern, the exclusion never takes effect, and the code reports member=True. I reproduced the resulting behavior at 082244d6: detection crosses that package's own package.json boundary and selects the repository-root Jest config. An explicitly excluded/independent package can therefore execute the wrong ancestor runner—the same false-positive boundary this PR is intended to prevent. The custom matcher also treats other valid workspace glob syntax such as brace expansion literally, so membership is not yet faithfully proven for supported npm/Yarn/pnpm declarations.

Please implement actual include/exclude workspace-pattern semantics (or use a faithful matcher) and add focused coverage for a broad positive plus matching negative pattern, including pnpm's documented !**/test/** form. Brace-expansion coverage should be added if npm/Yarn workspace declarations remain advertised as supported.

Validation performed at the current head:

  • tests/test_get_test_command.py: 65 passed
  • compile and git diff --check: passed
  • real downstream runner boundary: 6 suites, 339/339 passed
  • all GitHub checks: green

The testing is strong for the original deep-path/Jest defect, but it is not sufficient for the expanded workspace-membership implementation: the automated tests cover only simple positive packages/* matching and do not exercise pnpm exclusions or richer workspace glob semantics. The PR also does not include the exact CLI workflow/transcript requested by docs/runbooks/pr-loop-process.md; please include reproducible final evidence with the follow-up.

Serhan-Asad and others added 2 commits July 11, 2026 22:05
Failing tests for gltanaka's review finding: _belongs_to_ancestor_workspace
ignores pnpm `!` exclusions and treats brace expansion literally. A package
under packages/app/test/fixture (excluded by `!**/test/**`) is wrongly
reported a member, and a packages/{app,lib} member is wrongly missed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_belongs_to_ancestor_workspace treated any ancestor `workspaces`/pnpm/
lerna declaration as membership and matched globs literally, so a package
explicitly excluded by pnpm's supported `!**/test/**` (or missed by a
`{a,b}` brace glob) was mis-classified — an excluded/independent package
could cross its own package.json boundary and adopt the repo-root runner.

Membership now requires a positive glob match AND no `!` exclusion match,
with brace alternations expanded and segment-wise `*`/`**` semantics.
Adds `_expand_braces`, `_split_top_level_commas`, `_package_matches_workspace`;
pnpm YAML parsing narrowed to specific exceptions. Prompt + fingerprint
updated. Closes gltanaka's changes-requested review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Changes-requested addressed — workspace include/exclude + brace semantics

Thanks @gltanaka. The finding is correct and now fixed with TDD (failing test → fix):

  • test(get_test_command)cac9ddc30
  • fix(get_test_command)6a8b1fa65

Root cause

_workspace_globs_for() returned positive and !-negative patterns as indistinguishable strings and _belongs_to_ancestor_workspace() used any(...), so pnpm's supported !**/test/** never took effect (a leading ! also didn't even match under the custom matcher), and brace alternations were matched literally. A package under packages/app/test/fixture therefore reported member=True and crossed its own package.json boundary to the repo-root Jest config.

Fix

Membership now requires ≥1 positive glob match AND no ! exclusion match, with brace {a,b} expansion and segment-wise */** matching. New helpers _expand_braces, _split_top_level_commas, _package_matches_workspace; pnpm YAML parsing narrowed to specific exceptions (unparseable → conservatively non-member).

Red → green (the two reproducing tests)

Before (082244d6):

FAILED test_pnpm_exclusion_pattern_excludes_matching_package
  -> npx jest --no-coverage --runTestsByPath .../packages/app/test/fixture/... (adopted repo config)
FAILED test_brace_expansion_in_workspace_glob_matches_member
  -> npx tsx .../packages/app/...                                            (member missed)

After (6a8b1fa65): 2 passed, full detection/monorepo suite 39 passed.

Membership unit checks:

packages/app/test/x  vs [packages/**, !**/test/**] -> False   (excluded)
packages/app         vs [packages/**, !**/test/**] -> True    (member)
packages/app         vs [packages/{app,lib}]        -> True    (brace expanded)
vendor/tool          vs [packages/*]                -> False   (unrelated)

Coverage added

test_pnpm_exclusion_pattern_excludes_matching_package (broad positive packages/** + pnpm !**/test/**), test_brace_expansion_in_workspace_glob_matches_member, on top of the existing workspace-root-inheritance, independent-leaf, and non-git negative tests.

Verification (head 6a8b1fa65)

  • tests/test_get_test_command.py detection/workspace suites: 39 passed
  • pylint pdd/get_test_command.py: 9.95/10 (only a pre-existing open()-encoding warning in _load_language_format, untouched)
  • Downstream real runner boundary: 6/6 suites resolve to cwd=frontend, 339/339 through the returned TestCommand
  • git log origin/main..HEAD --oneline: intentional commits only; no regenerated architecture.json/prompts/unrelated pdd/ files

Re: the runbook transcript — this reproducible red→green + downstream evidence is captured above; happy to attach anything further you'd like in the requested format.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Coordination note from the #1998 stack orchestrator (stack/e2e-staging-v2).

Checked for overlap: #1998 does not touch pdd/get_test_command.py, its prompt, or tests/test_get_test_command.py, so there is no code conflict between #2003 and #1998 — they can land in either order.

The only coupling is evidence-level: #1998's real-execution/E2E staging runs exercise the test-runner discovery path this PR fixes. If #2003 lands first, #1998's staging evidence benefits from the corrected deeply-nested / dynamic-route TS runner discovery; if #1998 lands first, its evidence was gathered against the pre-fix discovery. No blocker either way — flagging for the manager's awareness when sequencing.

Serhan-Asad and others added 5 commits July 12, 2026 11:29
Independent Codex gpt-5.6-sol xhigh review found six issues; five fixed
(one rejected as pre-existing/out-of-scope, see below).

- F2 nested workspace root: _workspace_root_for now returns the declaring
  workspace root, used as a traversal ceiling so an independent intermediate
  package.json between a member and its workspace root no longer stops the
  walk (e.g. member vendor/container/packages/app under root glob
  vendor/container/packages/*). Previously returned None instead of Jest.
- F3 source precedence: pnpm-workspace.yaml is authoritative — pnpm ignores
  the package.json `workspaces` field, so a stale/attacker-controlled list no
  longer unions in and over-authorizes membership. Missing/unparseable pnpm
  YAML fails closed.
- F4 malformed manifests: a package.json/lerna.json whose parsed top level is
  not an object ([] or a bare string) contributes no globs instead of raising
  AttributeError during discovery.
- F5 symlink containment: the repo root is anchored lexically (nearest .git
  without following symlinks); a test dir symlinked outside the repo can no
  longer smuggle the walk into an out-of-repo config. In-repo symlinks still
  resolve normally.
- F6 brace-bomb budget: brace expansion is bounded by _MAX_BRACE_EXPANSION;
  an untrusted {a,b}-style brace bomb fails membership closed instead of
  materializing an exponential list.

F1 (Windows shell=True quoting) rejected: pdd's verify boundary is POSIX-only
(callers use subprocess start_new_session=True; no Windows classifier; base
already returned unquoted shell=True strings). The argv/shell=False migration
is the cross-cutting change the module docstring explicitly scopes out.

Prompt raised to doctrine altitude for the new behaviors; fingerprint meta
re-synced (prompt/code/test hashes). +11 regression/negative-control tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-5 review)

Second independent Codex gpt-5.6-sol xhigh review (fresh, at the new head)
found five medium issues, all in the untrusted-manifest trust-boundary class;
all five fixed with regression coverage.

- R2-1 symlink to foreign checkout: _lexical_repo_root only anchors at a
  non-symlinked directory, so a symlinked component whose `.git` probe would
  follow the link out of the tree (repo/link -> outside, both with .git) no
  longer mis-anchors containment; the out-of-repo config is refused.
- R2-2 invalid-UTF-8 pnpm YAML: read now also catches UnicodeError → membership
  unproven instead of crashing discovery with UnicodeDecodeError.
- R2-3 `**` exponential match / RecursionError: glob matching is now an
  iterative O(n*m) dynamic program (no recursion, no slicing) with a segment
  budget; a wall of `**` fails closed instead of backtracking/recursing.
- R2-4 brace-bomb RecursionError: _expand_braces is iterative (worklist, not
  recursion) with worklist+output budgets, so deep nesting fails closed via
  _BraceBudgetError rather than escaping as RecursionError. _package_matches_
  workspace now catches _PatternBudgetError/RecursionError defensively.
- R2-5 non-string glob entries: `_string_globs` requires every declared entry
  to be a str; a JSON/YAML `true`/number makes the declaration malformed (no
  globs) instead of coercing to a glob like "True".

Prompt raised to doctrine altitude for bounded/no-recursion pattern evaluation,
string-only declarations, invalid-encoding fail-closed, and foreign-checkout
symlink containment. Fingerprint meta re-synced. +7 regression tests (85 total).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ine altitude (round-6 review)

Third independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found six
findings; all addressed.

- R3-1 symlink to nested foreign checkout: repository containment now anchors at
  the deepest-symlink boundary (component-aware), so a `.git` probe cannot follow
  a symlinked path component into a foreign checkout below it. Anchors at the true
  repo; the foreign config is refused.
- R3-2 lexical-root depth cap: the lexical repo-root search now walks to the
  filesystem root (no artificial 200 cap), so a deep path ending in an escaping
  symlink still anchors containment and refuses the out-of-repo config.
- R3-3 aggregate resource budget: brace expansion now shares one budget across
  the whole membership check; raw-glob count is capped; comma-splitting and
  segment-splitting are bounded before allocation. A many-glob or comma/slash-wall
  manifest fails closed instead of exhausting memory.
- R3-4 pnpm YAML recursion bomb: YAML parsing also catches RecursionError → fail
  closed instead of crashing discovery.
- R3-5 dotfile matching: glob matching applies minimatch `dot:false` — a wildcard
  segment no longer matches a leading-dot segment (so `packages/*` excludes
  `packages/.shadow`; `packages/.*` still includes it).
- R3-6 prompt altitude: rewrote the prompt to stable numbered R1–R13 MUST/MUST NOT
  behavioral contracts with a Vocabulary and pinned Interface, removing transcribed
  private-helper names and algorithm recipes (DP/fnmatch/iterative), per
  docs/prompting_guide.md.

Fingerprint meta re-synced. +11 regression tests (95 total). Suite green, E2E
green, pylint E 10.00.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…& injection gaps (round-7 review)

Fourth independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found
five findings; all fixed.

- R4-1 (high) `..`+symlink mis-anchor: os.path.abspath collapses `..` textually,
  which is unsound across symlinks and could mis-anchor containment into a
  foreign checkout. Detection now fails closed when the naive-collapsed path and
  the true resolution disagree (a `..` traversed a symlink); a `..` with no
  symlink is unaffected.
- R4-2 (high) placeholder re-injection: pdd/fix_error_loop.py re-ran
  {file}/{test} substitution on the already-complete, shell-quoted command,
  so a maliciously named path (containing `{test};touch …`) broke the quoting
  → command injection. get_test_command_for_file already substitutes CSV paths
  and embeds the quoted runner path, so the redundant re-substitution is removed
  and documented (prompt R14).
- R4-3 config-file symlink escape: a runner config that is itself a symlink (or
  broken symlink) resolving outside the repository is now refused, anchored on
  the canonical repo root (reliable even where the lexical anchor is unset by a
  harmless system symlink); an in-repo config symlink still works.
- R4-4 workspace root without package.json: the declaring workspace root now
  caps the walk even when it has no package.json of its own (pnpm/lerna root),
  so an unrelated ancestor config above it is not adopted; a config AT the root
  is still inherited.
- R4-5 JSON recursion/oversized manifest: package.json/lerna.json parsing now
  catches RecursionError, and every declaration file is size-bounded before it
  is read/parsed. A parse-failing lerna.json no longer falls through to the
  `packages/*` default (fail closed).

Prompt R8/R10 broadened and R14 added (all behavioral). Fingerprint meta
re-synced. +11 regression tests (105 total). Suite green, E2E green, pylint E
10.00.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pt contract (round-8 review)

Fifth independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found four
findings; three fixed, one (low/optional) rejected with evidence.

- R5-1 (high) manifest symlinked to a device: a pnpm-workspace.yaml/package.json
  symlinked to /dev/zero reports st_size 0 then streams forever. _read_manifest_text
  now requires the resolved target to be a regular file (S_ISREG) and reads at
  most _MAX_MANIFEST_BYTES+1 from a byte-capped handle; a symlink to a genuine
  regular file still works.
- R5-2 (high) brace-expansion byte blowup: the count budget allowed a near-5MB
  prefix glob with a few brace groups to materialize ~5GB of strings. Added a
  per-raw-glob length cap (_MAX_GLOB_LENGTH) that bounds expansion in bytes, not
  just result count. Real globs are tiny; an over-long one fails membership closed.
- R5-3 (medium) fix-loop prompt provenance: the {file}/{test} no-re-substitution
  rule lived only in get_test_command's prompt, so regenerating fix_error_loop.py
  could reopen the injection. Added the MUST NOT rule to fix_error_loop_python.prompt
  and a direct _run_non_python_initial_verification regression test.
- R5-4 (low, optional) REJECTED: nested `{a{b,c}}` bash-brace parity. The impl
  emits it literally and fails membership CLOSED (never falsely includes); it is
  not a real workspace-glob pattern (would require a package literally named
  `{ab}`), and full bash-brace parity is out of proportion/scope.

The reviewer independently re-verified fingerprint consistency (prompt/code/
example/test + include_deps all match). Meta re-synced. +5 regression tests
(109 total). Suite green, E2E green, pylint E 10.00.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Serhan-Asad and others added 8 commits July 12, 2026 13:08
…e-parse (round-9 review)

Sixth independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found three
medium findings; all fixed.

- R6-1 dangling pnpm symlink: `Path.exists()` is False for a dangling symlink, so
  an authoritative pnpm-workspace.yaml was ignored and membership fell through to a
  stale package.json `workspaces`. pnpm presence is now detected lexically
  (exists() or is_symlink()); a present-but-unreadable/dangling pnpm config yields
  no globs (fail closed) and never falls through.
- R6-2 symlink-loop crash: a self-referential/looping symlink path makes
  Path.resolve() raise RuntimeError on 3.12, which propagated out of
  get_test_command_for_file and crashed sync/fix orchestration. Resolution is now
  guarded (OSError, RuntimeError) in _detect_ts_test_runner and the containment
  helpers → refuse discovery (None) instead of crashing.
- R6-3 O(n^2) manifest re-parsing: _workspace_root_for was called at each walk
  step and re-read every ancestor manifest, so deeply nested packages with padded
  manifests could re-parse gigabytes. Added a per-discovery cache keyed by
  canonical ancestor path so each manifest is parsed at most once.

Meta re-synced. +5 regression tests (114 total; incl. a read-at-most-once
assertion). Suite green, E2E green, pylint E 10.00.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… package.json boundary (round-10 review)

Seventh independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found two
findings; both fixed, plus a same-class injection in the delegated smart-detection
path.

- R7-1 (high) CSV-fallback command injection: `get_test_command_for_file` step 2
  substituted the raw test path into the CSV command without shell-quoting, so a
  path like `/repo/$(touch PWN)/a.py` injected under the callers' `shell=True`.
  The substitution now uses `shlex.quote`.
- Same-class injection in step 3 (smart detection): `default_verify_cmd_for`
  (pdd/agentic_langtest.py), whose command `get_test_command_for_file` returns as-is,
  substituted the path unquoted — and its Python fallback used bare double quotes,
  which do NOT stop `$()` command substitution. Both now use `shlex.quote`.
- R7-2 (medium) dangling package.json boundary: the JS-project boundary used
  `Path.exists()`, which is False for a dangling/looping `package.json` symlink, so
  the walk slipped past an independent package and adopted an unrelated ancestor
  config. Boundary detection now uses `os.path.lexists` (present-but-dangling still
  stops the walk); a proven workspace member still inherits correctly.

Prompt R13 updated to require shell-quoting the CSV path. Fingerprint meta
re-synced (incl. the agentic_langtest include-dep hash). +6 regression tests
(get_test_command 117 + agentic_langtest 15 = shell-injection and dangling/looping
symlink coverage). Suites green, E2E green, pylint E 10.00.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dget, grounding provenance (round-11 review)

Eighth independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found three
findings; all fixed.

- R8-1 (high) agentic_fix.py command injection: `_verify_and_log` and the
  preflight path re-substituted `{test}`/`{cwd}` into a command that may already
  be a finalized `default_verify_cmd_for` output, so a resolved path containing a
  literal `{test}` + shell metacharacters broke its quoting and injected under
  `bash -lc`. Now the PDD_AGENTIC_VERIFY_CMD *template* is substituted with
  `shlex.quote`d values, while a *finalized* command (env unset) runs as-is with
  no re-substitution. No caller passes a template via the param, so the env var is
  the authoritative template source.
- R8-2 (medium) agentic_langtest provenance: the module's own prompt still claimed
  "all non-Python languages return None" and an unsafe double-quoted Python path,
  and the grounding example (`context/agentic_langtest_example.py`, an include-dep
  of the get_test_command prompt) demonstrated the same unsafe double-quoting.
  Back-propagated the real CSV-then-pytest resolution and the POSIX shell-quoting
  contract into the prompt, and rewrote the grounding example to `shlex.quote`
  every shell-substituted path (JS require target JSON-encoded then shell-quoted).
- R8-3 (medium) aggregate matching DoS: the per-check DP-cell budget did not bound
  work across the whole discovery walk, so a heavy manifest re-evaluated at each of
  many nested package boundaries could stall for tens of seconds. The DP-cell
  budget is now shared across the entire `_detect_ts_test_runner` call; a legit
  deep chain still resolves.

Fingerprint meta re-synced (incl. the changed grounding-example include-dep hash).
+4 regression tests (get_test_command 119 + agentic_langtest 15 + agentic_fix
TestVerifyAndLog 9). Suites green, E2E green, pylint E clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…explicit fix-loop provenance, faithful grounding (round-12 review)

Ninth independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found five
findings; all fixed.

- R9-1 (high) get_run_command injection: `get_run_command_for_file` substituted the
  path into the run template unquoted, and `agentic_fix` preflight/`_verify_and_log`
  execute it via `bash -lc` — so a Java/other test at `/repo/$(touch PWN)/x` injected.
  The `{file}` substitution now uses `shlex.quote`.
- R9-2 (medium) whitespace-normalized glob: `_package_matches_workspace` stripped
  surrounding whitespace, turning `" packages/* "` (literal-whitespace, a non-match
  in workspace tools) into a broader `packages/*` and falsely proving membership.
  Whitespace is now preserved; only an exactly-empty entry is skipped.
- R9-3 (medium) fix-loop provenance heuristic: agentic_fix `_verify_and_log`
  inferred template-vs-finalized from ambient `PDD_AGENTIC_VERIFY_CMD` state, which
  mishandles an explicit `verify_cmd=` template arg. Provenance is now tracked
  explicitly (`verify_cmd_is_template`): templates get shell-quoted `{test}`/`{cwd}`
  substitution, finalized commands run as-is.
- R9-4 (medium) grounding example contradicted the contract: the get_test_command
  prompt's grounding (`context/agentic_langtest_example.py`) hardcoded JS npm / Java
  Maven-Gradle logic instead of the real CSV-first→Python-fallback→None resolution.
  Rewrote `default_verify_cmd_for` to mirror the real contract with `shlex.quote`.
- R9-5 (low) interpreter path unquoted: the Python fallback now shell-quotes
  `sys.executable` too (a Python installed under a path with spaces no longer
  re-splits), in both the code and the grounding example.

Fingerprint meta re-synced (incl. the changed grounding-example + agentic_langtest
include-dep hashes). +5 regression tests across get_test_command / get_run_command /
agentic_fix. Suites green, E2E green, pylint E clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s (round-13 review)

Tenth independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found one
finding; fixed.

- R10-1 (medium) pnpm YAML construction crash: a `pnpm-workspace.yaml` value such
  as `packages: [2020-99-99]` makes PyYAML's timestamp constructor raise a bare
  `ValueError` ("month must be in 1..12") — which is NOT a `yaml.YAMLError`, so it
  escaped the handler and crashed runner discovery. The parse now also catches
  `ValueError`/`TypeError`/`OverflowError` (any construction failure on untrusted
  YAML) and fails membership closed.

Fingerprint meta re-synced. +2 regression tests (malformed-timestamp scalar,
end-to-end). Suite 123 passed, E2E green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…und-14 review)

Eleventh independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found one
finding; fixed.

- R11-1 (high) manifest parse OOM: a valid under-5MB workspace manifest containing
  millions of short entries materialized hundreds of MB of Python objects during
  json.loads/yaml.safe_load — before the `_MAX_RAW_GLOBS` count guard could run —
  and could OOM a worker. Real manifests are tiny, so the byte caps are now small:
  1 MiB for package.json/lerna.json and 256 KiB for pnpm-workspace.yaml (YAML
  amplifies more per byte and a real pnpm workspace file is a few KB). Peak parse
  memory is now bounded to ~100 MB with generous headroom for legit manifests.
  `_string_globs` also rejects an over-`_MAX_RAW_GLOBS`-cardinality list up front,
  before validating/copying it, so no second full-list traversal occurs.

Fingerprint meta re-synced. +1 regression test (under-byte-cap over-cardinality +
over-byte-cap). Suite 124 passed, E2E green, pylint E clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t (round-15 review)

Twelfth independent Codex gpt-5.6-sol xhigh review (fresh, at new head) found one
finding; fixed.

- R12-1 (medium) lerna explicit-null default: `lerna.json` `{"packages": null}` was
  treated the same as an omitted key and granted the documented `packages/*`
  default, falsely proving membership and adopting the root Jest config — which
  contradicts the prompt contract that only an *omitted* key gets the default. The
  code now distinguishes absence (`"packages" not in lerna` → default) from an
  explicit value (`null` → `_string_globs(None)` → no globs, fail closed).

Fingerprint meta re-synced. +1 regression test (omitted vs explicit-null, direct +
end-to-end). Suite 125 passed, E2E green, pylint E clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…option groups

Round-13 review (Codex gpt-5.6-sol xhigh) found _expand_braces stopped at the
first single-option brace and emitted the whole pattern literally, so a real
alternation appearing later in the pattern was never expanded. With workspace
globs ["packages/**", "!packages/{foo}/{a,b}"] the `{a,b}` in the exclusion was
left unexpanded, the exclusion never matched leaf `packages/{foo}/a`, and the
excluded package was falsely treated as a workspace member — inheriting an
ancestor's Jest config.

Add _find_expandable_brace: scan left-to-right for the first two-or-more-option
brace, descending into single-option groups (so `{a{b,c}}` expands its inner
`{b,c}`, bash parity) and skipping past fully-literal groups (so `{foo}/{a,b}`
still finds `{a,b}`). Only a pattern with no real alternation anywhere is emitted
whole. All existing budgets (brace/comma/segment/cell) are unchanged and still
fail closed on bombs.

Regression: excluded leaf now non-member, sibling the exclusion does not name is
still a member (negative control), nested-in-singleton expands. Prompt R6 updated
to state the contract; meta re-synced; E2E green.

Also back-propagate the shell-quoting security contract into the
get_run_command and agentic_fix prompts (no meta; provenance completeness).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Serhan-Asad and others added 4 commits July 12, 2026 15:16
…lternation

Round-14 review (Codex gpt-5.6-sol xhigh) found _find_expandable_brace returned
None on the first *unbalanced* `{` (no matching `}`), so a later balanced
alternation was never reached — the same short-circuit class as round 13, but for
an unmatched brace. With globs ["packages/**", "!packages/{foo/{a,b}"] the `{a,b}`
in the exclusion never expanded, the exclusion never matched leaf
`packages/{foo/a`, and the excluded package was falsely proven a workspace member
(inheriting an ancestor's Jest config). This contradicts prompt R6, which already
requires an unbalanced brace to be literal without short-circuiting.

Treat an unmatched `{` as a literal single character and keep scanning
(`i += 1; continue`) for a later expandable group, matching bash
(`echo packages/{foo/{a,b}` -> `packages/{foo/a packages/{foo/b`). Only a pattern
with no real alternation anywhere is emitted whole.

Regression: excluded leaves `a`/`b` non-members, unnamed sibling `c` still a
member (negative control), trailing lone `{` stays literal. Docstring updated;
meta re-synced (code+test hashes); E2E green; lint 9.72 (no regression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…work

Round-15 review (Codex gpt-5.6-sol xhigh) found two medium defects in the brace
expander:

R15-1 (correctness): _split_top_level_commas is not escape-aware, so a workspace
glob like "packages/{foo\,bar,baz}" — where `\,` is a literal comma and thus two
options, not three — over-expanded to include "packages/bar", falsely proving an
independent leaf a workspace member and letting it adopt an ancestor's Jest
config. Skipping an individual escaped glob would be unsafe for exclusions (a
misparsed `!` glob could fail to exclude → false member), so any glob set
containing a backslash now fails membership closed. Matches prompt R6's new
escape clause.

R15-2 (DoS): _find_expandable_brace recursively rescans each nested singleton on
every worklist entry, so a 861-char glob ("{"*400 + x + "}"*400 + "/{a,b}"*10) —
within every byte/count/segment budget and producing exactly 1024 expansions —
cost 24.8s of pure scanning. Add an aggregate _MAX_BRACE_SCAN_WORK budget
charging every scanned character, shared across the whole discovery walk (like
the DP-cell budget), raising _BraceBudgetError so such a pattern fails closed
(now ~0.9s). Realistic globs cost <200K scans — ~40x under budget; a 500-option
alternation still expands and matches in ~10ms.

Regression: escaped-comma positive/exclusion sets non-member; deep-nested
singleton time-bounded; large legit brace still resolves members. Prompt R6/R9
updated; meta re-synced (prompt+code+test); E2E green; lint 9.73.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…closed

Round-16 review (Codex gpt-5.6-sol xhigh) found the per-segment fnmatch matcher
diverges from minimatch on bracket character classes. With
workspaces ["packages/[^a]"], minimatch's [^a] negates the class (match any char
but `a`), but Python fnmatch treats `^` as a literal member, so the code matched
`packages/a` (should be excluded) and rejected `packages/b` (should match) —
falsely proving `packages/a` a workspace member and adopting an unrelated
ancestor Jest config. POSIX classes like [[:alpha:]] are likewise unsupported by
fnmatch.

Fail membership closed on any glob containing `[` (the class opener), the same
single-boundary treatment already used for backslash escapes. The supported glob
language is now exactly literal / `*` / `**` / `?` / `{,}`; anything else is
unproven and never adopts a foreign config (the leaf just uses its nearest
package.json). A bracket in a *path segment* (a Next.js dynamic-route dir like
`[eventId]`) is literal data, not a glob metacharacter, and still matches an
ordinary `*` glob — the E2E dynamic-route case is unaffected.

Regression: `[^a]` positive/exclusion and `[[:alpha:]]`/`[a-z]` fail closed;
`[eventId]` path still matches `packages/*`; `*`/`{,}`/`?`/backslash behavior
unchanged. Prompt R6 updated; meta re-synced (prompt+code+test); E2E green;
lint 9.73.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Proactively close the last minimatch-parity gap in the same class as rounds
15-16 (backslash escapes, bracket character classes). minimatch expands extglobs
— ?(…), *(…), +(…), @(…), !(…) — but the per-segment fnmatch matcher treats them
literally, so an extglob *exclusion* under-matches: e.g. with
["packages/*", "!packages/@(foo|bar)"], packages/foo is NOT excluded (fnmatch
compares the literal string "@(foo|bar)") and is falsely proven a workspace
member, adopting an unrelated ancestor Jest config. Verified by direct probe.

Extend the existing single-boundary fail-closed guard in
_package_matches_workspace to also reject any glob containing an extglob prefix
(_EXTGLOB_MARKERS). The supported glob language is now exactly
literal / * / ** / ? / {,}; backslash, bracket classes, and extglobs all fail
closed (leaf uses its nearest package.json, never a foreign config). Bare `?`/`*`
wildcards and `@`-scoped-style path segments without `(` are unaffected.

Regression added; prompt R6 updated; meta re-synced (prompt+code+test); E2E
green; get_test_command suite 133 passed; lint 9.73.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants