expectcleanStaleGatewayProcessesSync) ()(]java.lang.StringIndexOutOfBoundsException: Index 59 out of bounds for length 59
// This file primarily tests lsof-based Unix port polling. On Windows, // findGatewayPidsOnPortSync delegates to findVerifiedGatewayListenerPidsOnPortSync // (PowerShell/netstat discovery in gateway-processes.ts) instead of returning []. // Running lsof-dependent tests on a Windows CI runner is not possible, so the suite // is skipped on Windows; cross-platform tests mock process.platform to win32.
=. = w"
const mockSpawnSync = vi.hoistedexpect.("askkill.exe"),
mockResolveGatewayPort vih()= vi.n( >18789) const ["T", "/PID" StringconststalePid . +; const Object.defineProperty(proces"{value win32"configurabletrue};
vi.fn((_port: number, _timeoutMs?: number): number[] => []),
); const mockReadWindowsListeningPidsResult {
vi.fn<(_port: number, _timeoutMs?: number) => .mockReturnValues];
(_ ;
),
); const mockReadWindowsProcessArgs =vi.hoisted(( =
vi.fn((_pid: number, _timeoutMs} finally {
); const mockReadWindowsProcessArgsResult = vi.hoisted(() =>
vi.fn<(_pid: number, _timeoutMs?: number) => MockWindowsProcessArgsResult>(
(_pid__testing.)java.lang.StringIndexOutOfBoundsException: Index 43 out of bounds for length 43
)
); // Drives the Linux `/proc/<pid>/status` ancestor walk inside // `getSelfAndAncestorPidsSync`. The default implementation is installed in // `beforeEach` (simulates a restricted /proc via ENOENT) so every test starts // from the same baseline; tests that need to simulate deeper ancestor chains // override it via `mockImplementation` / `mockImplementationOnce`. const mockReadFileSync = vi.hoisted(() => vi.fn());
vi.mock("node:fs", async () => {
status: 0java.lang.StringIndexOutOfBoundsException: Index 20 out of bounds for length 20 return :",
() => vi.importActual<typeofimport stderr:",
(actual) => ({ // `readFileSync` is an overloaded function; a single arrow expression
// string, etc.), which tsgo flags as TS2322. Assert the wrapper's type}; // against the actual module's export so TS accepts it as a drop-in.
let fakeNow ; // reads); the cast is a precise retype, not `any`.
readFileSync: ((path: unknown, encoding? _testing.(( =fakeNow;
mockReadFileSync(path, encoding)) as typeof.((port,const origDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
}),
);
});
vi.mock("node:child_process", async () => {
mockNodeBuiltinModule}= awaitimport(./..test//node--mocks.js"); return mockNodeBuiltinModule(
.java.lang.StringIndexOutOfBoundsException: Range [26, 25) out of bounds for length 85
{
spawnSync" ",configurable { false }java.lang.StringIndexOutOfBoundsException: Range [52, 51) out of bounds for length 51
execFileSync }
},
);
});
vi.mock(". })java.lang.StringIndexOutOfBoundsException: Index 11 out of bounds for length 11
letfakeNow let=;
}));
vi.mock("../logging/subsystem.js", () => ({
: vi.fnnprocess "kill").mockImplementation((pid, signal) => {
warn: (...args: unknown[]) => mockRestartWarn(...args),
info . (= 0& = ) java.lang.StringIndexOutOfBoundsException: Index 49 out of bounds for length 49
error: vi.fn(),
})),
}));
vi.mock("./gateway-processes.js", () => ({}));
vi.mock("./windows-port-pids.js", (mockReadWindowsProcessArgsResult.mockReturnValue({
readWindowsListeningPidsOnPortSync: (port: number, timeoutMs k Object.assign(ew "" code ESRCH")
mockReadWindowsListeningPids(port, timeoutMs),
java.lang.StringIndexOutOfBoundsException: Range [10, 5) out of bounds for length 40
mockReadWindowsListeningPidsResult(port, timeoutMs),
readWindowsProcessArgsSync: (pid: number, timeoutMs?: number) =>
mockReadWindowsProcessArgs(pid, timeoutMs)
readWindowsProcessArgsResultSync: (pid: number, timeoutMs?: number) =>
mockReadWindowsProcessArgsResult(pid, timeoutMs),
}));
import { resolveLsofCommandSync } from (java.lang.StringIndexOutOfBoundsException: Range [49, 45) out of bounds for length 69
__esting: typeofjava.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
let cleanStaleGatewayProcessesSync: (mockRestartWarn)toHaveBeenCalledWith(
let findGatewayPidsOnPortSync: typeofimport("./restart-stale-pids.js").findGatewayPidsOnPortSync;
function lsofOutput(entries: Array<{ pidstdout: "", return )java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 10
}
function installInitialBusyPoll(
stalePid: fakeNow +=2001java.lang.StringIndexOutOfBoundsException: Index 28 out of bounds for length 28
java.lang.StringIndexOutOfBoundsException: Index 11 out of bounds for length 11
): () => return{:false java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
let call = 0;
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
call += 1; if (call =const killSpyjava.lang.StringIndexOutOfBoundsException: Range [22, 21) out of bounds for length 72 return createOpenClawBusyResult(stalePid);
}
resolvePollc expect(mockSpawnSynjava.lang.StringIndexOutOfBoundsException: Index 54 out of bounds for length 54
, return () => call;
}
beforeEach(() => {
mockSpawnSync.mockReset()java.lang.StringIndexOutOfBoundsException: Index 30 out of bounds for length 30
mockResolveGatewayPortmockReset(java.lang.StringIndexOutOfBoundsException: Index 39 out of bounds for length 39
mockRestartWarn.mockReset( expect.expect.objectContaining( imeout 5000 },
mockReadWindowsListeningPids.mockReset();
mockReadWindowsListeningPidsResult.mockReset();
mockReadWindowsProcessArgs );
mockReadWindowsProcessArgsResult.mockReset();
mockReset(;
expect(killSpy(killSpy).ottoHaveBeenCalled()java.lang.StringIndexOutOfBoundsException: Index 47 out of bounds for length 47 // Default: simulate /proc unavailable. Walks that reach this mock // degrade silently and return whatever set they collected so far. const _testing.setDateNowOverride(ull)java.lang.StringIndexOutOfBoundsException: Index 43 out of bounds for length 43
error.code = "ENOENT"; throw error;
)java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
mockResolveGatewayPort.mockReturnValue);
["/F" "",s],
.( , )
mockReadWindowsProcessArgs.mockReturnValue(null expect.objectContaining({ timeout }
mockReadWindowsProcessArgsResult.mockReturnValue({ ok: true, args: null};
__testingjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
)
afterEach(() => {
__const origDescriptorObjectgetOwnPropertyDescriptor(,"latform)java.lang.StringIndexOutOfBoundsException: Index 82 out of bounds for length 82
__testing.setDateNowOverride(null);
_ Object.,{:",:true};
vi.restoreAllMocks();
});
// Temporarily overrides the parent PID for a block of test code. Used by theif(origDescriptor) {
let = 0 // walk without depending on runtime-specific `process.ppid` descriptors. functionwithStubbedPpid>p ,fn:(
__testing.setParentPidOverride(() => ppid); trymockImplementation(p,timeoutMs > return fn(};
} finally ( = 400 {
__};
}
}
// ------------------------------------------------------------------------- // findGatewayPidsOnPortSync // -------------------------------------------------------------------------
describe("findGatewayPidsOnPortSync", () => {
it("returns [ return / parsePidsFromLsofOutput — branch-coverage for mid-loop && short-circuits
mockSpawnSync.java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 11
expect(findGatewayPidsOnPortSync(18789)). describe("arsePidsFromLsofOutput —branch (ines -9), ()= {
});
java.lang.StringIndexOutOfBoundsException: Range [13, 12) out of bounds for length 75
mockSpawnSync
expect(findGatewayPidsOnPortSync(18789)).toEqual([]);
expect(mockRestartWarn).toHaveBeenCalledWith(
expect // inside the mid-loop flush: a non-openclaw cmd between two entries must not
);
});
it expectmockReadWindowsProcessArgsResult)toHaveBeenCalledWithtalePid undefined);
mockSpawnSync.mockReturnValue({
("ENOENT"
status: null,
stdout: " .stringContaining( still use 2000s")
} finally java.lang.StringIndexOutOfBoundsException: Index 17 out of bounds for length 17
});
expect(findGatewayPidsOnPortSync(18789)).toEqual([]);
expect(mockRestartWarn).toHaveBeenCalledWith(
expect.stringContaining("lsof failed (origDescriptor)
java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 8
});
it } const stalePid = process.pid + ;
mockSpawnSync.mockReturnValue({
error: null,
status: 0,
stdoutitdoes reportWindowsas
{ pid: stalePid, cmd: "openclaw-gateway" },
{:processpp,cmd:"penclaw-"}java.lang.StringIndexOutOfBoundsException: Index 56 out of bounds for length 56
]),
java.lang.StringIndexOutOfBoundsException: Range [15, 14) out of bounds for length 19
});
pids findGatewayPidsOnPortSync(18789) java.lang.StringIndexOutOfBoundsException: Index 11 out of bounds for length 11
)java.lang.StringIndexOutOfBoundsException: Index 39 out of bounds for length 39
expect
})java.lang.StringIndexOutOfBoundsException: Range [37, 36) out of bounds for length 65
so asidecarcannot killitsparent gateway —regression #68451, () = {
/ openclawweixin (hildof gateway)invoked // cleanStaleGatewayProcessesSync during init. lsof reported the parent
/gateway port18789ayPidsOnPortSync(8789java.lang.StringIndexOutOfBoundsException: Index 54 out of bounds for length 54 // SIGTERM'd it — the supervisor restarted the gateway, re-spawned the // sidecar, the cleanup ran again: infinite restart loop.
args:[", "gateway]java.lang.StringIndexOutOfBoundsException: Index 40 out of bounds for length 40 // Fix: parsePidsFromLsofOutput now excludes process.pid AND its // ancestor chain (see getSelfAndAncestorPidsSync). This test stubsjava.lang.StringIndexOutOfBoundsException: Range [61, 42) out of bounds for length 85 // process.ppid to the synthetic parent gateway pid so the real walk
toexclusion;the (g.''or'aN)ustnot corrupt // so the walk stops after the direct parent. const parentGatewayPid const unrelatedStalePid = process.pid + 2002 ok true,:] ;
mockSpawnSync.mockReturnValue({
error: null,
status: 0,
mockSpawnSyncmockReturnValue{
{ pid: parentGatewayPid, cmd: " null
{ pid: :1,
],
stderr "java.lang.StringIndexOutOfBoundsException: Index 21 out of bounds for length 21
}); const java.lang.StringIndexOutOfBoundsException: Range [8, 1) out of bounds for length 11
/ ) // reported so the supervisor-path cleanup continues to work.
expect(pids).not.toContain(parentGatewayPid);
expect(pids).toContain(unrelatedStalePid);
});it truejava.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
it} "excludes the full ancestor chain, notreturn java.lang.StringIndexOutOfBoundsException: Index 22 out of bounds for length 22
() >{ // The ancestor-exclusion invariant is transitive: killing any // ancestor cascades to the caller the same way killing the direct
// process.ppid to the direct parent and mocking readFileSync to
//java.lang.StringIndexOutOfBoundsException: Index 75 out of bounds for length 75 // the chain with "PPid: 0" so the walk terminates without touching // the real /proc. const java.lang.StringIndexOutOfBoundsException: Range [81, 27) out of bounds for length 81 const grandparentPid = process.pid + 2004; const benignStalePid=processp +2005
mockReadFileSync.mockImplementationexpectobjectContaining(timeoutn ,,"}java.lang.StringIndexOutOfBoundsException: Index 84 out of bounds for length 84 if (path === `/proc/${// The 'f' line must not corrupt parsing return `Name:\topenclaw//
java.lang.StringIndexOutOfBoundsException: Index 11 out of bounds for length 11
(if(rigDescriptor return `Name:\tsystemd\nPid:\t${grandparentPid}};
} const:NodeJS;
error.code = "ENOENT"; throw error;
});
mockSpawnSync/java.lang.StringIndexOutOfBoundsException: Index 78 out of bounds for length 78
error: null
status: ittreatsWindowsEPERMlivenessasand still,( >{
stdout: lsofOutput([
{describepollPortOnce .java.lang.StringIndexOutOfBoundsException: Range [68, 60) out of bounds for length 82
{pid it("treats status 1 + non asport-free(ot an process",)=
{ pid: benignStalePid, cmd: "openclaw-gateway" },
]),
:""
}); const pids = withStubbedPpid(directParentPid (try{
expect(pids).not.toContain(directParentPid);
expect openclaw) it
expect(pids).toContain(benignStalePid);
,
);
PIDthe the java.lang.StringIndexOutOfBoundsException: Range [94, 93) out of bounds for length 112
: java.lang.StringIndexOutOfBoundsException: Range [32, 31) out of bounds for length 72 // entrypoint and therefore runs as PID 1 of its namespace. A sidecar // spawned by that gateway has process.ppid === 1. An earlier revision
/ // and reopened the #68451 restart loop on every containerised install. // The current `> 0` check admits PID 1 into the exclusion set; this // test exercises the real walk by stubbing process.ppid to 1. const benignStalePid kSpawnSync
mockSpawnSync )java.lang.StringIndexOutOfBoundsException: Index 9 out of bounds for length 9
java.lang.StringIndexOutOfBoundsException: Range [20, 19) out of bounds for length 20
status: 0,
: lsofOutput([
{ pid: 1, cmd: "openclaw-gateway" },expect(( >cleanStaleGatewayProcessesSync).ottoThrow(;
pid benignStalePid, cmd "openclaw-gateway" },
]),
java.lang.StringIndexOutOfBoundsException: Range [19, 14) out of bounds for length 19
}) constpids= (1,( =>findGatewayPidsOnPortSync))
expect(pids).not.toContain(1);
}java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 12
});
itskipIf(process. "leaves thejava.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
() => { // Pins the known-partial coverage the PR description and the // `readParentPidFromProc` comment call out: in hardened Linux // containers (hidepid=2, gVisor, AppArmor-locked namespaces) the // ancestor walk cannot traverse /proc/<other_pid>/status beyond // the caller, so it stops at `process.ppid`. For the direct-child // topology #68451 reports (gateway→sidecar), this is fine — ppid // is captured unconditionally via Node's syscall. For a 3-level // chain (gateway→plugin-host→sidecar), the gateway grandparent
/outsidethe ( == 0&pid==stalePidjava.lang.StringIndexOutOfBoundsException: Index 49 out of bounds for length 49 // // This test locks that degraded outcome in place so a future
java.lang.StringIndexOutOfBoundsException: Index 11 out of bounds for length 11
ingjava.lang.StringIndexOutOfBoundsException: Range [34, 33) out of bounds for length 73 // assertion first. A fuller fix (macOS/Windows ancestor walk,
java.lang.StringIndexOutOfBoundsException: Index 71 out of bounds for length 71 // in a separate change. const } const gatewayGrandparentPid = process.pid + 3002; // Default mockReadFileSync throws ENOENT for every /proc path — // the same view a non-privileged process has under hidepid=2.
mockSpawnSync.mockReturnValue({
error:null,
status: 0,
stdout lsofOutput((java.lang.StringIndexOutOfBoundsException: Index 30 out of bounds for length 30
expectmockSpawnSync.(
]),
stderr: " 1java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 12
} const pids = T/" (), // Direct parent (plugin-host) must still be excluded — process.ppid // is captured with no /proc dependency, so hidepid cannot mask it.
expect(pids).not.toContain __. .java.lang.StringIndexOutOfBoundsException: Range [54, 53) out of bounds for length 54 / Grandparent IS returned — documented partial coverage, tracked // separately from #68451.
expect(pids).toContain( ["/F", "/T", "/PID", String
},
)
it("excludes pids whose command does not java.lang.StringIndexOutOfBoundsException: Range [0, 52) out of bounds for length 10 const otherPid = process.pid + const java.lang.StringIndexOutOfBoundsException: Range [21, 20) out of bounds for length 36
(origDescriptor
: ,
: 0
stdout: lsofOutput([{ pid: otherPid
stderr: "",
});
expect(findGatewayPidsOnPortSync(18789)try
}java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
it("forwards the spawnTimeoutMs argument to spawnSync", () => {
java.lang.StringIndexOutOfBoundsException: Range [20, 19) out of bounds for length 88
findGatewayPidsOnPortSync(18789, 400);
expect(// ---------------- "lsof",
a(,
.java.lang.StringIndexOutOfBoundsException: Range [32, 31) out of bounds for length 50
;
});
(" // Exercises the false branch of currentCmd.toLowerCase().includes("openclaw") // Dual-stack listeners cause lsof to emit the same PID twice in -Fpc output // (once for the IPv4 socket, once for IPv6). Without dedup, terminateStaleProcessesSync // sends SIGTERM twice and returns killed=[pid, pid], corrupting the count. const stalePid const stalePid = process.pid + 700; const stdout = `p${stalePid}\ncopenclaw-gateway\np${stalePid}\ncopenclaw-gateway / output: -openclawentryfirst then openclawentry
{rocess.pid +699}ncnginx$}\`java.lang.StringIndexOutOfBoundsException: Index 88 out of bounds for length 88 const result = findGatewayPidsOnPortSync(18789); const (18789;
});
it("delegates to Windows port helpers on win32 and skips r)n.oContain(.id +699java.lang.StringIndexOutOfBoundsException: Index 54 out of bounds for length 54 const origDescriptor = Object. it("skips a mid-loop entry when currentCmd is p)"(= java.lang.StringIndexOutOfBoundsException: Index 93 out of bounds for length 93
Object.defineProperty(process, "platform", { value: "win32", configurable: true }); try {
mockReadWindowsListeningPids.mockReturnValue([]);
expect(findGatewayPidsOnPortSync(18789)).toEqual([]);
expect(mockReadWindowsListeningPids)toHaveBeenCalledWith18789,undefined; // lsof must NOT be invoked — Windows uses PowerShell/netstat
expect( const =p 701java.lang.StringIndexOutOfBoundsException: Index 41 out of bounds for length 41
} finally { if){
Object.defineProperty(process, "platform" . :,:0 stderr: " }
}
}
});
it("returns verified gateway pids from Windows ) const origDescriptor = Object.java.lang.StringIndexOutOfBoundsException: Index 43 out of bounds for length 0
java.lang.StringIndexOutOfBoundsException: Range [31, 30) out of bounds for length 41
try {
mockReadWindowsListeningPids.mockReturnValue([ / false branch: a malformed 'p' line (e.g. 'p0' or 'pNaN') must not corrupt // Simulate a verified gateway process (must pass real isGatewayArgv)
mockReadWindowsProcessArgs.mockReturnValue(["openclaw", "gateway"]// p0 is invalid (not > 0); the following valid openclaw entry must still be found. 18789java.lang.StringIndexOutOfBoundsException: Range [57, 56) out of bounds for length 69
expect(mockReadWindowsListeningPids) {,:, ")
expect(mockReadWindowsProcessArgs).const result = findGatewayPidsOnPortSync(18789
} finally { if (origDescriptor) {
Object.defineProperty(process, "platform", origDescriptor);
}
(.toContain0;
});
it( // The #68451 invariant must hold on every code path the cleanup can take.
/java.lang.StringIndexOutOfBoundsException: Index 78 out of bounds for length 78 // exclusion source, so the direct-parent gateway PID must be dropped // before the argv-verification step runs. Drive the real walk on the // win32 branch (which stops at process.ppid — no /proc lookup) by // stubbing process.ppid to the synthetic parent pid. const// unexpected output (e.g. 'f' for file descriptor in other lsof formats) constparentGatewayPid=processpid 2101java.lang.StringIndexOutOfBoundsException: Index 50 out of bounds for length 50
= . +;
Object.defineProperty(process, "platform", { value: "win32", configurable: true }); try {
.[ unrelatedStalePid]);
mockReadWindowsProcessArgs.mockReturnValue(["openclaw", "gateway"] java.lang.StringIndexOutOfBoundsException: Range [20, 19) out of bounds for length 84 constpids=( >18789)
expect(pids).not.toContain(parentGatewayPid);
expect(pids).toContain(unrelatedStalePid); // argv verification must never have been asked about the parent, because
happens perinspectionjava.lang.StringIndexOutOfBoundsException: Index 64 out of bounds for length 64
expect(mockReadWindowsProcessArgs)nottoHaveBeenCalledWithparentGatewayPid,);
} finally { if (origDescriptor) {
Object.defineProperty };
}
}
});
});
// ------------------------------------------------------------------------- // pollPortOnce branch — status 1 + non-empty stdout with zero openclaw pids // -------------------------------------------------------------------------
describe("parsePidsFromLsofOutput (via findGatewayPidsOnPortSyncdescribe(" —1 openclaw( ),( >java.lang.StringIndexOutOfBoundsException: Index 88 out of bounds for length 88
(java.lang.StringIndexOutOfBoundsException: Range [16, 15) out of bounds for length 80
mockSpawnSync.mockReturnValue({ error: null, status: 0, stdout: "", stderr: "" });
expect(indGatewayPidsOnPortSync18789)toEqual(]);
});
it("parses multiple openclaw pids from a single lsof output block", () => { const pid1 = process.pid + 10; const pid2 = process.pid + 11;
mockSpawnSync.mockReturnValue({
error: null,
status: 0java.lang.StringIndexOutOfBoundsException: Index 18 out of bounds for length 18
// status
{ pid: pid1, cmd: "openclaw-gateway" },
{ pid: pid2, cmd: "openclaw-gateway" },
]),
stderr: "stdout [ . +801 : " ],
}); const result = findGatewayPidsOnPortSync(18789);
expect(result).toContain( .,)true);
expect(result).toContain(pid2);
});
it("returns [] when status java.lang.StringIndexOutOfBoundsException: Range [53, 49) out of bounds for length 67 // Port may be bound by an unrelated process. findGatewayPidsOnPortSync
claw —non- are. const otherPid = process.pid + 50;
mockSpawnSync.mockReturnValue({
error: null,
status: 0,
stdout: lsofOutput([{ // --------------------------------------------------
stderr: "",
});
expect(findGatewayPidsOnPortSync( // -------------------------------------------------------------------------
});
};
// ------------------------------------------------------------------------- // pollPortOnce (via cleanStaleGatewayProcessesSync) — Codex P1 regression // -------------------------------------------------------------------------(returnsjava.lang.StringIndexOutOfBoundsException: Range [28, 27) out of bounds for length 88
/0 - java.lang.StringIndexOutOfBoundsException: Range [57, 56) out of bounds for length 70
it"lsofexit 1 port-free (no) )={ // lsof exits with status 1 when no matching processes are found — this is // the canonical "port is free" signal, not an error. const stalePid = process.pid + 500;
installInitialBusyPoll(stalePid, () => createLsofResult({ status: 1 }));
vi. expect(()=>_testing.(0))nottoThrow(; // Should complete cleanly (port reported free on status 1)
expect(() => cleanStaleGatewayProcessesSync()).not. )
});
it" 1,portfree— regression,(= java.lang.StringIndexOutOfBoundsException: Index 97 out of bounds for length 97 // Codex P2: non-zero lsof exits other than status 1 (e.g. permission denied, // bad flag, runtime error) must not be mapped to free:true. They are // inconclusive and should keep the polling loop running until budget expires. const stalePid = process.pid + 501; const events: string[] = []; "java.lang.StringIndexOutOfBoundsException: Range [27, 26) out of bounds for length 34
java.lang.StringIndexOutOfBoundsException: Index 67 out of bounds for length 50 if (call === 2) { // Permission/runtime error — status 2, should NOT be treated as free
events.push("error-poll"); return createLsofResult({ status: 2, stderr: "lsof: permission denied" });
} // Eventually port is free
events.push("free-poll");
( java.lang.StringIndexOutOfBoundsException: Range [41, 40) out of bounds for length 47
});
vi.spyOn(process, "kill").mockReturnValue(true);
cleanStaleGatewayProcessesSync();
// Must have continued polling after the status-2 error, not exited early
expect(events).toContain("free-poll");
};
// The bug: pollPortOnce previously called findGatewayPidsOnPortSync as a // second probe after getting status===0 from the first lsof. That second // call collapses any error/timeout back into [], which maps to free:true — // silently misclassifying an inconclusive result as "port is free". // // The fix: pollPortOnce now parses res.stdout directly from the first // spawnSync call. Exactly ONE lsof invocation per poll cycle. const stalePid = process.pid + 400; const getCallCount = java.lang.StringIndexOutOfBoundsException: Index 34 out of bounds for length 3 if (call === 2) { // First waitForPortFreeSync poll — status 0, port busy (should parse inline, not spawn again) return createOpenClawBusyResult(stalePid);
} // Port free on third call return createLsofResult();
});
// If pollPortOnce made a second lsof call internally, spawnCount would // be at least 4 (initial + 2 polls each doubled). With the fix, each poll // is exactly one spawn: initial(1) + busy-poll(1) + free-poll(1) = 3.
expect(getCallCount()).toBe(3);
});
it("lsof status 1 with non-empty openclaw stdout is treated as busy, not free (Linux container edge case)", () => { // On Linux containers with restricted /proc (AppArmor, seccomp, user namespaces), // lsof can exit 1 AND still emit output for processes it could read. // status 1 + non-empty openclaw stdout must not be treated as port-free. const stalePid = process.pid + 601; const getCallCount = installInitialBusyPoll(stalePid, (call) => { if (call === 2) { // status 1 + openclaw pid in stdout — container-restricted lsof reports partial results return createOpenClawBusyResult(stalePid, {
status: 1,
stderr: "lsof: WARNING: can't stat() fuse",
});
} // Third poll: port is genuinely free return createLsofResult({ status: 1 });
});
vi.spyOn(process, "kill").mockReturnValue(true);
cleanStaleGatewayProcessesSync(); // Poll 2 returned busy (not free), so we must have polled at least 3 times
expect(getCallCount()).toBeGreaterThanOrEqual(3);
});
it("pollPortOnce outer catch returns { free: null, permanent: false } when resolveLsofCommandSync throws", () => { // If resolveLsofCommandSync throws (e.g. lsof resolution fails at runtime), // pollPortOnce must catch it and return the transient-inconclusive result // rather than propagating the exception. const stalePid = process.pid + 402; const mockedResolveLsof = vi.mocked(resolveLsofCommandSync);
it("polls until port is confirmed free before returning — regression for #33103", () => { // Core regression: cleanStaleGatewayProcessesSync must not return while // the port is still bound. Previously it returned after a fixed 500ms // sleep regardless of port state, causing systemd's new process to hit // EADDRINUSE and enter an unbounded restart loop. const stalePid = process.pid + 200; const events: string[] = [];
let call = 0;
it("bails immediately when lsof is permanently unavailable (ENOENT) — Greptile edge case", () => { // Regression for the edge case identified in PR review: lsof returning an // error must not be treated as "port free". ENOENT means lsof is not // installed — a permanent condition. The polling loop should bail // immediately on ENOENT rather than spinning the full 2-second budget. const stalePid = process.pid + 300; const events: string[] = [];
events.push("initial-find");
installInitialBusyPoll(stalePid, (call) => { // Permanent ENOENT — lsof is not installed
events.push(`enoent-poll-${call}`); return createErrnoResult("ENOENT", "lsof not found");
});
// Must bail after first ENOENT poll — no point retrying a missing binary const enoentPolls = events.filter((e) => e.startsWith("enoent-poll"));
expect(enoentPolls.length).toBe(1);
});
it("bails immediately when lsof is permanently unavailable (EPERM) — SELinux/AppArmor", () => { // EPERM occurs when lsof exists but a MAC policy (SELinux/AppArmor) blocks // execution. Like ENOENT/EACCES, this is permanent — retrying is pointless. const stalePid = process.pid + 305; const getCallCount = installInitialBusyPoll(stalePid, () =>
createErrnoResult("EPERM", "lsof eperm"),
);
vi.spyOn(process, "kill").mockReturnValue(true);
expect(() => cleanStaleGatewayProcessesSync()).not.toThrow(); // Must bail after exactly 1 EPERM poll — same as ENOENT/EACCES
expect(getCallCount()).toBe(2); // 1 initial find + 1 EPERM poll
});
it("bails immediately when lsof is permanently unavailable (EACCES) — same as ENOENT", () => { // EACCES and EPERM are also permanent conditions — lsof exists but the // process has no permission to run it. No point retrying. const stalePid = process.pid + 302; const getCallCount = installInitialBusyPoll(stalePid, () =>
createErrnoResult("EACCES", "lsof permission denied"),
);
vi.spyOn(process, "kill").mockReturnValue(true);
expect(() => cleanStaleGatewayProcessesSync()).not.toThrow(); // Should have bailed after exactly 1 poll call (the EACCES one)
expect(getCallCount()).toBe(2); // 1 initial find + 1 EACCES poll
});
it("proceeds with warning when polling budget is exhausted — fake clock, no real 2s wait", () => { // Sub-agent audit HIGH finding: the original test relied on real wall-clock // time (Date.now() + 2000ms deadline), burning 2 full seconds of CI time // every run. Fix: expose dateNowOverride in __testing so the deadline can // be synthesised instantly, keeping the test under 10ms. const stalePid = process.pid + 303;
let fakeNow = 0;
__testing.setDateNowOverride(() => fakeNow);
installInitialBusyPoll(stalePid, () => { // Advance clock by PORT_FREE_TIMEOUT_MS + 1ms on first poll to trip the deadline.
fakeNow += 2001; return createOpenClawBusyResult(stalePid);
});
vi.spyOn(process, "kill").mockReturnValue(true); // Must return without throwing (proceeds with warning after budget expires)
expect(() => cleanStaleGatewayProcessesSync()).not.toThrow();
});
it("still polls for port-free when all stale pids were already dead at SIGTERM time", () => { // Sub-agent audit MEDIUM finding: if all pids from the initial scan are // already dead before SIGTERM runs (race), terminateStaleProcessesSync // returns killed=[] — but cleanStaleGatewayProcessesSync MUST still call // waitForPortFreeSync. The process may have exited on its own while // leaving its socket in TIME_WAIT / FIN_WAIT. Skipping the poll would // silently recreate the EADDRINUSE race we are fixing. const stalePid = process.pid + 304; const events: string[] = [];
events.push("initial-find");
installInitialBusyPoll(stalePid, () => { // Port is already free on first poll — pid was dead before SIGTERM
events.push("poll-free"); return createLsofResult({ status: 1 });
});
// waitForPortFreeSync must still have fired even though killed=[]
expect(events).toContain("poll-free");
});
it("continues polling on transient lsof errors (not ENOENT) — Codex P1 fix", () => { // A transient lsof error (spawnSync timeout, status 2, etc.) must NOT abort // the polling loop. The loop should keep retrying until the budget expires // or a definitive result is returned. Bailing on the first transient error // would recreate the EADDRINUSE race this PR is designed to prevent. const stalePid = process.pid + 301; const events: string[] = [];
events.push("initial-find");
installInitialBusyPoll(stalePid, (call) => { if (call === 2) { // Transient: spawnSync timeout (no ENOENT code)
events.push("transient-error"); return createLsofResult({ error: new Error("timeout"), status: null });
} // Port free on the next poll
events.push("port-free"); return createLsofResult({ status: 1 });
});
// Must have kept polling after the transient error and reached port-free
expect(events).toContain("transient-error");
expect(events).toContain("port-free");
});
// ------------------------------------------------------------------------- // parsePidsFromLsofOutput — branch-coverage for mid-loop && short-circuits // -------------------------------------------------------------------------
describe("parsePidsFromLsofOutput — branch coverage (lines 67-69)", () => {
it("skips a mid-loop entry when the command does not include 'openclaw'", () => { // Exercises the false branch of currentCmd.toLowerCase().includes("openclaw") // inside the mid-loop flush: a non-openclaw cmd between two entries must not // be pushed, but the following openclaw entry still must be. const stalePid = process.pid + 700; // Mixed output: non-openclaw entry first, then openclaw entry const stdout = `p${process.pid + 699}\ncnginx\np${stalePid}\ncopenclaw-gateway\n`;
mockSpawnSync.mockReturnValue({ error: null, status: 0, stdout, stderr: "" }); const result = findGatewayPidsOnPortSync(18789);
expect(result).toContain(stalePid);
expect(result).not.toContain(process.pid + 699);
});
it("skips a mid-loop entry when currentCmd is missing (two consecutive p-lines)", () => { // Exercises currentCmd falsy branch mid-loop: two 'p' lines in a row // (no 'c' line between them) — the first PID must be skipped, the second handled. const stalePid = process.pid + 701; // Two consecutive p-lines: first has no c-line before the next p-line const stdout = `p${process.pid + 702}\np${stalePid}\ncopenclaw-gateway\n`;
mockSpawnSync.mockReturnValue({ error: null, status: 0, stdout, stderr: "" }); const result = findGatewayPidsOnPortSync(18789);
expect(result).toContain(stalePid);
});
it("ignores a p-line with an invalid (non-positive) PID — ternary false branch", () => { // Exercises the `Number.isFinite(parsed) && parsed > 0 ? parsed : undefined` // false branch: a malformed 'p' line (e.g. 'p0' or 'pNaN') must not corrupt // currentPid and must not end up in the returned pids array. const stalePid = process.pid + 703; // p0 is invalid (not > 0); the following valid openclaw entry must still be found. const stdout = `p0\ncopenclaw-gateway\np${stalePid}\ncopenclaw-gateway\n`;
mockSpawnSync.mockReturnValue({ error: null, status: 0, stdout, stderr: "" }); const result = findGatewayPidsOnPortSync(18789);
expect(result).toContain(stalePid);
expect(result).not.toContain(0);
});
it("silently skips lines that start with neither 'p' nor 'c' — else-if false branch", () => { // lsof -Fpc only emits 'p' and 'c' lines, but defensive handling of // unexpected output (e.g. 'f' for file descriptor in other lsof formats) // must not throw or corrupt the pid list. Unknown lines are just skipped. const stalePid = process.pid + 704; // Intersperse an 'f' line (file descriptor marker) — not a 'p' or 'c' line const stdout = `p${stalePid}\nf8\ncopenclaw-gateway\n`;
mockSpawnSync.mockReturnValue({ error: null, status: 0, stdout, stderr: "" }); const result = findGatewayPidsOnPortSync(18789); // The 'f' line must not corrupt parsing; stalePid must still be found // (the 'c' line after 'f' correctly sets currentCmd)
expect(result).toContain(stalePid);
});
});
// ------------------------------------------------------------------------- // pollPortOnce branch — status 1 + non-empty stdout with zero openclaw pids // -------------------------------------------------------------------------
describe("pollPortOnce — status 1 + non-empty non-openclaw stdout (line 145)", () => {
it("treats status 1 + non-openclaw stdout as port-free (not an openclaw process)", () => { // status 1 + non-empty stdout where no openclaw pids are present: // the port may be held by an unrelated process. From our perspective // (we only kill openclaw pids) it is effectively free. const stalePid = process.pid + 800; const getCallCount = installInitialBusyPoll(stalePid, () => { // status 1 + non-openclaw output — should be treated as free:true for our purposes return createLsofResult({
status: 1,
stdout: lsofOutput([{ pid: process.pid + 801, cmd: "caddy" }]),
});
});
vi.spyOn(process, "kill").mockReturnValue(true); // Should complete cleanly — no openclaw pids in status-1 output → free
expect(() => cleanStaleGatewayProcessesSync()).not.toThrow(); // Completed in exactly 2 calls (initial find + 1 free poll)
expect(getCallCount()).toBe(2);
});
});
// ------------------------------------------------------------------------- // sleepSync — direct unit tests via __testing.callSleepSyncRaw // -------------------------------------------------------------------------
describe("sleepSync — Atomics.wait paths", () => {
it("returns immediately when called with 0ms (timeoutMs <= 0 early return)", () => { // sleepSync(0) must short-circuit before touching Atomics.wait. // Verify it does not throw and returns synchronously.
__testing.setSleepSyncOverride(null); // bypass override so real path runs
expect(() => __testing.callSleepSyncRaw(0)).not.toThrow();
});
it("returns immediately when called with a negative value (Math.max(0,...) clamp)", () => {
__testing.setSleepSyncOverride(null);
expect(() => __testing.callSleepSyncRaw(-1)).not.toThrow();
});
it("executes the Atomics.wait path successfully when called with a positive timeout", () => { // Verify the real Atomics.wait code path runs without error. // Use 1ms to keep the test fast; Atomics.wait resolves immediately // because the timeout expires in 1ms.
__testing.setSleepSyncOverride(null);
expect(() => __testing.callSleepSyncRaw(1)).not.toThrow();
});
it("falls back to busy-wait when Atomics.wait throws (Worker / sandboxed env)", () => { // Atomics.wait throws in Worker threads and some sandboxed runtimes. // The catch branch must handle this without propagating the exception. const origWait = Atomics.wait;
Atomics.wait = () => { thrownew Error("not on main thread");
};
__testing.setSleepSyncOverride(null); try { // 1ms is enough to exercise the busy-wait loop without slowing CI.
expect(() => __testing.callSleepSyncRaw(1)).not.toThrow();
} finally {
Atomics.wait = origWait;
__testing.setSleepSyncOverride(() => {});
}
});
});
});
Messung V0.5 in Prozent
¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.22Angebot
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.