|
42 | 42 | * #14015 with its own review gate. `the key set is exactly the carriers` pins |
43 | 43 | * that fence from this side, so a well-meaning widening goes red here. |
44 | 44 | * |
| 45 | + * ## [#16358] The second property this file pins: the human channel stays EMPTY |
| 46 | + * |
| 47 | + * `a coded failure at import surfaces BOTH carriers` below ends with |
| 48 | + * `expect(run.stderr).toBe('')` under the comment *"A --json run leaks nothing |
| 49 | + * to the human channel"*. That comment states a property of the `--json` face |
| 50 | + * AS A WHOLE, and it was honest about the one door it drove — a module that |
| 51 | + * EXISTS AND THROWS AT IMPORT, where esbuild bundles cleanly and prints |
| 52 | + * nothing. The other doors into the same `catch` were uncovered, and they |
| 53 | + * leaked: esbuild's own logger writes straight to stderr from inside |
| 54 | + * `bundleRequire`, BEFORE anything throws, so the `catch` that builds the |
| 55 | + * one-key `{error}` document never gets a chance to suppress it. |
| 56 | + * |
| 57 | + * Re-driven at `7f96e1417e` before the repair, `bin/run-dev.js`, `NO_COLOR=1`: |
| 58 | + * |
| 59 | + * os lint --eval --json --generator /tmp/os16358/nope.mjs |
| 60 | + * exit 1 · stdout 143 B (well-formed `{error}`) · stderr 55 B |
| 61 | + * `✘ [ERROR] Could not resolve "/tmp/os16358/nope.mjs"` |
| 62 | + * os lint --eval --json --generator /tmp/os16358/warn.mjs (LOADS FINE) |
| 63 | + * exit 0 · stdout 3158 B (the live eval report) · stderr 340 B |
| 64 | + * `▲ [WARNING] The "typeof" operator will never evaluate to "null"` |
| 65 | + * |
| 66 | + * ⇒ same command, same face, three answers — with a green pin asserting the |
| 67 | + * one that held. The repair passes `esbuildOptions: { logLevel: 'silent' }` |
| 68 | + * to that ONE `bundleRequire` call and ONLY when `--json` is set; the two |
| 69 | + * cases below drive the two uncovered doors, and each carries its own |
| 70 | + * negative control so a fix that silenced the REFUSAL along with the logger |
| 71 | + * goes red here rather than reading green: |
| 72 | + * |
| 73 | + * - unresolvable path — stderr empty AND the exit is still 1 with the |
| 74 | + * well-formed one-key `{error}` naming the unresolved path; |
| 75 | + * - warning-only — stderr empty on the `--json` face AND the SAME fixture |
| 76 | + * still shows the warning on the HUMAN face. That second leg is what |
| 77 | + * keeps the first from going vacuous: if a future esbuild stopped |
| 78 | + * emitting `impossible-typeof`, an `stderr === ''` assertion alone would |
| 79 | + * stay green while measuring nothing, and the human-face leg reddens |
| 80 | + * instead of hiding it. It also pins the scope of the silence — ⛔ the |
| 81 | + * repair must not reach the face that asked for human output. |
| 82 | + * |
45 | 83 | * ## Why no `dist/` sits on the measured path |
46 | 84 | * |
47 | 85 | * These run the CLI through `bin/run-dev.js`, the SOURCE entry — same CLI, run |
@@ -139,6 +177,21 @@ export default function () { return {}; } |
139 | 177 | const NOT_A_FUNCTION = `export default { nope: true }; |
140 | 178 | `; |
141 | 179 |
|
| 180 | +/** |
| 181 | + * [#16358] Bundles and LOADS successfully, and makes esbuild emit a warning |
| 182 | + * while doing it (`impossible-typeof`). Nothing throws here, so no `catch` |
| 183 | + * ever sees this diagnostic and nothing carries it onto stdout — before the |
| 184 | + * repair it reached stderr on both faces, including the machine one. |
| 185 | + * |
| 186 | + * The generated stack is deliberately trivial: this fixture measures the |
| 187 | + * CHANNEL, not the rubric. `mode: 'live'` in the payload is what proves the |
| 188 | + * module was actually loaded and called. |
| 189 | + */ |
| 190 | +const WARNS_BUT_LOADS = `const probe = 1; |
| 191 | +if (typeof probe === 'null') { throw new Error('unreachable'); } |
| 192 | +export default function () { return { objects: [] }; } |
| 193 | +`; |
| 194 | + |
142 | 195 | beforeAll(() => { |
143 | 196 | dir = mkdtempSync(join(tmpdir(), 'os-lint-eval-envelope-')); |
144 | 197 | }); |
@@ -211,6 +264,58 @@ describe('os lint --eval --json — nothing is minted', () => { |
211 | 264 | }, 120_000); |
212 | 265 | }); |
213 | 266 |
|
| 267 | +describe('os lint --eval --json — the human channel stays empty on EVERY door [#16358]', () => { |
| 268 | + it('the unresolvable path leaks nothing to stderr — and still refuses on stdout', async () => { |
| 269 | + // The door the sibling pin above does NOT drive. esbuild never reaches |
| 270 | + // module evaluation: it throws a `BuildFailure`, and its logger had |
| 271 | + // already written `✘ [ERROR] Could not resolve "…"` to stderr from inside |
| 272 | + // `bundleRequire` — 55 bytes measured at 7f96e1417e for this path length |
| 273 | + // (the emission is `✘ [ERROR] Could not resolve "<path>"`, so the byte |
| 274 | + // count tracks the path; the card's 54 was a 20-character path). |
| 275 | + const run = await runJson(join(dir, 'does-not-exist.mjs')); |
| 276 | + |
| 277 | + // The property the card exists for, in the same shape the existing pin |
| 278 | + // uses one describe up. |
| 279 | + expect(run.stderr).toBe(''); |
| 280 | + |
| 281 | + // ⛔ NEGATIVE CONTROL, on the SAME run: silencing esbuild's logger must |
| 282 | + // not also silence the refusal. A repair that swallowed the throw would |
| 283 | + // satisfy the line above and fail every line below. |
| 284 | + const payload = payloadOf(run, 'unresolvable path — stderr pin'); |
| 285 | + expect(run.code).toBe(1); |
| 286 | + expect(Object.keys(payload)).toEqual(['error']); |
| 287 | + expect(payload.error).toContain('Failed to load generator'); |
| 288 | + expect(payload.error).toContain('Could not resolve'); |
| 289 | + }, 120_000); |
| 290 | + |
| 291 | + it('a generator that only WARNS leaks nothing either — and the human face still shows it', async () => { |
| 292 | + const file = generator('warns-but-loads', WARNS_BUT_LOADS); |
| 293 | + |
| 294 | + // The third door: nothing throws at all, so the `catch` is never entered |
| 295 | + // and there is no error path to blame. 340 bytes of esbuild warning |
| 296 | + // reached stderr here before the repair, on a run that exits 0. |
| 297 | + const machine = await runJson(file); |
| 298 | + expect(machine.stderr).toBe(''); |
| 299 | + |
| 300 | + const payload = payloadOf(machine, 'warning generator — machine face') as unknown as { |
| 301 | + mode?: string; |
| 302 | + error?: unknown; |
| 303 | + }; |
| 304 | + // The module really was loaded and called — otherwise `stderr === ''` |
| 305 | + // above would be measuring a run that never bundled anything. |
| 306 | + expect(payload.mode).toBe('live'); |
| 307 | + expect(payload.error).toBeUndefined(); |
| 308 | + |
| 309 | + // ⛔ SCOPE CONTROL: the silence is the machine face's, not the command's. |
| 310 | + // The same fixture on the HUMAN face must still show esbuild's warning — |
| 311 | + // which also keeps the assertion above from going vacuous if a future |
| 312 | + // esbuild stops emitting this diagnostic. |
| 313 | + const human = await runLint(['--generator', file]); |
| 314 | + expect(human.stderr).toContain('[WARNING]'); |
| 315 | + expect(human.stderr).toContain('typeof'); |
| 316 | + }, 120_000); |
| 317 | +}); |
| 318 | + |
214 | 319 | describe('os lint --eval — the untouched controls', () => { |
215 | 320 | it('the human path is unchanged: still exit 1, still no JSON document', async () => { |
216 | 321 | const run = await runLint(['--generator', join(dir, 'does-not-exist.mjs')]); |
|
0 commit comments