From a9b5fe409958633bb0422c6443e20d1bdfb1c501 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 19:35:27 +0000 Subject: [PATCH] test: tighten system event and lsof helper coverage --- src/infra/ports-lsof.test.ts | 67 +++++++++++++++++++++++++++++++++ src/infra/system-events.test.ts | 54 +++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/infra/ports-lsof.test.ts diff --git a/src/infra/ports-lsof.test.ts b/src/infra/ports-lsof.test.ts new file mode 100644 index 00000000000..eb599112a5a --- /dev/null +++ b/src/infra/ports-lsof.test.ts @@ -0,0 +1,67 @@ +import fs from "node:fs"; +import fsPromises from "node:fs/promises"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { resolveLsofCommand, resolveLsofCommandSync } from "./ports-lsof.js"; + +const LSOF_CANDIDATES = + process.platform === "darwin" + ? ["/usr/sbin/lsof", "/usr/bin/lsof"] + : ["/usr/bin/lsof", "/usr/sbin/lsof"]; + +describe("lsof command resolution", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("prefers the first executable async candidate", async () => { + const accessSpy = vi.spyOn(fsPromises, "access").mockImplementation(async (target) => { + if (target === LSOF_CANDIDATES[0]) { + return; + } + throw new Error("unexpected"); + }); + + await expect(resolveLsofCommand()).resolves.toBe(LSOF_CANDIDATES[0]); + expect(accessSpy).toHaveBeenCalledTimes(1); + }); + + it("falls through async candidates before using the shell fallback", async () => { + const accessSpy = vi.spyOn(fsPromises, "access").mockImplementation(async (target) => { + if (target === LSOF_CANDIDATES[0]) { + throw new Error("missing"); + } + if (target === LSOF_CANDIDATES[1]) { + return; + } + throw new Error("unexpected"); + }); + + await expect(resolveLsofCommand()).resolves.toBe(LSOF_CANDIDATES[1]); + expect(accessSpy).toHaveBeenCalledTimes(2); + + accessSpy.mockImplementation(async () => { + throw new Error("missing"); + }); + await expect(resolveLsofCommand()).resolves.toBe("lsof"); + }); + + it("mirrors candidate resolution for the sync helper", () => { + const accessSpy = vi.spyOn(fs, "accessSync").mockImplementation((target) => { + if (target === LSOF_CANDIDATES[0]) { + throw new Error("missing"); + } + if (target === LSOF_CANDIDATES[1]) { + return undefined; + } + throw new Error("unexpected"); + }); + + expect(resolveLsofCommandSync()).toBe(LSOF_CANDIDATES[1]); + expect(accessSpy).toHaveBeenCalledTimes(2); + + accessSpy.mockImplementation(() => { + throw new Error("missing"); + }); + expect(resolveLsofCommandSync()).toBe("lsof"); + }); +}); diff --git a/src/infra/system-events.test.ts b/src/infra/system-events.test.ts index 0b92aa36568..cf16416e210 100644 --- a/src/infra/system-events.test.ts +++ b/src/infra/system-events.test.ts @@ -3,7 +3,15 @@ import { drainFormattedSystemEvents } from "../auto-reply/reply/session-updates. import type { OpenClawConfig } from "../config/config.js"; import { resolveMainSessionKey } from "../config/sessions.js"; import { isCronSystemEvent } from "./heartbeat-runner.js"; -import { enqueueSystemEvent, peekSystemEvents, resetSystemEventsForTest } from "./system-events.js"; +import { + drainSystemEventEntries, + enqueueSystemEvent, + hasSystemEvents, + isSystemEventContextChanged, + peekSystemEventEntries, + peekSystemEvents, + resetSystemEventsForTest, +} from "./system-events.js"; const cfg = {} as unknown as OpenClawConfig; const mainKey = resolveMainSessionKey(cfg); @@ -56,6 +64,50 @@ describe("system events (session routing)", () => { expect(second).toBe(false); }); + it("normalizes context keys when checking for context changes", () => { + const key = "agent:main:test-context"; + expect(isSystemEventContextChanged(key, " build:123 ")).toBe(true); + + enqueueSystemEvent("Node connected", { + sessionKey: key, + contextKey: " BUILD:123 ", + }); + + expect(isSystemEventContextChanged(key, "build:123")).toBe(false); + expect(isSystemEventContextChanged(key, "build:456")).toBe(true); + expect(isSystemEventContextChanged(key)).toBe(true); + }); + + it("returns cloned event entries and resets duplicate suppression after drain", () => { + const key = "agent:main:test-entry-clone"; + enqueueSystemEvent("Node connected", { + sessionKey: key, + contextKey: "build:123", + }); + + const peeked = peekSystemEventEntries(key); + expect(hasSystemEvents(key)).toBe(true); + expect(peeked).toHaveLength(1); + peeked[0].text = "mutated"; + expect(peekSystemEvents(key)).toEqual(["Node connected"]); + + expect(drainSystemEventEntries(key).map((entry) => entry.text)).toEqual(["Node connected"]); + expect(hasSystemEvents(key)).toBe(false); + + expect(enqueueSystemEvent("Node connected", { sessionKey: key })).toBe(true); + }); + + it("keeps only the newest 20 queued events", () => { + const key = "agent:main:test-max-events"; + for (let index = 1; index <= 22; index += 1) { + enqueueSystemEvent(`event ${index}`, { sessionKey: key }); + } + + expect(peekSystemEvents(key)).toEqual( + Array.from({ length: 20 }, (_, index) => `event ${index + 3}`), + ); + }); + it("filters heartbeat/noise lines, returning undefined", async () => { const key = "agent:main:test-heartbeat-filter"; enqueueSystemEvent("Read HEARTBEAT.md before continuing", { sessionKey: key });