Skip to content

fix(playground): emit PHP escapes instead of raw control characters - #2407

Merged
chubes4 merged 1 commit into
mainfrom
fix/2402-query-recorder-control-chars
Aug 29, 2026
Merged

fix(playground): emit PHP escapes instead of raw control characters#2407
chubes4 merged 1 commit into
mainfrom
fix/2402-query-recorder-control-chars

Conversation

@chubes4

@chubes4 chubes4 commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

First slice of #2402 item 5. Fixes a real defect and returns three excluded tests to the gate.

The bug

packages/runtime-playground/src/query-recorder.ts:80 builds PHP source inside a JavaScript template literal:

$operation = strtolower( strtok( trim( (string) $fingerprint ), " \t\n\r\0\x0B" ) ?: '' );

Those are single backslashes, so JavaScript resolves every sequence when the template is evaluated. The generated PHP carries actual tab, newline, carriage return, NUL and vertical tab bytes instead of PHP's own escape sequences.

Measured before the fix — the generated source contained a raw NUL at index 4893:

context: "n = strtolower( strtok( trim( (string) $fingerprint ), \" \t\n\r\u0000\u000b\" ) ?: '' );"

Why it matters

PHP parses either form to the same string, so behaviour is unchanged. The problem is transport: Node refuses argv entries containing null bytes, so any harness invoking php -r with this code dies before PHP starts.

TypeError [ERR_INVALID_ARG_VALUE]: The argument 'args[1]' must be a string without null bytes.
    at runCommandText (scripts/test-kit.ts:55:28)
    at runPhpJson (scripts/test-kit.ts:60:10)

scripts/test-kit.ts runPhpJson does exactly that, which is why three tests could never pass. They were added in June 2026, never wired to a gate, and sat rotting until discovery in #2406 surfaced them — where they had to be excluded.

The fix

Escape the backslashes so PHP receives the escape text rather than the resolved bytes. One line.

Verification

Generated output is clean:

length: 19416 | raw NUL/VT chars: 0
contains PHP escape text: true

The three blocked tests now pass, and return to discovery:

test before after
tests/rest-request-query-params.test.ts ERR_INVALID_ARG_VALUE pass
tests/wordpress-crud-contracts.test.ts ERR_INVALID_ARG_VALUE pass
tests/performance-observation-contracts.test.ts ERR_INVALID_ARG_VALUE pass

Nothing that consumes the generator regressed. Since the emitted PHP text changes, I checked every dependent:

bench-command-step-behavior.test.ts        PASS
agent-task-contracts.test.ts               PASS
playground-fuzz-suite-public.test.ts       PASS
nested-fuzz-suite-recipe-command.test.ts   PASS
scripts/php-fuzz-suite-runner-smoke.php    PASS

Full aggregate green:

npm run check
# [smoke] check: 4 declared, 329 discovered (concurrency 8), 8 serial
# [smoke] check passed: 341 command(s)
# real 7m59.799s

Discovery exclusions drop from 15 to 12.

Remaining in item 5

Seven excluded tests still need triage — they fail for reasons unrelated to this bug and each needs its own judgement on fix-versus-delete. Separately, the test: scripts in package.json are now largely redundant with discovery and can be pruned. Both are follow-ups; bundling them here would bury a one-line production fix under unrelated churn.


AI assistance disclosure: authored by Claude (Sonnet 4.5) running in OpenCode, directed by @chubes4. The model traced the three excluded tests to a shared root cause, confirmed the raw NUL byte in generated output, verified the fix removes it, and re-ran every consumer of the generator plus the full aggregate before opening. All figures are measured. Reviewed by a human before opening.

query-recorder.ts builds PHP source inside a JavaScript template literal. The
whitespace list " \t\n\r\0\x0B" was written with single backslashes, so
JavaScript resolved each sequence at evaluation time and the generated PHP
carried actual tab, newline, carriage return, NUL and vertical tab bytes
rather than PHP's own escape sequences.

PHP parses either form to the same string, so behaviour is unchanged, but the
raw NUL makes the generated source unpassable as a process argument: Node
rejects argv entries containing null bytes. Any harness invoking 'php -r'
with this code fails before PHP runs.

That blocked three tests, which is why they sat unmaintained since June and
were excluded when discovery landed in #2406:

  tests/rest-request-query-params.test.ts
  tests/wordpress-crud-contracts.test.ts
  tests/performance-observation-contracts.test.ts

Escape the backslashes so PHP receives the escape text. Generated output now
contains zero raw NUL or vertical tab characters, the three tests pass, and
they return to discovery. Exclusions drop from 15 to 12.

Verified unchanged: bench-command-step-behavior, agent-task-contracts,
playground-fuzz-suite-public, nested-fuzz-suite-recipe-command, and
php-fuzz-suite-runner-smoke all still pass. Full aggregate green at 341
commands in 7m59s.
@chubes4
chubes4 merged commit d00e93d into main Aug 29, 2026
5 checks passed
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.

1 participant