mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
9384ce031b9cea30c19cccf0ced97e116ee3fdbb
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2b63091421 |
fix(runner): adopt untracked runner webhooks only from the reporting process generation (#1943)
* feat(cli): report the CLI process start marker in session metadata The session webhook only names hostPid. A PID alone does not tell the runner which process generation reported it: once the CLI exits, the OS can hand the same PID to any other process, including another runner-spawned CLI. Add metadata.hostStartMarker, the reporting process's own start marker (getProcessStartMarker(process.pid), the same probe the runner uses for its resume records and the stop-session generation check). buildSessionMetadata is the only place that sets hostPid, so every bootstrap path (fresh, existing, lazy, shared Codex) reports it. The runner uses it to decide whether an untracked runner webhook may be adopted (decideUntrackedRunnerWebhook). * fix(runner): adopt untracked runner webhooks only from the reporting process generation Since #1911 the runner adopts an untracked `startedBy: 'runner'` webhook (runner restart before the webhook; Claude has no HAPI id on argv) and persists getProcessStartMarker(pid) of whatever currently owns hostPid, so that a later stopSession can tree-kill that generation. If the webhook is late - the reporting CLI has exited and the OS has already reused its PID - the persisted marker belongs to a foreign process, possibly another runner-spawned CLI. The generation check in stopSession then passes and the foreign process tree is killed. The webhook now has to prove the generation itself: the CLI reports its own marker as metadata.hostStartMarker and decideUntrackedRunnerWebhook adopts only when it matches the live PID. A marker that names another generation, or a failed probe, is ignored without killing anything, also on the webhook-timeout path: a proven mismatch means the PID is not the ghost. A webhook without a marker (older CLI) is left untracked instead of adopted blindly; the timeout kill for those CLIs is unchanged. The integration test drives a real runner over its control server: a webhook carrying the live marker is adopted, listed and stoppable; a webhook with a stale or missing marker leaves the PID untracked and alive, and stop-session reports unknown. Follow-up to #1911 (#1910). * fix(cli): probe the process start marker only for runner-started sessions buildSessionMetadata probed getProcessStartMarker(process.pid) on every bootstrap, terminal starts included. The runner reads metadata.hostStartMarker only in the untracked `startedBy: 'runner'` path (decideUntrackedRunnerWebhook); a terminal webhook never consumes it, and neither do hub or web. The probe is a synchronous spawn: `ps` on POSIX, `tasklist` plus a PowerShell Get-CimInstance call on Windows, each with a 10 s timeout. A terminal `hapi` start on Windows therefore blocked on a PowerShell cold start before the session row existed, for a value nobody reads. With shared Codex the probe also ran per /new and /fork on the event loop shared by all sibling roots. Set hostStartMarker only when startedBy is 'runner'; that is the one case the runner evaluates. This narrows the earlier commit: every bootstrap path reports the marker for runner-started sessions, not for terminal ones. * fix(codex): keep hostStartMarker in the shared runtime's runner webhook The shared Codex runtime bootstraps its roots with reportStarted: false and reports them to the runner itself, from root.session.getMetadata(). That is the hub's view of the metadata: the hub parses stored metadata through MetadataSchema, so a hub that predates hostStartMarker strips the field and the webhook arrives without it. The hub's update-metadata merge never returns it either, so the loss lasts for the root's lifetime. With the generation check, an untracked runner-started webhook without a marker is ignored instead of adopted. A runner restart (for example the version handoff of a single-host upgrade, where the hub process keeps running the old code) followed by /new or /fork in a long-lived shared root therefore left the root unlisted and untracked, the very situation the adoption from #1911 addresses. The Claude and lazy bootstrap paths are unaffected: they report the local metadata. Take the marker from the local bootstrap metadata instead. The opt-in runtime integration test models the stripped hub view in its session mock and asserts the reported payload still carries the marker. The mock also gains the `on` subscription that SharedCodexRoot requires since #1911, without which the suite's bind test could not run. * test(runner): run the late-webhook suite in the serial integration project lateRunnerWebhook.integration.test.ts starts a real runner and a detached child process per case, but the default vitest project picked it up and `bun run test` ran it in a parallel worker. Tests that spawn runner process trees belong to the serial integration project (vitest.integration.config.ts, CI job `integration`), as the config comments and .github/workflows/test.yml require. Include the file in the integration project, exclude it from the default one, and remove the temporary HAPI_HOME of each case, which was left behind in the temp directory. --------- Co-authored-by: Benedict Groppe <195190800+BGGBTAC@users.noreply.github.com> |
||
|
|
b7e892b99e | fix(release): sync iOS and Android versions | ||
|
|
291e7bc40b |
fix(test): stop runner integration suite from leaking detached process trees (#1515) (#1521)
* fix(test): stop runner integration suite from leaking detached process trees (#1515) The default CLI test run included runner.integration.test.ts, which spawns real detached runner/session process trees. A failing, timed-out, or interrupted test (or a plain runner stop) left those trees alive under PID 1 — on the Mac this accumulated ~600 Node/Bun/agent processes and several GiB of RSS over repeated runs. Test harness changes only; production runner session-preservation semantics are untouched: - Exclude runner.integration.test.ts from the default parallel unit-test suite; move it into a dedicated serial integration project (vitest.integration.config.ts, 'bun run test:integration'). The 20-session stress test is opt-in via HAPI_RUN_STRESS_TESTS=true. - Add a test-owned process/session registry (processRegistry.ts): every runner, runner-spawned session, and terminal-style child is registered immediately after spawn; afterEach/afterAll run two-stage cleanup (logical stopRunnerSession first, then bounded process-tree kill), followed by a marker sweep for agent grandchildren reparented to PID 1. - Add a per-run HAPI_TEST_MARKER env stamp + identity/secret env neutralization for test children (integrationEnv.ts) so outer HAPI/pi session variables never leak into test processes and the final audit can recognize test-owned processes by env alone. - Final suite audit in globalSetup teardown: reap anything still carrying the run marker and fail with PID/command diagnostics if anything cannot be reaped, before removing the temp home. - Regression coverage: a deliberately failing test registers a detached child and the follow-up audit must find zero test-owned processes. - CI: replace the dead .env.integration-test step with a dedicated integration job running the serial project. * refactor(test): drop unused killByChildProcess import and child field from registry * chore(test): raise integration hookTimeout to 60s for slow teardown hosts * fix(test): fail loudly when the process-table audit cannot scan; assert regression child death Bot review #1521 findings: - A failed `ps` scan (unsupported flags, buffer exhaustion, permissions) previously returned [] and silently disabled both teardown audit layers. It now throws; globalSetup teardown catches the scan error into the audit error (temp home is still removed) so the run fails visibly. - The regression audit test cleaned the leak with the reaper before asserting, and force-killed the fresh marked runner. The failing test's direct child PID is now asserted dead in afterEach right after registry cleanup (before the marker sweep), and the audit test stops its own runner gracefully before reaping. * fix(test): bound the logical cleanup phase so a hung runner cannot stall the hook Bot review #1521: stopRunnerSession carries the worker's 60s HTTP timeout (setup.ts raises HAPI_RUNNER_HTTP_TIMEOUT for the stress test), and the integration hook timeout is also 60s — N sequential stops could exhaust the hook budget before the process-tree fallback and marker sweep ran, recreating the very leak this change prevents. Logical shutdown is now parallel (Promise.allSettled over all tracked sessions) and the whole phase (stops + PID resolution) races against a 15s budget, so stage-2 tree-kill and the marker sweep always get their share of the hook window. * fix(test): bound graceful runner stop in hooks; keep credentials out of audit diagnostics Bot review #1521 (follow-up): - stopRunner()'s HTTP stop can burn the worker-wide 60s timeout on a hung-but-live runner, starving the marker sweep within the hook budget. afterEach/afterAll now race the graceful stop against a 10s bound; a runner that does not stop in time is force-reaped by the sweep (it carries the run marker) and the next beforeEach's alive-PID guard ignores any stale state file. - The env-bearing ps scan (ps eww) was also used for diagnostics, so the first 500 chars of a short-command process could print inherited credentials (CLI_API_TOKEN etc.) into teardown error logs. The scan now only identifies marked PIDs; command lines are fetched separately without 'e', falling back to '(command unavailable)' instead of the env dump. * fix(test): reap runner model-probe orphans before the zero-survivor inspection Bot review #1521 (Minor): inspect-before-reap. Applying it exposed a real race: each test's runner legitimately spawns marker-carrying children at startup (agent acp + agent --list-models model-catalog probes). Stopping the runner orphans them (ppid 1) with the run marker, so the audit test's OWN runner polluted the pure inspection with fresh probes spawned after the failing test's sweep window. - reapTestOwnedProcesses now re-kills every re-scan iteration instead of killing once and only re-scanning, so a process that survived its first SIGKILL (mid-exec) or spawned mid-kill is not given a free pass. - The regression audit test stops its runner, reaps (clearing its own legitimate orphan probes), then inspects: anything still marked is a genuine survivor the bounded reaper could not remove and fails the suite. Killable leaks from the failing test are already asserted dead in afterEach before the sweep runs. * fix(test): strictly bound the marker reaper; make per-test sweep unconditional and verified Bot review #1521 (follow-up): - The 10s reap deadline did not bound the awaited per-tree kills: each killProcessTreeByPid can wait up to 2s per PID, so several stuck processes could still exceed the 60s hook budget. Every process in a test-owned tree carries the marker (env is inherited), so tree-walking is unnecessary: the reaper now SIGKILLs every marked PID found by each scan, fire-and-forget, and re-scans every 250ms — the deadline strictly bounds the function. - The per-test sweep was skipped when the direct-child assertion failed first, and its survivors were ignored. afterEach now snapshots the regression-child state BEFORE the unconditional sweep, then verifies both the registry result and the sweep leftovers. * fix(test): replace it.fails regression with a direct assertion test Bot review #1521 (Minor): Vitest applies the it.fails expected-failure inversion after afterEach, so a broken registry assertion inside the hook would be masked as an expected failure, and the marker sweep would erase the evidence before the follow-up audit ran. The regression is now a normal test that registers a detached child at spawn time, deliberately performs NO per-test teardown, runs only the spawn-time registered cleanup, and asserts the child PID is dead. The afterEach no longer carries the registry-leak assertion (moved into the test body where it cannot be inverted); the per-test sweep assertion and the final audit test are unchanged. * fix(test): bound registry stage-2 tree-kills; require live regression fixture Bot review #1521 (follow-up): - Stage-2 killProcessTreeByPid awaits per descendant serially and can consume the whole 60s hook for a large/stuck tree. Signals are all delivered synchronously (children first) before any waiting, so racing the awaits against a 5s budget bounds the phase without skipping any kill; waitForAllDead still verifies the outcome. - The regression test could pass vacuously if its fixture exited during the startup delay (the registry exit listener would remove it before cleanup). It now asserts the child is alive before running cleanup. * fix(test): kill registered roots with bare synchronous SIGKILL, no pgrep walk Bot review #1521 (follow-up): racing the mapped killProcessTreeByPid calls against a timer does not bound the phase — evaluating the map invokes each call immediately, and each runs the recursive synchronous pgrep walk before its first await, which can consume the hook before the timer, runner stop, or marker sweep run. Stage-2 now SIGKILLs registered roots directly (fire-and-forget, no tree walk, no per-PID waits) and waits a bounded 5s for death. Descendants are reaped by the unconditional marker sweep immediately afterward — every descendant inherits the run marker, so tree-walking is unnecessary. * fix(test): drop duplicate process-death wait in registry cleanup Bot review #1521 (Minor): the duplicated waitForAllDead delayed the authoritative marker sweep by another 5s under the exact stuck-process condition the harness must handle. Keep the single bounded wait; the afterEach marker sweep remains the guarantee. |
||
|
|
df35a8c523 | fix(test): isolate integration tests from production hub via temp hub globalSetup (#734) | ||
|
|
b4654acb92 | init |