test: cover browser cleanup for cron and subagents (#60146) (thanks @BrianWang1990)

This commit is contained in:
Peter Steinberger 2026-04-04 12:03:03 +01:00
parent e697838899
commit 3b09b58c5d
3 changed files with 103 additions and 0 deletions

View File

@ -153,6 +153,7 @@ Docs: https://docs.openclaw.ai
- Browser/host inspection: keep static Chrome inspection helpers out of the activated browser runtime so `openclaw doctor browser` and related checks do not eagerly load the bundled browser plugin. (#59471) Thanks @vincentkoc.
- Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like `ws://localhost.:...` rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.
- Browser/attach-only profiles: disconnect cached Playwright CDP sessions when stopping attach-only or remote CDP profiles, while still reporting never-started local managed profiles as not stopped. (#60097) Thanks @pedh.
- Browser/task cleanup: close tracked browser tabs and best-effort browser processes when cron-isolated agents and subagents finish, so background browser runs stop leaking orphaned sessions. (#60146) Thanks @BrianWang1990.
- Agents/output sanitization: strip namespaced `antml:thinking` blocks from user-visible text so Anthropic-style internal monologue tags do not leak into replies. (#59550) Thanks @obviyus.
- Kimi Coding/tools: normalize Anthropic tool payloads into the OpenAI-compatible function shape Kimi Coding expects so tool calls stop losing required arguments. (#59440) Thanks @obviyus.
- Image tool/paths: resolve relative local media paths against the agent `workspaceDir` instead of `process.cwd()` so inputs like `inbox/receipt.png` pass the local-path allowlist reliably. (#57222) Thanks Priyansh Gupta.

View File

@ -17,6 +17,10 @@ const lifecycleEventMocks = vi.hoisted(() => ({
emitSessionLifecycleEvent: vi.fn(),
}));
const browserMaintenanceMocks = vi.hoisted(() => ({
closeTrackedBrowserTabsForSessions: vi.fn(async () => 0),
}));
vi.mock("../tasks/task-executor.js", () => ({
completeTaskRunByRunId: taskExecutorMocks.completeTaskRunByRunId,
failTaskRunByRunId: taskExecutorMocks.failTaskRunByRunId,
@ -27,6 +31,10 @@ vi.mock("../sessions/session-lifecycle-events.js", () => ({
emitSessionLifecycleEvent: lifecycleEventMocks.emitSessionLifecycleEvent,
}));
vi.mock("../plugin-sdk/browser-maintenance.js", () => ({
closeTrackedBrowserTabsForSessions: browserMaintenanceMocks.closeTrackedBrowserTabsForSessions,
}));
vi.mock("./subagent-registry-helpers.js", async () => {
const actual = await vi.importActual<typeof import("./subagent-registry-helpers.js")>(
"./subagent-registry-helpers.js",
@ -58,6 +66,7 @@ describe("subagent registry lifecycle hardening", () => {
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
browserMaintenanceMocks.closeTrackedBrowserTabsForSessions.mockClear();
mod = await import("./subagent-registry-lifecycle.js");
});
@ -164,4 +173,86 @@ describe("subagent registry lifecycle hardening", () => {
expect(entry.cleanupCompletedAt).toBeTypeOf("number");
expect(persist).toHaveBeenCalled();
});
it("cleans up tracked browser sessions before subagent cleanup flow", async () => {
const persist = vi.fn();
const entry = createRunEntry({
expectsCompletionMessage: false,
});
const runSubagentAnnounceFlow = vi.fn(async () => true);
const controller = mod.createSubagentRegistryLifecycleController({
runs: new Map([[entry.runId, entry]]),
resumedRuns: new Set(),
subagentAnnounceTimeoutMs: 1_000,
persist,
clearPendingLifecycleError: vi.fn(),
countPendingDescendantRuns: () => 0,
suppressAnnounceForSteerRestart: () => false,
shouldEmitEndedHookForRun: () => false,
emitSubagentEndedHookForRun: vi.fn(async () => {}),
notifyContextEngineSubagentEnded: vi.fn(async () => {}),
resumeSubagentRun: vi.fn(),
captureSubagentCompletionReply: vi.fn(async () => "final completion reply"),
runSubagentAnnounceFlow,
warn: vi.fn(),
});
await expect(
controller.completeSubagentRun({
runId: entry.runId,
endedAt: 4_000,
outcome: { status: "ok" },
reason: SUBAGENT_ENDED_REASON_COMPLETE,
triggerCleanup: true,
}),
).resolves.toBeUndefined();
expect(browserMaintenanceMocks.closeTrackedBrowserTabsForSessions).toHaveBeenCalledWith({
sessionKeys: [entry.childSessionKey],
onWarn: expect.any(Function),
});
expect(runSubagentAnnounceFlow).toHaveBeenCalledWith(
expect.objectContaining({
childSessionKey: entry.childSessionKey,
}),
);
});
it("skips browser cleanup when steer restart suppresses cleanup flow", async () => {
const entry = createRunEntry({
expectsCompletionMessage: false,
});
const runSubagentAnnounceFlow = vi.fn(async () => true);
const controller = mod.createSubagentRegistryLifecycleController({
runs: new Map([[entry.runId, entry]]),
resumedRuns: new Set(),
subagentAnnounceTimeoutMs: 1_000,
persist: vi.fn(),
clearPendingLifecycleError: vi.fn(),
countPendingDescendantRuns: () => 0,
suppressAnnounceForSteerRestart: () => true,
shouldEmitEndedHookForRun: () => false,
emitSubagentEndedHookForRun: vi.fn(async () => {}),
notifyContextEngineSubagentEnded: vi.fn(async () => {}),
resumeSubagentRun: vi.fn(),
captureSubagentCompletionReply: vi.fn(async () => "final completion reply"),
runSubagentAnnounceFlow,
warn: vi.fn(),
});
await expect(
controller.completeSubagentRun({
runId: entry.runId,
endedAt: 4_000,
outcome: { status: "ok" },
reason: SUBAGENT_ENDED_REASON_COMPLETE,
triggerCleanup: true,
}),
).resolves.toBeUndefined();
expect(browserMaintenanceMocks.closeTrackedBrowserTabsForSessions).not.toHaveBeenCalled();
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
});
});

View File

@ -12,12 +12,14 @@ const {
loadConfigMock,
fetchWithSsrFGuardMock,
runCronIsolatedAgentTurnMock,
closeTrackedBrowserTabsForSessionsMock,
} = vi.hoisted(() => ({
enqueueSystemEventMock: vi.fn(),
requestHeartbeatNowMock: vi.fn(),
loadConfigMock: vi.fn(),
fetchWithSsrFGuardMock: vi.fn(),
runCronIsolatedAgentTurnMock: vi.fn(async () => ({ status: "ok" as const, summary: "ok" })),
closeTrackedBrowserTabsForSessionsMock: vi.fn(async () => 0),
}));
function enqueueSystemEvent(...args: unknown[]) {
@ -59,6 +61,10 @@ vi.mock("../cron/isolated-agent.js", () => ({
runCronIsolatedAgentTurn: runCronIsolatedAgentTurnMock,
}));
vi.mock("../plugin-sdk/browser-maintenance.js", () => ({
closeTrackedBrowserTabsForSessions: closeTrackedBrowserTabsForSessionsMock,
}));
import { buildGatewayCronService } from "./server-cron.js";
function createCronConfig(name: string): OpenClawConfig {
@ -80,6 +86,7 @@ describe("buildGatewayCronService", () => {
loadConfigMock.mockClear();
fetchWithSsrFGuardMock.mockClear();
runCronIsolatedAgentTurnMock.mockClear();
closeTrackedBrowserTabsForSessionsMock.mockClear();
});
it("routes main-target jobs to the scoped session for enqueue + wake", async () => {
@ -200,6 +207,10 @@ describe("buildGatewayCronService", () => {
sessionKey: "project-alpha-monitor",
}),
);
expect(closeTrackedBrowserTabsForSessionsMock).toHaveBeenCalledWith({
sessionKeys: ["project-alpha-monitor"],
onWarn: expect.any(Function),
});
} finally {
state.cron.stop();
}