diff --git a/CHANGELOG.md b/CHANGELOG.md index f958d8d18..7d89d606b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Docs: https://docs.clawd.bot ## 2026.1.18-2 +### Fixes +- Tests: stabilize plugin SDK resolution and embedded agent timeouts. + ## 2026.1.17-6 ### Changes diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.appends-new-user-assistant-after-existing-transcript.test.ts b/src/agents/pi-embedded-runner.run-embedded-pi-agent.appends-new-user-assistant-after-existing-transcript.test.ts index cbd8d7619..275b51e0e 100644 --- a/src/agents/pi-embedded-runner.run-embedded-pi-agent.appends-new-user-assistant-after-existing-transcript.test.ts +++ b/src/agents/pi-embedded-runner.run-embedded-pi-agent.appends-new-user-assistant-after-existing-transcript.test.ts @@ -146,7 +146,7 @@ const readSessionMessages = async (sessionFile: string) => { }; describe("runEmbeddedPiAgent", () => { - it("appends new user + assistant after existing transcript entries", async () => { + it("appends new user + assistant after existing transcript entries", { timeout: 90_000 }, async () => { const { SessionManager } = await import("@mariozechner/pi-coding-agent"); const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-")); @@ -271,7 +271,7 @@ describe("runEmbeddedPiAgent", () => { expect(firstAssistantIndex).toBeGreaterThan(firstUserIndex); expect(secondUserIndex).toBeGreaterThan(firstAssistantIndex); expect(secondAssistantIndex).toBeGreaterThan(secondUserIndex); - }, 20_000); + }, 90_000); it("repairs orphaned user messages and continues", async () => { const { SessionManager } = await import("@mariozechner/pi-coding-agent"); diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.writes-models-json-into-provided-agentdir.test.ts b/src/agents/pi-embedded-runner.run-embedded-pi-agent.writes-models-json-into-provided-agentdir.test.ts index e15d79d43..4d54e215d 100644 --- a/src/agents/pi-embedded-runner.run-embedded-pi-agent.writes-models-json-into-provided-agentdir.test.ts +++ b/src/agents/pi-embedded-runner.run-embedded-pi-agent.writes-models-json-into-provided-agentdir.test.ts @@ -192,7 +192,7 @@ describe("runEmbeddedPiAgent", () => { await expect(fs.stat(path.join(agentDir, "models.json"))).resolves.toBeTruthy(); }); - it("persists the first user message before assistant output", { timeout: 45_000 }, async () => { + it("persists the first user message before assistant output", { timeout: 60_000 }, async () => { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-")); const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-")); const sessionFile = path.join(workspaceDir, "session.jsonl"); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index af8cc6b09..909b3e2a5 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -359,6 +359,33 @@ export async function runEmbeddedAttempt( runAbortController.abort(); void activeSession.abort(); }; + const abortable = (promise: Promise): Promise => { + const signal = runAbortController.signal; + if (signal.aborted) { + const err = new Error("aborted"); + (err as { name?: string }).name = "AbortError"; + return Promise.reject(err); + } + return new Promise((resolve, reject) => { + const onAbort = () => { + const err = new Error("aborted"); + (err as { name?: string }).name = "AbortError"; + signal.removeEventListener("abort", onAbort); + reject(err); + }; + signal.addEventListener("abort", onAbort, { once: true }); + promise.then( + (value) => { + signal.removeEventListener("abort", onAbort); + resolve(value); + }, + (err) => { + signal.removeEventListener("abort", onAbort); + reject(err); + }, + ); + }); + }; const subscription = subscribeEmbeddedPiSession({ session: activeSession, @@ -454,7 +481,7 @@ export async function runEmbeddedAttempt( } try { - await activeSession.prompt(params.prompt, { images: params.images }); + await abortable(activeSession.prompt(params.prompt, { images: params.images })); } catch (err) { promptError = err; } finally { diff --git a/src/commands/doctor.falls-back-legacy-sandbox-image-missing.test.ts b/src/commands/doctor.falls-back-legacy-sandbox-image-missing.test.ts index 049ca35c6..a1f15cdf3 100644 --- a/src/commands/doctor.falls-back-legacy-sandbox-image-missing.test.ts +++ b/src/commands/doctor.falls-back-legacy-sandbox-image-missing.test.ts @@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({ buildWorkspaceSkillStatus: () => ({ skills: [] }), })); +vi.mock("../plugins/loader.js", () => ({ + loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }), +})); + vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { @@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({ vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }), + upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), })); vi.mock("../telegram/token.js", () => ({ @@ -374,5 +378,5 @@ describe("doctor command", () => { expect(runLegacyStateMigrations).toHaveBeenCalledTimes(1); expect(confirm).not.toHaveBeenCalled(); - }, 20_000); + }, 30_000); }); diff --git a/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.test.ts b/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.test.ts index bc0c7747d..c4cf14c80 100644 --- a/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.test.ts +++ b/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.test.ts @@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({ buildWorkspaceSkillStatus: () => ({ skills: [] }), })); +vi.mock("../plugins/loader.js", () => ({ + loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }), +})); + vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { @@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({ vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }), + upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), })); vi.mock("../telegram/token.js", () => ({ @@ -362,7 +366,7 @@ describe("doctor command", () => { expect(written.routing).toBeUndefined(); }); - it("migrates legacy gateway services", async () => { + it("migrates legacy gateway services", { timeout: 30_000 }, async () => { readConfigFileSnapshot.mockResolvedValue({ path: "/tmp/clawdbot.json", exists: true, diff --git a/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.test.ts b/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.test.ts index 62df3b6c2..a4a3e8fd5 100644 --- a/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.test.ts +++ b/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.test.ts @@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({ buildWorkspaceSkillStatus: () => ({ skills: [] }), })); +vi.mock("../plugins/loader.js", () => ({ + loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }), +})); + vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { @@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({ vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }), + upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), })); vi.mock("../telegram/token.js", () => ({ @@ -375,7 +379,7 @@ describe("doctor command", () => { expect(runLegacyStateMigrations).toHaveBeenCalledTimes(1); expect(confirm).not.toHaveBeenCalled(); - }, 20_000); + }, 30_000); it("skips gateway restarts in non-interactive mode", async () => { readConfigFileSnapshot.mockResolvedValue({ @@ -448,5 +452,5 @@ describe("doctor command", () => { const profiles = (written.auth as { profiles: Record }).profiles; expect(profiles["anthropic:me@example.com"]).toBeTruthy(); expect(profiles["anthropic:default"]).toBeUndefined(); - }, 20_000); + }, 30_000); }); diff --git a/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.test.ts b/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.test.ts index 5abf068c5..8904c8e58 100644 --- a/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.test.ts +++ b/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.test.ts @@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({ buildWorkspaceSkillStatus: () => ({ skills: [] }), })); +vi.mock("../plugins/loader.js", () => ({ + loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }), +})); + vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { @@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({ vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }), + upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), })); vi.mock("../telegram/token.js", () => ({ @@ -373,7 +377,7 @@ describe("doctor command", () => { ); }), ).toBe(true); - }, 20_000); + }, 30_000); it("warns when extra workspace directories exist", async () => { readConfigFileSnapshot.mockResolvedValue({ diff --git a/src/commands/doctor.warns-state-directory-is-missing.test.ts b/src/commands/doctor.warns-state-directory-is-missing.test.ts index 08292689e..e6788f642 100644 --- a/src/commands/doctor.warns-state-directory-is-missing.test.ts +++ b/src/commands/doctor.warns-state-directory-is-missing.test.ts @@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({ buildWorkspaceSkillStatus: () => ({ skills: [] }), })); +vi.mock("../plugins/loader.js", () => ({ + loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }), +})); + vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { @@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({ vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }), + upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), })); vi.mock("../telegram/token.js", () => ({ @@ -348,7 +352,7 @@ describe("doctor command", () => { const stateNote = note.mock.calls.find((call) => call[1] === "State integrity"); expect(stateNote).toBeTruthy(); expect(String(stateNote?.[0])).toContain("CRITICAL"); - }, 20_000); + }, 30_000); it("warns about opencode provider overrides", async () => { readConfigFileSnapshot.mockResolvedValue({ diff --git a/src/gateway/gateway.tool-calling.mock-openai.test.ts b/src/gateway/gateway.tool-calling.mock-openai.test.ts index 8fae81d5c..b756c2e7a 100644 --- a/src/gateway/gateway.tool-calling.mock-openai.test.ts +++ b/src/gateway/gateway.tool-calling.mock-openai.test.ts @@ -252,7 +252,7 @@ async function connectClient(params: { url: string; token: string }) { } describe("gateway (mock openai): tool calling", () => { - it("runs a Read tool call end-to-end via gateway agent loop", async () => { + it("runs a Read tool call end-to-end via gateway agent loop", { timeout: 90_000 }, async () => { const prev = { home: process.env.HOME, configPath: process.env.CLAWDBOT_CONFIG_PATH, diff --git a/src/gateway/gateway.wizard.e2e.test.ts b/src/gateway/gateway.wizard.e2e.test.ts index 4edfc858a..cd835b3b7 100644 --- a/src/gateway/gateway.wizard.e2e.test.ts +++ b/src/gateway/gateway.wizard.e2e.test.ts @@ -268,5 +268,5 @@ describe("gateway wizard (e2e)", () => { process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; } - }, 60_000); + }, 90_000); }); diff --git a/src/gateway/server.auth.test.ts b/src/gateway/server.auth.test.ts index 4a09b1861..0de47cedf 100644 --- a/src/gateway/server.auth.test.ts +++ b/src/gateway/server.auth.test.ts @@ -14,10 +14,10 @@ import { installGatewayTestHooks(); describe("gateway server auth/connect", () => { - test("closes silent handshakes after timeout", { timeout: 15_000 }, async () => { + test("closes silent handshakes after timeout", { timeout: 30_000 }, async () => { const { server, ws } = await startServerWithClient(); const closed = await new Promise((resolve) => { - const timer = setTimeout(() => resolve(false), 12_000); + const timer = setTimeout(() => resolve(false), 25_000); ws.once("close", () => { clearTimeout(timer); resolve(true); diff --git a/src/gateway/server.health.test.ts b/src/gateway/server.health.test.ts index dec5e3bdd..245b820f2 100644 --- a/src/gateway/server.health.test.ts +++ b/src/gateway/server.health.test.ts @@ -16,7 +16,7 @@ import { installGatewayTestHooks(); describe("gateway server health/presence", () => { - test("connect + health + presence + status succeed", { timeout: 8000 }, async () => { + test("connect + health + presence + status succeed", { timeout: 20_000 }, async () => { const { server, ws } = await startServerWithClient(); await connectOk(ws); diff --git a/src/gateway/server.misc.test.ts b/src/gateway/server.misc.test.ts index 0c671c988..644ff75ba 100644 --- a/src/gateway/server.misc.test.ts +++ b/src/gateway/server.misc.test.ts @@ -46,7 +46,7 @@ describe("gateway server misc", () => { } }); - test("send dedupes by idempotencyKey", { timeout: 8000 }, async () => { + test("send dedupes by idempotencyKey", { timeout: 20_000 }, async () => { const { server, ws } = await startServerWithClient(); await connectOk(ws); diff --git a/src/gateway/server.models-voicewake.test.ts b/src/gateway/server.models-voicewake.test.ts index cbbdff3d3..66bead812 100644 --- a/src/gateway/server.models-voicewake.test.ts +++ b/src/gateway/server.models-voicewake.test.ts @@ -64,7 +64,7 @@ describe("gateway server models + voicewake", () => { test( "voicewake.get returns defaults and voicewake.set broadcasts", - { timeout: 15_000 }, + { timeout: 30_000 }, async () => { const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-home-")); const restoreHome = setTempHome(homeDir); diff --git a/src/plugins/runtime/types.ts b/src/plugins/runtime/types.ts index c09a7cad1..89f592a1f 100644 --- a/src/plugins/runtime/types.ts +++ b/src/plugins/runtime/types.ts @@ -1,5 +1,39 @@ import type { LogLevel } from "../../logging/levels.js"; +type ShouldLogVerbose = typeof import("../../globals.js").shouldLogVerbose; +type DispatchReplyWithBufferedBlockDispatcher = + typeof import("../../auto-reply/reply/provider-dispatcher.js").dispatchReplyWithBufferedBlockDispatcher; +type CreateReplyDispatcherWithTyping = + typeof import("../../auto-reply/reply/reply-dispatcher.js").createReplyDispatcherWithTyping; +type ResolveEffectiveMessagesConfig = + typeof import("../../agents/identity.js").resolveEffectiveMessagesConfig; +type ResolveHumanDelayConfig = typeof import("../../agents/identity.js").resolveHumanDelayConfig; +type ResolveAgentRoute = typeof import("../../routing/resolve-route.js").resolveAgentRoute; +type BuildPairingReply = typeof import("../../pairing/pairing-messages.js").buildPairingReply; +type ReadChannelAllowFromStore = + typeof import("../../pairing/pairing-store.js").readChannelAllowFromStore; +type UpsertChannelPairingRequest = + typeof import("../../pairing/pairing-store.js").upsertChannelPairingRequest; +type FetchRemoteMedia = typeof import("../../media/fetch.js").fetchRemoteMedia; +type SaveMediaBuffer = typeof import("../../media/store.js").saveMediaBuffer; +type BuildMentionRegexes = typeof import("../../auto-reply/reply/mentions.js").buildMentionRegexes; +type MatchesMentionPatterns = + typeof import("../../auto-reply/reply/mentions.js").matchesMentionPatterns; +type ResolveChannelGroupPolicy = + typeof import("../../config/group-policy.js").resolveChannelGroupPolicy; +type ResolveChannelGroupRequireMention = + typeof import("../../config/group-policy.js").resolveChannelGroupRequireMention; +type CreateInboundDebouncer = + typeof import("../../auto-reply/inbound-debounce.js").createInboundDebouncer; +type ResolveInboundDebounceMs = + typeof import("../../auto-reply/inbound-debounce.js").resolveInboundDebounceMs; +type ResolveCommandAuthorizedFromAuthorizers = + typeof import("../../channels/command-gating.js").resolveCommandAuthorizedFromAuthorizers; +type ResolveTextChunkLimit = typeof import("../../auto-reply/chunk.js").resolveTextChunkLimit; +type ChunkMarkdownText = typeof import("../../auto-reply/chunk.js").chunkMarkdownText; +type HasControlCommand = typeof import("../../auto-reply/command-detection.js").hasControlCommand; +type ResolveStateDir = typeof import("../../config/paths.js").resolveStateDir; + export type RuntimeLogger = { debug?: (message: string) => void; info: (message: string) => void; @@ -11,52 +45,52 @@ export type PluginRuntime = { version: string; channel: { text: { - chunkMarkdownText: typeof import("../../auto-reply/chunk.js").chunkMarkdownText; - resolveTextChunkLimit: typeof import("../../auto-reply/chunk.js").resolveTextChunkLimit; - hasControlCommand: typeof import("../../auto-reply/command-detection.js").hasControlCommand; + chunkMarkdownText: ChunkMarkdownText; + resolveTextChunkLimit: ResolveTextChunkLimit; + hasControlCommand: HasControlCommand; }; reply: { - dispatchReplyWithBufferedBlockDispatcher: typeof import("../../auto-reply/reply/provider-dispatcher.js").dispatchReplyWithBufferedBlockDispatcher; - createReplyDispatcherWithTyping: typeof import("../../auto-reply/reply/reply-dispatcher.js").createReplyDispatcherWithTyping; - resolveEffectiveMessagesConfig: typeof import("../../agents/identity.js").resolveEffectiveMessagesConfig; - resolveHumanDelayConfig: typeof import("../../agents/identity.js").resolveHumanDelayConfig; + dispatchReplyWithBufferedBlockDispatcher: DispatchReplyWithBufferedBlockDispatcher; + createReplyDispatcherWithTyping: CreateReplyDispatcherWithTyping; + resolveEffectiveMessagesConfig: ResolveEffectiveMessagesConfig; + resolveHumanDelayConfig: ResolveHumanDelayConfig; }; routing: { - resolveAgentRoute: typeof import("../../routing/resolve-route.js").resolveAgentRoute; + resolveAgentRoute: ResolveAgentRoute; }; pairing: { - buildPairingReply: typeof import("../../pairing/pairing-messages.js").buildPairingReply; - readAllowFromStore: typeof import("../../pairing/pairing-store.js").readChannelAllowFromStore; - upsertPairingRequest: typeof import("../../pairing/pairing-store.js").upsertChannelPairingRequest; + buildPairingReply: BuildPairingReply; + readAllowFromStore: ReadChannelAllowFromStore; + upsertPairingRequest: UpsertChannelPairingRequest; }; media: { - fetchRemoteMedia: typeof import("../../media/fetch.js").fetchRemoteMedia; - saveMediaBuffer: typeof import("../../media/store.js").saveMediaBuffer; + fetchRemoteMedia: FetchRemoteMedia; + saveMediaBuffer: SaveMediaBuffer; }; mentions: { - buildMentionRegexes: typeof import("../../auto-reply/reply/mentions.js").buildMentionRegexes; - matchesMentionPatterns: typeof import("../../auto-reply/reply/mentions.js").matchesMentionPatterns; + buildMentionRegexes: BuildMentionRegexes; + matchesMentionPatterns: MatchesMentionPatterns; }; groups: { - resolveGroupPolicy: typeof import("../../config/group-policy.js").resolveChannelGroupPolicy; - resolveRequireMention: typeof import("../../config/group-policy.js").resolveChannelGroupRequireMention; + resolveGroupPolicy: ResolveChannelGroupPolicy; + resolveRequireMention: ResolveChannelGroupRequireMention; }; debounce: { - createInboundDebouncer: typeof import("../../auto-reply/inbound-debounce.js").createInboundDebouncer; - resolveInboundDebounceMs: typeof import("../../auto-reply/inbound-debounce.js").resolveInboundDebounceMs; + createInboundDebouncer: CreateInboundDebouncer; + resolveInboundDebounceMs: ResolveInboundDebounceMs; }; commands: { - resolveCommandAuthorizedFromAuthorizers: typeof import("../../channels/command-gating.js").resolveCommandAuthorizedFromAuthorizers; + resolveCommandAuthorizedFromAuthorizers: ResolveCommandAuthorizedFromAuthorizers; }; }; logging: { - shouldLogVerbose: typeof import("../../globals.js").shouldLogVerbose; + shouldLogVerbose: ShouldLogVerbose; getChildLogger: ( bindings?: Record, opts?: { level?: LogLevel }, ) => RuntimeLogger; }; state: { - resolveStateDir: typeof import("../../config/paths.js").resolveStateDir; + resolveStateDir: ResolveStateDir; }; };