You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As someone changing how a command writes its output, I want a lint rule that
refuses ambient mutation, so a test cannot quietly replace a stream, a global or
a console method that every other test in the process is also using.
Current gap
Nothing stops a module from assigning to the environment it runs in. One test
helper did:
// packages/cli/tests/plan-cli.test.ts, before #803process.stdout.write=((chunk: string|Uint8Array)=>{chunks.push(typeofchunk==="string" ? chunk : newTextDecoder().decode(chunk));returntrue;})astypeofprocess.stdout.write;
That is one assignment, and it cost three separate things:
It stood in for a stream without honouring the stream's contract. write(chunk[, encoding][, callback]) calls back when the stream has taken
the bytes. This double never called back. So every case that drove xmd plan
proved its behaviour against something no real stdout does.
It hid a production defect for as long as it existed. The command wrote
with the fire-and-forget one-argument form, which is exactly the Make piped xmd syntax output complete #715 defect —
a piped program past the pipe buffer loses its tail, silently, exit 0. No plan
case could see that, because the double accepted everything instantly.
It turned the fix into a hang. When the command started waiting for the
callback (Deliver every whole CLI result before exit #803), the double never delivered one, and the tiers failed with Promise resolution is still pending but the event loop has already resolved.
The symptom named neither the stub nor the stream.
The seam to do this properly already existed and was already the documented
convention. PlanDependencies.progress is described as "the entrypoint's facts
about its own process.stderr … Nothing here detects a runtime or inspects a
terminal", with the harness capturing chunks and refuseProgress standing
where a broken pipe would. Stdout simply had no counterpart, so the test reached
around the design instead of through it. #803 adds deliver() beside progress, and the mutation is gone — but nothing prevents the next one.
Contract
A rule local/no-ambient-mutation reports assignment to state the module does
not own: members of process (notably process.stdout/stderr and their
methods), globalThis, Deno, and console.
It reports the assignment, and names the alternative: take the capability as a
dependency, or compose it as middleware.
Recognition is by binding rather than by spelling, the way local/no-sync-filesystem resolves Deno and node:fs — a local named process that the module declared itself is not the global.
Restoring in a finally or an ensure() does not make it acceptable: the
window is still shared with everything else running in the process, which is
why the verification battery cannot be run concurrently (Make the full verification battery safe to run concurrently #279).
The rule is "error" in .oxlintrc.json, with no test-file override: a test
is the place this happens, so exempting tests would exempt the whole problem.
Writing to a stream. The subject is assignment, not use: the service hosts
that hand a child's bytes to process.stdout are calling a stream, not
replacing one, and stay untouched.
Reaching for a global to read it. process.stdout.isTTY is a fact the
entrypoint states; deciding whether that should also become a dependency is
its own question.
Story
As someone changing how a command writes its output, I want a lint rule that
refuses ambient mutation, so a test cannot quietly replace a stream, a global or
a console method that every other test in the process is also using.
Current gap
Nothing stops a module from assigning to the environment it runs in. One test
helper did:
That is one assignment, and it cost three separate things:
write(chunk[, encoding][, callback])calls back when the stream has takenthe bytes. This double never called back. So every case that drove
xmd planproved its behaviour against something no real stdout does.
with the fire-and-forget one-argument form, which is exactly the Make piped
xmd syntaxoutput complete #715 defect —a piped program past the pipe buffer loses its tail, silently, exit 0. No plan
case could see that, because the double accepted everything instantly.
callback (Deliver every whole CLI result before exit #803), the double never delivered one, and the tiers failed with
Promise resolution is still pending but the event loop has already resolved.The symptom named neither the stub nor the stream.
The seam to do this properly already existed and was already the documented
convention.
PlanDependencies.progressis described as "the entrypoint's factsabout its own
process.stderr… Nothing here detects a runtime or inspects aterminal", with the harness capturing chunks and
refuseProgressstandingwhere a broken pipe would. Stdout simply had no counterpart, so the test reached
around the design instead of through it. #803 adds
deliver()besideprogress, and the mutation is gone — but nothing prevents the next one.Contract
local/no-ambient-mutationreports assignment to state the module doesnot own: members of
process(notablyprocess.stdout/stderrand theirmethods),
globalThis,Deno, andconsole.dependency, or compose it as middleware.
local/no-sync-filesystemresolvesDenoandnode:fs— a local namedprocessthat the module declared itself is not the global.finallyor anensure()does not make it acceptable: thewindow is still shared with everything else running in the process, which is
why the verification battery cannot be run concurrently (Make the full verification battery safe to run concurrently #279).
"error"in.oxlintrc.json, with no test-file override: a testis the place this happens, so exempting tests would exempt the whole problem.
Acceptance
plan-cli.test.tshelper, and the message sendsthe reader to a dependency or middleware.
process.env.X,globalThis.X,console.logand amethod of
Deno.stdout.nor reading any of these.
deno task lintstays green onmainwith the rule enabled, or every site itfinds is fixed in the same change.
Out of scope
that hand a child's bytes to
process.stdoutare calling a stream, notreplacing one, and stay untouched.
process.stdout.isTTYis a fact theentrypoint states; deciding whether that should also become a dependency is
its own question.
Relationships
dependency it should have used.
xmd syntaxoutput complete #715.local/require-scope-bound-event-registrationandlocal/no-sync-filesystemas a rule about not reaching around a lifetime.