test(config): read the render fixture's rollback in a temp root of its own

Two defects in the case I committed in cb1833e675, both found by running it.

The anti-slop gate refuses module mocking, and it is right to: the case recorded
the served port by mocking the harness module around the real
`createBundleServer`. Gone, with no disable.

Its replacement read the shared temp directory for the fixture's scratch prefix,
which the render check next door writes to from a worker of its own. So the case
watched that tree appear and be swept up mid-run and called it a change: one red
in four alone, and red in the full suite, where the two run together. `TMPDIR`
now points at a directory this worker made, so the fixture's scratch tree lands
somewhere nothing else writes and what is left in there afterwards was left by
the setup under test. The failed launch also leaves Playwright artifacts and a
browser profile in there, which are Playwright's to clean, so the reading is
filtered to the name the fixture gives its own trees.

The listening-socket half is unchanged and was right: spelled `TCPServerWrap` as
Node spells it, and read a tick after the close callback, because the handle is
still listed while that callback runs.

Both halves now fail on their own without the thing they measure: with no
rollback at all the socket count is one above its baseline, twice out of twice;
with the rollback but no `rm`, the scratch tree is still there. Three green runs
with both.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-20 17:46:23 -04:00
parent cb1833e675
commit 8b37221b57
@@ -1,9 +1,9 @@
import { readdir } from 'node:fs/promises'
import { connect } from 'node:net'
import { mkdtemp, readdir, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs'
import { startTerminalRenderFixture } from './mobile-web-app-terminal-render-fixture.mjs'
/**
* What the render fixture gives back when it never finishes starting.
@@ -18,53 +18,56 @@ import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-depende
* executable that is not there, rather than by standing a double in front of Playwright.
*/
/** The name the fixture gives its scratch tree, which is the only thing in here it owns. */
const SCRATCH_PREFIX = 'orca-c75-terminal-render-'
const describeFixture = mobileWebAppDependenciesPresent() ? describe : describe.skip
// The port the fixture served on, which it never hands out and which `close` makes unreachable.
// Recorded through the real server rather than a double: what is under test is whether the thing
// that was actually listening stopped.
const { ports } = vi.hoisted(() => ({ ports: [] }))
vi.mock('./mobile-web-app-render-harness.mjs', async (importOriginal) => {
const harness = await importOriginal()
return {
...harness,
createBundleServer: async (options) => {
const served = await harness.createBundleServer(options)
ports.push(served.server.address().port)
return served
}
}
let temporaryRoot = null
let realTemporaryRoot = null
beforeAll(async () => {
// The fixture names its scratch tree after `os.tmpdir()`, and the render check next door names
// its own the same way in a worker of its own — so reading the shared temp directory reports
// that one appearing and being swept up mid-case, which is not this case's business. Pointing
// `TMPDIR` at a directory of this worker's own makes the reading exact: whatever is left in
// here afterwards was left by the setup under test.
temporaryRoot = await mkdtemp(join(tmpdir(), 'orca-c75-fixture-rollback-'))
realTemporaryRoot = process.env.TMPDIR
process.env.TMPDIR = temporaryRoot
})
const { startTerminalRenderFixture } = await import('./mobile-web-app-terminal-render-fixture.mjs')
afterAll(async () => {
if (realTemporaryRoot === undefined) {
delete process.env.TMPDIR
} else {
process.env.TMPDIR = realTemporaryRoot
}
await rm(temporaryRoot, { recursive: true, force: true })
})
/** Whether a connection to this port is refused, which is what a closed listener answers. */
function refusesConnections(port) {
return new Promise((resolve) => {
const socket = connect({ host: '127.0.0.1', port })
socket.on('connect', () => {
socket.destroy()
resolve(false)
})
socket.on('error', () => resolve(true))
})
}
async function scratchDirectories() {
const entries = await readdir(tmpdir())
return entries.filter((entry) => entry.startsWith(SCRATCH_PREFIX)).sort()
/**
* Listening sockets this process holds, which is the handle that keeps a worker alive.
*
* Spelled as Node spells it: filtering on `TCPSERVERWRAP` matches nothing and reads zero in both
* arms, which agrees with everything. And the handle is still listed while the close callback
* runs, so the reading is taken a tick later, once the loop has let it go.
*/
async function settledListeningSockets() {
await new Promise((resolve) => setTimeout(resolve, 50))
return process.getActiveResourcesInfo().filter((resource) => resource === 'TCPServerWrap').length
}
describeFixture('the terminal render fixture', () => {
it('takes back the server and the scratch tree when the browser will not start', async () => {
const scratchBefore = await scratchDirectories()
const socketsBefore = await settledListeningSockets()
const realBrowser = process.env.ORCA_MOBILE_WEB_RENDER_BROWSER
process.env.ORCA_MOBILE_WEB_RENDER_BROWSER = join(tmpdir(), 'orca-c75-no-such-browser')
process.env.ORCA_MOBILE_WEB_RENDER_BROWSER = join(temporaryRoot, 'orca-c75-no-such-browser')
try {
// Named, not merely thrown: a build that broke for its own reason would also reject, and
// would satisfy a bare `toThrow` while saying nothing about the rollback under test. It
// also has to be the original error and not whatever the cleanup raised on its way out.
// Named, not merely thrown, and this is also the precondition the two readings below need.
// A bare `toThrow` passes for a build that broke for its own reason, which would leave
// nothing serving and nothing on disk and agree with both assertions for the wrong reason.
// Reaching the launch at all means `createBundleServer` returned, because it is the
// statement before it.
await expect(startTerminalRenderFixture()).rejects.toThrow(
/Failed to launch chromium because executable doesn't exist/
)
@@ -76,10 +79,11 @@ describeFixture('the terminal render fixture', () => {
}
}
// The precondition: the launch has to have been reached with a server already listening, or
// there is nothing for the rollback to have released and the refusal below means nothing.
expect(ports, 'the setup got as far as serving the bundle').toHaveLength(1)
expect(await refusesConnections(ports[0])).toBe(true)
expect(await scratchDirectories()).toEqual(scratchBefore)
expect(await settledListeningSockets()).toBe(socketsBefore)
// The fixture's own trees, by the name it gives them. The launch that failed leaves Playwright
// artifacts and a browser profile in here too, and those are Playwright's to clean, not the
// rollback's.
const left = (await readdir(temporaryRoot)).filter((entry) => entry.startsWith(SCRATCH_PREFIX))
expect(left).toEqual([])
}, 600_000)
})