mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
* 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>
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
/**
|
|
* Dedicated, serial project for the runner integration suite.
|
|
*
|
|
* `runner.integration.test.ts` and `lateRunnerWebhook.integration.test.ts`
|
|
* start real detached runner/session process trees against the isolated
|
|
* temporary hub. They must never run inside the default parallel unit-test
|
|
* suite (see the exclude in `vitest.config.ts`); run them explicitly with:
|
|
*
|
|
* bun run test:integration # serial runner lifecycle coverage
|
|
* bun run test:integration:stress # + the 20-session stress test
|
|
*
|
|
* The files run one after another in a single worker
|
|
* (`fileParallelism: false`) so resource ownership stays unambiguous and the
|
|
* suite-level registry cleanup (see `src/test/processRegistry.ts`) is
|
|
* authoritative.
|
|
*/
|
|
import { defineConfig } from 'vitest/config'
|
|
import { resolve } from 'node:path'
|
|
|
|
export default defineConfig({
|
|
test: {
|
|
globals: false,
|
|
environment: 'node',
|
|
include: [
|
|
'src/runner/runner.integration.test.ts',
|
|
'src/runner/lateRunnerWebhook.integration.test.ts',
|
|
],
|
|
globalSetup: './src/test/globalSetup.ts',
|
|
setupFiles: './src/test/setup.ts',
|
|
// Real detached process trees: never parallelize this suite.
|
|
fileParallelism: false,
|
|
// beforeEach starts a real runner (state-file wait can exceed the
|
|
// default 5s hook budget on slow machines); afterEach runs the
|
|
// two-stage cleanup + marker sweep, which can take longer on hosts
|
|
// with slow process teardown.
|
|
testTimeout: 20_000,
|
|
hookTimeout: 60_000,
|
|
coverage: {
|
|
provider: 'v8',
|
|
reporter: ['text', 'json', 'html'],
|
|
exclude: [
|
|
'node_modules/**',
|
|
'dist/**',
|
|
'**/*.d.ts',
|
|
'**/*.config.*',
|
|
'**/mockData/**',
|
|
],
|
|
},
|
|
},
|
|
resolve: {
|
|
alias: {
|
|
'@': resolve('./src'),
|
|
},
|
|
},
|
|
})
|