Fix Jest runner discovery for deeply nested / dynamic-route TS tests#2003
Fix Jest runner discovery for deeply nested / dynamic-route TS tests#2003Serhan-Asad wants to merge 26 commits into
Conversation
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>
PDD-Auto-Heal-Checkpoint: success
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>
Adversarial review loop (gpt-5.6-codex, xhigh) — resolvedTwo rounds of independent review; all correctness findings fixed: Round 1
Round 2
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>
Review round 3 — resolved
All correctness findings across three review rounds are addressed. The only intentionally-scoped item is Windows |
gltanaka
left a comment
There was a problem hiding this comment.
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 ignored —
pdd/get_test_command.py:120-127._workspace_globs_for()returns positive and negative pnpm patterns as indistinguishable strings, then_belongs_to_ancestor_workspace()usesany(...). With the officially supported pnpm shapepackages: ['packages/**', '!**/test/**'], a package underpackages/app/test/fixturematches the positive pattern, the exclusion never takes effect, and the code reportsmember=True. I reproduced the resulting behavior at082244d6: detection crosses that package's ownpackage.jsonboundary 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.
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>
Changes-requested addressed — workspace include/exclude + brace semanticsThanks @gltanaka. The finding is correct and now fixed with TDD (failing test → fix):
Root cause
FixMembership now requires ≥1 positive glob match AND no Red → green (the two reproducing tests)Before ( After ( Membership unit checks: Coverage added
Verification (head
|
|
Coordination note from the #1998 stack orchestrator ( Checked for overlap: #1998 does not touch 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. |
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>
…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>
…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>
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:Shallow discovery. It walked up only 5 parent directories looking for a
jest/vitest/playwrightconfig. A colocated page test such asfrontend/src/app/hackathon/[eventId]/team/__tests__/test_page.tsxsits 6–8 directories belowfrontend/jest.config.js, so the config was never found and resolution fell back tonpx tsx <file>— which has nodescribe/itglobals and runs from the wrong cwd.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
package.json) is reached, with a defensive iteration cap. The nearest ancestor config still wins, and the search never escapes above the project root.--runTestsByPathso 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 thepackage.jsonproject-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-cliand its guarded--runTestsByPathregression activates.🤖 Generated with Claude Code