Repository navigation
test(cli): runServe() takes a kernel-assigned port and returns the bound one; the released-port pin widens to every CLI spawn - #24386
Merged
objectstack-fleet[bot] merged 4 commits intoOct 18, 2026
Conversation
…e bound port (WIP) Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…wn; unconverted files ledgered for slice 2 (WIP) Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…pe in pieces Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…-dev-column-port Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Oct 18, 2026
Contributor
Author
席位核对ACCEPT (lane review) ·
Generated by Claude Code |
objectstack-fleet
Bot
deleted the
claude/issue-24342-s1-dev-column-port
branch
October 18, 2026 19:45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #24342
Clause-②: no
Basis for the Clause-② line: every changed file is under
packages/cli/test/**, the CLI's test harness.@objectstack/clipublishesdist,README.mdandCHANGELOG.md(itsfiles[]). Measured afterpnpm --filter @objectstack/cli build: the symbols this PR adds or spreads (RELEASED_DRAW_LEDGER,cliSpawners,SLICE_2,KERNEL_ASSIGNED_PORT,readKernelAssignedPort) occur in 0 of those files. The positive controlchildPortEnv, which comes fromsrc/, occurs in 2. Nothing published moves, so this PR carries theskip-changesetlabel and no changeset.This is slice 1 of #24342, the
runServe()family. The card stays open for slice 2: the 25 files listed under "What slice 2 converts" below.Why the card is split
Triage
6251398054: "If the change is over 1,000 lines, split it by file family, as the card says."bin/run.js --devfiles.runServe(). The bespoke spawners and thebin/run.js --devfiles are slice 2.What changed
runServe()(test/helpers/serve-process.ts):--portnow gets--port KERNEL_ASSIGNED_PORT(0). The child's ownlisten()picks the port, so no released number crosses a gap.ServeRungainsport: the port the child bound, read back from its own banner. It isundefinedwhen the run ended without a complete banner (a boot that died, or awaitForthat matched an earlier line).--portkeeps both old checks,portContentionError()andportDriftError(). The held-port arm ofserve-port-readbackdepends on that.--port 0there is no number to drift from, so the check becomes the read-back.runServe()rejects a complete banner that names no non-zerohttp://localhostport, throughreadKernelAssignedPort(). That covers an unreadableAPI:row (anOS_AUTH_URL/BETTER_AUTH_URL/OS_BASE_URLorigin) and an announcedlocalhost:0. A boot with no complete banner gets no verdict, as before.runServe()caller files, every draw removed (21 draws):serve-app-runtime-hooks.e2e(1),serve-boot-diagnostics-noise-budget.e2e(1),serve-boot-diagnostics.e2e(2),serve-cluster-driver-diagnosis.e2e(1),serve-no-artifact.e2e(5),serve-organizations-host-resolution.e2e(3),serve-organizations-mount-failure.e2e(4),serve-package-declared-capabilities.test(1),serve-port-readback.e2e(1).serve-package-registry-always-on.e2e(2): itsrunServe()call, and also its ownos serve --devboot, so the whole file leaves the ledger. The boot reads its port back and each of its two boots builds its own base URL.serve-port-readback.e2e.runServe()resolves with aportgreater than 0 that equals the banner's read-back.readKernelAssignedPort()'s answers: a complete banner,localhost:0, and an unreadable or missing banner.released-port-production-spawn.pin.test.ts). Its population widens from "namesbin/run.js" to "spawns the CLI". A file is in it when its code does any of these:bin/run.jsorbin/run-dev.js, as one string or as segments in one call;CLIorrunServefrom the harness, by name, under an alias, or as a namespace member.TSXalone does not count, because it runs whatever script it is handed.RELEASED_DRAW_LEDGER, since an entry is no longer "outside the production column". It holds every unconverted file with its exact draw count and the reasonslice 2 of #24342: 25 files, 28 draws. The four oldOUTSIDE_THE_PRODUCTION_COLUMNentries are among them. The pin is green.runServecaller, an aliasedCLI, a namespaceCLI,TSXalone, and a local non-harnessCLI. The source entry is now in the population.runServe+(is spelled in pieces inside the pin.vitest-tiers.tsreads that shape in code position, string literals included, and would otherwise move the pin to theintegrationtier. Measured: every changed test file keeps its tier from the base commit.reservePort(),randomPort(),boundPortFromBanner()andportDriftError()now say what may still draw: instruments, and the slice-2 files while they stay in the ledger.Measured before writing it (A1 to A3)
git grep -l "reservePort\|randomPort"names 39 files at6a49efb9c, andgit grep -l "runServe("names 21. A TypeScript AST census, counting calls rather than lines, splits the 37 drawing test files like this:runServe(): 21 draws, all converted here.CLIthemselves (bespoke tsx spawners): 24 draws, slice 2.bin/run.js --devoros dev: 4 draws, slice 2.serve-port-bind-probe7,serve-probe-child-attribution1). They stay outside the population.runServe(hits: 10 are the callers above. 4 draw but namerunServeonly in prose (package-install-local-handlers,serve-migrate-exit-schema-parity,serve-port-drift-notice,serve-process-child-env), so they are bespoke spawners and slice 2. 2 define a local function of that name and draw nothing. 3 are production files cli test harness: reservePort()'s close-to-spawn race reached main — the hourly full run went red on start-mcp-stdio-early-initialize (port 45195 taken beforeos startbound it); 46 test files share the draw #24295 already converted. The other 2 are the helper and the pin.runServe()shape (tsxserve --port 0): every converted boot reached its banner with a non-zerohttp://localhostport. One reading: port 44449.runServe()caller needs the number before the spawn.runServe()stops the child oncewaitFormatches, and no caller addresses the port afterwards. The one caller that passes a number, the held-port arm, holds it on purpose.Reverse verification (one-time, not kept)
scripts/ablation-replace.mjs(WRAP mode):serve-boot-diagnostics.e2e'srunServe(dir, ['--log-level', 'debug'], …)became['--port', randomPort(), '--log-level', 'debug'].Tests 1 failed | 3 passed (4), withserve-boot-diagnostics.e2e.test.ts: 1 released-port draw(s) (line 112), ledger allows 0.CLI, so only the newrunServeleg of the population could catch it.e97518ea9587),git diff HEADempty.git statusclean):runServe(dir, [], …)resolved with port 44449.OS_BASE_URL=https://app.example.comrejected withCANNOT READ BACK THE BOUND PORT of \os serve (bin/run-dev.js, via runServe)`: it was asked for port 0 …`.serve-port-readback's held-port arm hands an explicit held port torunServe(), and the boot is rejected withPORT DRIFT. That is the red this card describes. The kernel-assigned positive control in the same run resolves. Both pass (16/16).Tests
All on head
98af344ee, after mergingorigin/main5da2e8881; closure rebuilt withturbo run build --filter='@objectstack/cli...'.vitest run --maxWorkers=2):released-port-production-spawn.pin4/4 (projectunit)serve-package-declared-capabilities5/5OS_TEST_TIERS=nightly):serve-port-readback16/16serve-boot-diagnostics2/2serve-boot-diagnostics-noise-budget3/3serve-app-runtime-hooks1/1serve-cluster-driver-diagnosis8/8serve-no-artifact5/5serve-organizations-host-resolution3/3serve-organizations-mount-failure4/4serve-package-registry-always-on5/5188625089.pnpm --filter @objectstack/cli typecheck: exit 0. The test layer holds its 3-file / 28-error ledger.tsc --listFiles -p tsconfig.test.jsonputs all 12 changed files in the program, with 0 errors in them.pnpm exec eslint --no-inline-configover the 12 changed files: exit 0.Gates (local subset, head
98af344ee)node scripts/pm/dispatch-gates.mjs --commandsderives 52 commands. Each ran with its exit code captured before any pipe, and all 52 exited 0.--ranreconciliation:Run reconciliation — 52 derived, 52 run, 0 NOT-MEASURED, 0 UNRUN.check:init-service-contract,check:live-db-isolation,check:meta-type-normalized,check:optional-error-sink,check:resume-authority-declared,check:route-envelope,check:runner-env-posture,check:settings-bind-window,check:startup-registry-verdict,check:verify-stand-in,check:wildcard-fallthrough): all 11 exited 0.check:cli-test-child-env(red once on cli test harness: reservePort()'s close-to-spawn race reached main — the hourly full run went red on start-mcp-stdio-early-initialize (port 45195 taken beforeos startbound it); 46 test files share the draw #24295) printed155 spawner source(s) among 305 under packages/cli/test/**; no new bulk process.env copy reaches a spawned child, all 189 spawn call(s) declare their child's env, exit 0.pnpm lintand the CI farm are CI's, read from this PR's check-runs.What slice 2 converts (25 files, 28 draws, each ledgered as
slice 2 of #24342)portDriftError()and nameportContentionError()on exit (15 files). Spawn onKERNEL_ASSIGNED_PORT, settle onreadKernelAssignedPort(out, what), carry the port on the handle, and build each boot's base URL from it. Several of these reuse one drawn port across 2 to 8 boots.artifact-flag-precedence.integrationpackage-install-local-{boot-steps,handlers,hooks,jobs,jobs-pull,jobs-shared-name,uninstall-cleanups}.integrationpackage-restart-acceptance.integrationserve-config-boot-flat-docs.integrationserve-config-boot-residual.integration(2: plusbootRefused)serve-host-config-metadata-door.integration(2)serve-organization-record-page.e2eserve-view-container-read.integrationserve-runtime-assets-branding-warning.e2e,start-signal-forwarding.e2e.runServe()shape (3):serve-app-anchored-optional-import.e2e(runServeFrom)serve-process-child-env.e2e(probeOrigin's default draw; itsreadyVerdictalready handles a requested 0)serve-migrate-exit-schema-parity.integration(migrate-and-exit)serve-port-drift-notice.e2e's NEGATIVE boot.--port 0never shifts, becausegetAvailablePort(0)answers 0.serve-publishes-bound-port.e2e(2).bin/run.js --dev/os dev(4):dev-boot-output-order.integration: a pty command line that reads no port.serve-mcp-capability-collision.e2e,serve-mcp-stdio-answers.e2e,serve-stdio-stdout-purity.e2e: a module-level drawnport, a localboundPort()and a localportDriftError(). Convert by reading the first boot's port back and dropping the local drift function.Acceptance notes
os serve --port 0printsWARN Port 0 is in use, using port N insteadin the ready banner's Boot diagnostics block, at the default log level.serve --port 0againstexport default {};: one such line at the default level and one atinfo, beside a banner that names the real port.plugin-hono-server'skernel:listeninghook, which compares the requested port (0) with the bound one. Port 0 is a legal request (utils/port-contract.ts,MIN_PORT = 0), not a busy port.runServe()'s new default is a default. A test of the port doors themselves (OS_PORT,PORT, the built-in default) has to pass its port explicitly, and the docblock says so. No caller does that today: all 21 call sites passed--port.main, not in this PR'sTest Core, so their readings above are this machine's.Generated by Claude Code