diff --git a/src/agents/bash-tools.exec.pty-cleanup.test.ts b/src/agents/bash-tools.exec.pty-cleanup.test.ts index 015d97a47..323fe2f35 100644 --- a/src/agents/bash-tools.exec.pty-cleanup.test.ts +++ b/src/agents/bash-tools.exec.pty-cleanup.test.ts @@ -47,13 +47,20 @@ test("exec disposes PTY listeners after normal exit", async () => { test("exec tears down PTY resources on timeout", async () => { const disposeData = vi.fn(); const disposeExit = vi.fn(); - const kill = vi.fn(); + let exitListener: ((event: { exitCode: number; signal?: number }) => void) | undefined; + const kill = vi.fn(() => { + // Mirror real PTY behavior: process exits shortly after force-kill. + exitListener?.({ exitCode: 137, signal: 9 }); + }); ptySpawnMock.mockImplementation(() => ({ pid: 0, write: vi.fn(), onData: () => ({ dispose: disposeData }), - onExit: () => ({ dispose: disposeExit }), + onExit: (listener: (event: { exitCode: number; signal?: number }) => void) => { + exitListener = listener; + return { dispose: disposeExit }; + }, kill, })); diff --git a/src/agents/openclaw-tools.subagents.steer-failure-clears-suppression.test.ts b/src/agents/openclaw-tools.subagents.steer-failure-clears-suppression.test.ts index 6ab4e9860..5b77b6732 100644 --- a/src/agents/openclaw-tools.subagents.steer-failure-clears-suppression.test.ts +++ b/src/agents/openclaw-tools.subagents.steer-failure-clears-suppression.test.ts @@ -1,24 +1,21 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { callGatewayMock, setSubagentsConfigOverride, } from "./openclaw-tools.subagents.test-harness.js"; +import { + addSubagentRunForTests, + listSubagentRunsForRequester, + resetSubagentRegistryForTests, +} from "./subagent-registry.js"; import "./test-helpers/fast-core-tools.js"; - -let createOpenClawTools: (typeof import("./openclaw-tools.js"))["createOpenClawTools"]; -let addSubagentRunForTests: (typeof import("./subagent-registry.js"))["addSubagentRunForTests"]; -let listSubagentRunsForRequester: (typeof import("./subagent-registry.js"))["listSubagentRunsForRequester"]; -let resetSubagentRegistryForTests: (typeof import("./subagent-registry.js"))["resetSubagentRegistryForTests"]; +import { createSubagentsTool } from "./tools/subagents-tool.js"; describe("openclaw-tools: subagents steer failure", () => { - beforeEach(async () => { - vi.resetModules(); - ({ createOpenClawTools } = await import("./openclaw-tools.js")); - ({ addSubagentRunForTests, listSubagentRunsForRequester, resetSubagentRegistryForTests } = - await import("./subagent-registry.js")); + beforeEach(() => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); const storePath = path.join( @@ -58,13 +55,9 @@ describe("openclaw-tools: subagents steer failure", () => { return {}; }); - const tool = createOpenClawTools({ + const tool = createSubagentsTool({ agentSessionKey: "agent:main:main", - agentChannel: "discord", - }).find((candidate) => candidate.name === "subagents"); - if (!tool) { - throw new Error("missing subagents tool"); - } + }); const result = await tool.execute("call-steer", { action: "steer", diff --git a/src/discord/monitor.tool-result.sends-status-replies-responseprefix.test.ts b/src/discord/monitor.tool-result.sends-status-replies-responseprefix.test.ts index 69d7d8fd0..f9a7ae752 100644 --- a/src/discord/monitor.tool-result.sends-status-replies-responseprefix.test.ts +++ b/src/discord/monitor.tool-result.sends-status-replies-responseprefix.test.ts @@ -124,43 +124,6 @@ function createCategoryGuildClient() { } describe("discord tool result dispatch", () => { - it("sends status replies with responsePrefix", async () => { - const cfg = { - ...BASE_CFG, - messages: { responsePrefix: "PFX" }, - channels: { discord: { dmPolicy: "open", allowFrom: ["*"], dm: { enabled: true } } }, - } as ReturnType; - - const runtimeError = vi.fn(); - const handler = await createDmHandler({ cfg, runtimeError }); - const client = createDmClient(); - - await handler( - { - message: { - id: "m1", - content: "/status", - channelId: "c1", - timestamp: new Date().toISOString(), - type: MessageType.Default, - attachments: [], - embeds: [], - mentionedEveryone: false, - mentionedUsers: [], - mentionedRoles: [], - author: { id: "u1", bot: false, username: "Ada" }, - }, - author: { id: "u1", bot: false, username: "Ada" }, - guild_id: null, - }, - client, - ); - - expect(runtimeError).not.toHaveBeenCalled(); - expect(sendMock).toHaveBeenCalledTimes(1); - expect(sendMock.mock.calls[0]?.[1]).toMatch(/^PFX /); - }, 30_000); - it("caches channel info lookups between messages", async () => { const cfg = { ...BASE_CFG, diff --git a/src/discord/monitor/provider.rest-proxy.test.ts b/src/discord/monitor/provider.rest-proxy.test.ts index 17037b54f..d91169a1b 100644 --- a/src/discord/monitor/provider.rest-proxy.test.ts +++ b/src/discord/monitor/provider.rest-proxy.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { resolveDiscordRestFetch } from "./rest-fetch.js"; const { undiciFetchMock, proxyAgentSpy } = vi.hoisted(() => ({ undiciFetchMock: vi.fn(), @@ -31,9 +32,7 @@ describe("resolveDiscordRestFetch", () => { } as const; undiciFetchMock.mockReset().mockResolvedValue(new Response("ok", { status: 200 })); proxyAgentSpy.mockReset(); - - const { __testing } = await import("./provider.js"); - const fetcher = __testing.resolveDiscordRestFetch("http://proxy.test:8080", runtime); + const fetcher = resolveDiscordRestFetch("http://proxy.test:8080", runtime); await fetcher("https://discord.com/api/v10/oauth2/applications/@me"); @@ -54,9 +53,7 @@ describe("resolveDiscordRestFetch", () => { error: vi.fn(), exit: vi.fn(), } as const; - const { __testing } = await import("./provider.js"); - - const fetcher = __testing.resolveDiscordRestFetch("bad-proxy", runtime); + const fetcher = resolveDiscordRestFetch("bad-proxy", runtime); expect(fetcher).toBe(fetch); expect(runtime.error).toHaveBeenCalled(); diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index b37d79a4e..c7c54ec90 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -7,7 +7,6 @@ import { } from "@buape/carbon"; import type { GatewayPlugin } from "@buape/carbon/gateway"; import { Routes } from "discord-api-types/v10"; -import { ProxyAgent, fetch as undiciFetch } from "undici"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js"; import type { HistoryEntry } from "../../auto-reply/reply/history.js"; @@ -29,7 +28,6 @@ import type { OpenClawConfig, ReplyToMode } from "../../config/config.js"; import { loadConfig } from "../../config/config.js"; import { danger, logVerbose, shouldLogVerbose, warn } from "../../globals.js"; import { formatErrorMessage } from "../../infra/errors.js"; -import { wrapFetchWithAbortSignal } from "../../infra/fetch.js"; import { createDiscordRetryRunner } from "../../infra/retry-policy.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { createNonExitingRuntime, type RuntimeEnv } from "../../runtime.js"; @@ -67,6 +65,7 @@ import { createDiscordNativeCommand, } from "./native-command.js"; import { resolveDiscordPresenceUpdate } from "./presence.js"; +import { resolveDiscordRestFetch } from "./rest-fetch.js"; export type MonitorDiscordOpts = { token?: string; @@ -118,26 +117,6 @@ function dedupeSkillCommandsForDiscord( return deduped; } -function resolveDiscordRestFetch(proxyUrl: string | undefined, runtime: RuntimeEnv): typeof fetch { - const proxy = proxyUrl?.trim(); - if (!proxy) { - return fetch; - } - try { - const agent = new ProxyAgent(proxy); - const fetcher = ((input: RequestInfo | URL, init?: RequestInit) => - undiciFetch(input as string | URL, { - ...(init as Record), - dispatcher: agent, - }) as unknown as Promise) as typeof fetch; - runtime.log?.("discord: rest proxy enabled"); - return wrapFetchWithAbortSignal(fetcher); - } catch (err) { - runtime.error?.(danger(`discord: invalid rest proxy: ${String(err)}`)); - return fetch; - } -} - async function deployDiscordCommands(params: { client: Client; runtime: RuntimeEnv; diff --git a/src/discord/monitor/rest-fetch.ts b/src/discord/monitor/rest-fetch.ts new file mode 100644 index 000000000..55cd5ff0a --- /dev/null +++ b/src/discord/monitor/rest-fetch.ts @@ -0,0 +1,27 @@ +import { ProxyAgent, fetch as undiciFetch } from "undici"; +import { danger } from "../../globals.js"; +import { wrapFetchWithAbortSignal } from "../../infra/fetch.js"; +import type { RuntimeEnv } from "../../runtime.js"; + +export function resolveDiscordRestFetch( + proxyUrl: string | undefined, + runtime: RuntimeEnv, +): typeof fetch { + const proxy = proxyUrl?.trim(); + if (!proxy) { + return fetch; + } + try { + const agent = new ProxyAgent(proxy); + const fetcher = ((input: RequestInfo | URL, init?: RequestInit) => + undiciFetch(input as string | URL, { + ...(init as Record), + dispatcher: agent, + }) as unknown as Promise) as typeof fetch; + runtime.log?.("discord: rest proxy enabled"); + return wrapFetchWithAbortSignal(fetcher); + } catch (err) { + runtime.error?.(danger(`discord: invalid rest proxy: ${String(err)}`)); + return fetch; + } +}