From 2df95c0b10fdef7551e9a6531cc53c71ca581e0b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 31 May 2026 20:41:57 +0100 Subject: [PATCH] chore(lint): enable no-misused-promises --- .oxlintrc.json | 1 + extensions/active-memory/index.ts | 4 +- .../src/app-server/side-question.test.ts | 187 +++--- extensions/copilot/src/attempt.test.ts | 13 +- extensions/discord/src/voice/audio.ts | 84 +-- extensions/discord/src/voice/manager.ts | 58 +- extensions/line/src/monitor.lifecycle.test.ts | 11 +- .../matrix/src/matrix/monitor/auto-join.ts | 36 +- .../matrix/src/matrix/monitor/events.ts | 9 +- .../matrix/src/matrix/sdk/crypto-bootstrap.ts | 2 +- ...ric-embedding-provider.integration.test.ts | 56 +- extensions/msteams/src/auth-coverage.test.ts | 23 +- extensions/msteams/src/setup-surface.ts | 4 +- extensions/nextcloud-talk/src/monitor.ts | 156 ++--- extensions/nostr/src/nostr-bus.ts | 9 +- .../openai/openai-chatgpt-oauth.runtime.ts | 4 +- extensions/qa-lab/src/bus-server.ts | 12 +- extensions/qa-lab/src/lab-server.ts | 576 +++++++++--------- .../src/providers/mock-openai/server.ts | 300 ++++----- extensions/qa-lab/web/src/app.ts | 50 +- .../src/substrate/fault-proxy.test.ts | 28 +- .../qa-matrix/src/substrate/fault-proxy.ts | 104 ++-- .../src/engine/api/media-chunked.test.ts | 33 +- .../src/engine/gateway/gateway-connection.ts | 2 +- .../src/engine/gateway/outbound-dispatch.ts | 6 +- .../slack/src/monitor/slash.test-harness.ts | 6 +- extensions/slack/src/send.upload.test.ts | 30 +- .../telegram/src/bot-handlers.runtime.ts | 12 +- .../bot.create-telegram-bot.test-harness.ts | 22 +- .../src/bot.create-telegram-bot.test.ts | 44 +- extensions/tlon/src/monitor/index.ts | 172 +++--- extensions/voice-call/src/manager/outbound.ts | 14 +- extensions/voice-call/src/manager/timers.ts | 24 +- extensions/voice-call/src/media-stream.ts | 6 +- extensions/whatsapp/src/session.ts | 52 +- .../src/monitor.pairing.lifecycle.test.ts | 8 +- .../src/monitor.reply-once.lifecycle.test.ts | 8 +- extensions/zalo/src/monitor.ts | 4 +- extensions/zalo/src/monitor.webhook.test.ts | 18 +- scripts/anthropic-prompt-probe.ts | 100 +-- .../openai-web-search-minimal/mock-server.mjs | 84 +-- .../clickclack-fixture.mjs | 146 ++--- scripts/e2e/mock-openai-server.mjs | 156 ++--- .../e2e/openai-image-auth-docker-client.ts | 126 ++-- scripts/e2e/parallels/host-command.ts | 26 +- scripts/e2e/parallels/npm-update-smoke.ts | 8 +- scripts/qa-otel-smoke.ts | 116 ++-- src/acp/client.ts | 46 +- src/acp/control-plane/manager.test-helpers.ts | 36 +- .../cli-runner/bundle-mcp.gemini.live.test.ts | 14 +- .../compact.hooks.test.ts | 2 +- .../context-engine-maintenance.ts | 38 +- .../tool-result-context-guard.test.ts | 36 +- ...ded-agent-subscribe.handlers.tools.test.ts | 4 +- src/agents/model-catalog.test.ts | 4 +- src/agents/sessions/tools/grep.ts | 138 ++--- src/auto-reply/inbound-debounce.ts | 4 +- src/auto-reply/reply/reply-dispatcher.ts | 17 +- src/cli/update-cli.test.ts | 78 +-- src/gateway/call.ts | 48 +- src/gateway/managed-image-attachments.test.ts | 46 +- src/gateway/probe.ts | 136 +++-- src/gateway/server-channels.ts | 2 +- src/infra/heartbeat-wake.ts | 102 ++-- ...enai-compatible-embedding-provider.test.ts | 60 +- src/proxy-capture/proxy-server.ts | 202 +++--- src/tui/tui-pty-local.e2e.test.ts | 36 +- src/wizard/setup.finalize.ts | 4 +- ui/src/ui/app-gateway.node.test.ts | 6 +- ui/src/ui/app-render-usage-tab.ts | 2 +- ui/src/ui/app-render.ts | 174 +++--- ui/src/ui/controllers/agents.test.ts | 8 +- ui/src/ui/controllers/dreaming.test.ts | 28 +- ui/src/ui/controllers/skills.test.ts | 13 +- 74 files changed, 2206 insertions(+), 2028 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index a9f57f0ed95e..fba802009807 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -78,6 +78,7 @@ "typescript/no-extraneous-class": "error", "typescript/no-import-type-side-effects": "error", "typescript/no-meaningless-void-operator": "error", + "typescript/no-misused-promises": "error", "typescript/no-inferrable-types": "error", "typescript/no-non-null-asserted-nullish-coalescing": "error", "typescript/no-unnecessary-qualifier": "error", diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 14e72efe6710..df9f503d9921 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -1793,7 +1793,9 @@ function watchTerminalMemorySearchResult(params: { if (stopped) { return; } - timeoutId = setTimeout(tick, TERMINAL_MEMORY_SEARCH_POLL_INTERVAL_MS); + timeoutId = setTimeout(() => { + void tick(); + }, TERMINAL_MEMORY_SEARCH_POLL_INTERVAL_MS); timeoutId.unref?.(); }; const tick = async () => { diff --git a/extensions/codex/src/app-server/side-question.test.ts b/extensions/codex/src/app-server/side-question.test.ts index 60435ae6846d..568e8e58b54f 100644 --- a/extensions/codex/src/app-server/side-question.test.ts +++ b/extensions/codex/src/app-server/side-question.test.ts @@ -54,9 +54,14 @@ const { testing, runCodexAppServerSideQuestion } = await import("./side-question type ServerRequest = Required> & { params?: RpcRequest["params"]; }; +type ClientRequest = ( + method: string, + requestParams?: unknown, + options?: unknown, +) => Promise; type FakeClient = { - request: ReturnType; + request: ReturnType>; addNotificationHandler: ReturnType; addRequestHandler: ReturnType; notifications: Array<(notification: CodexServerNotification) => void>; @@ -71,7 +76,7 @@ function createFakeClient(): FakeClient { const client: FakeClient = { notifications, requests, - request: vi.fn(), + request: vi.fn(), addNotificationHandler: vi.fn((handler: (notification: CodexServerNotification) => void) => { notifications.push(handler); return () => { @@ -625,19 +630,21 @@ describe("runCodexAppServerSideQuestion", () => { return {}; } if (method === "turn/start") { - setTimeout(async () => { - approvalResponse = await client.handleRequest({ - id: 42, - method: "item/commandExecution/requestApproval", - params: { - threadId: "side-thread", - turnId: "turn-1", - itemId: "cmd-side", - command: "/bin/bash -lc 'node -v'", - cwd: "/tmp/workspace", - }, - }); - client.emit(turnCompleted("side-thread", "turn-1", "Side answer.")); + setTimeout(() => { + void (async () => { + approvalResponse = await client.handleRequest({ + id: 42, + method: "item/commandExecution/requestApproval", + params: { + threadId: "side-thread", + turnId: "turn-1", + itemId: "cmd-side", + command: "/bin/bash -lc 'node -v'", + cwd: "/tmp/workspace", + }, + }); + client.emit(turnCompleted("side-thread", "turn-1", "Side answer.")); + })(); }, 0); return turnStartResult("turn-1"); } @@ -913,20 +920,22 @@ describe("runCodexAppServerSideQuestion", () => { return {}; } if (method === "turn/start") { - setTimeout(async () => { - toolResponse = await client.handleRequest({ - id: 42, - method: "item/tool/call", - params: { - threadId: "side-thread", - turnId: "turn-1", - callId: "tool-1", - tool: "wiki_status", - arguments: { topic: "AGENTS.md" }, - }, - }); - client.emit(agentDelta("side-thread", "turn-1", "Tool answer.")); - client.emit(turnCompleted("side-thread", "turn-1", "Tool answer.")); + setTimeout(() => { + void (async () => { + toolResponse = await client.handleRequest({ + id: 42, + method: "item/tool/call", + params: { + threadId: "side-thread", + turnId: "turn-1", + callId: "tool-1", + tool: "wiki_status", + arguments: { topic: "AGENTS.md" }, + }, + }); + client.emit(agentDelta("side-thread", "turn-1", "Tool answer.")); + client.emit(turnCompleted("side-thread", "turn-1", "Tool answer.")); + })(); }, 0); return turnStartResult("turn-1"); } @@ -966,20 +975,22 @@ describe("runCodexAppServerSideQuestion", () => { return {}; } if (method === "turn/start") { - setTimeout(async () => { - await client.handleRequest({ - id: 42, - method: "item/tool/call", - params: { - threadId: "side-thread", - turnId: "turn-1", - callId: "tool-1", - tool: "wiki_status", - arguments: { topic: "AGENTS.md" }, - }, - }); - client.emit(agentDelta("side-thread", "turn-1", "Tool answer.")); - client.emit(turnCompleted("side-thread", "turn-1", "Tool answer.")); + setTimeout(() => { + void (async () => { + await client.handleRequest({ + id: 42, + method: "item/tool/call", + params: { + threadId: "side-thread", + turnId: "turn-1", + callId: "tool-1", + tool: "wiki_status", + arguments: { topic: "AGENTS.md" }, + }, + }); + client.emit(agentDelta("side-thread", "turn-1", "Tool answer.")); + client.emit(turnCompleted("side-thread", "turn-1", "Tool answer.")); + })(); }, 0); return turnStartResult("turn-1"); } @@ -1045,20 +1056,22 @@ describe("runCodexAppServerSideQuestion", () => { return {}; } if (method === "turn/start") { - setTimeout(async () => { - await client.handleRequest({ - id: 42, - method: "item/tool/call", - params: { - threadId: "side-thread", - turnId: "turn-1", - callId: "tool-1", - tool: "wiki_status", - arguments: { topic: "AGENTS.md" }, - }, - }); - client.emit(agentDelta("side-thread", "turn-1", "Tool answer.")); - client.emit(turnCompleted("side-thread", "turn-1", "Tool answer.")); + setTimeout(() => { + void (async () => { + await client.handleRequest({ + id: 42, + method: "item/tool/call", + params: { + threadId: "side-thread", + turnId: "turn-1", + callId: "tool-1", + tool: "wiki_status", + arguments: { topic: "AGENTS.md" }, + }, + }); + client.emit(agentDelta("side-thread", "turn-1", "Tool answer.")); + client.emit(turnCompleted("side-thread", "turn-1", "Tool answer.")); + })(); }, 0); return turnStartResult("turn-1"); } @@ -1098,35 +1111,37 @@ describe("runCodexAppServerSideQuestion", () => { return {}; } if (method === "turn/start") { - setTimeout(async () => { - unrelatedUserInputResponse = await client.handleRequest({ - id: 42, - method: "item/tool/requestUserInput", - params: { - threadId: "parent-thread", - turnId: "parent-turn", - itemId: "input-parent", - questions: [], - }, - }); - userInputResponse = await client.handleRequest({ - id: 43, - method: "item/tool/requestUserInput", - params: { - threadId: "side-thread", - turnId: "turn-1", - itemId: "input-1", - questions: [ - { - id: "choice", - header: "Choice", - question: "Pick one", - options: [{ label: "A", description: "" }], - }, - ], - }, - }); - client.emit(turnCompleted("side-thread", "turn-1", "No input needed.")); + setTimeout(() => { + void (async () => { + unrelatedUserInputResponse = await client.handleRequest({ + id: 42, + method: "item/tool/requestUserInput", + params: { + threadId: "parent-thread", + turnId: "parent-turn", + itemId: "input-parent", + questions: [], + }, + }); + userInputResponse = await client.handleRequest({ + id: 43, + method: "item/tool/requestUserInput", + params: { + threadId: "side-thread", + turnId: "turn-1", + itemId: "input-1", + questions: [ + { + id: "choice", + header: "Choice", + question: "Pick one", + options: [{ label: "A", description: "" }], + }, + ], + }, + }); + client.emit(turnCompleted("side-thread", "turn-1", "No input needed.")); + })(); }, 0); return turnStartResult("turn-1"); } diff --git a/extensions/copilot/src/attempt.test.ts b/extensions/copilot/src/attempt.test.ts index 0c17bbb28c6c..697f28f5a0a6 100644 --- a/extensions/copilot/src/attempt.test.ts +++ b/extensions/copilot/src/attempt.test.ts @@ -53,16 +53,17 @@ type SessionEventShape = { timestamp: string; type: string; }; +type SendAndWaitFn = (options?: unknown) => Promise; type FakeSession = { - abort: ReturnType; + abort: ReturnType Promise>>; cfg: Record; - disconnect: ReturnType; + disconnect: ReturnType Promise>>; emit: (eventType: string, data: Record) => void; id: string; off: ReturnType; on: ReturnType; - sendAndWait: ReturnType; + sendAndWait: ReturnType>; sessionId: string; }; @@ -129,9 +130,9 @@ function makeAssistantMessageEvent( function createFakeSession(cfg: Record, id: string): FakeSession { const listeners = new Map void>>(); return { - abort: vi.fn(async () => undefined), + abort: vi.fn<() => Promise>(async () => undefined), cfg, - disconnect: vi.fn(async () => undefined), + disconnect: vi.fn<() => Promise>(async () => undefined), emit: (eventType: string, data: Record) => { const event = makeEvent(eventType, data); for (const listener of listeners.get(eventType) ?? []) { @@ -151,7 +152,7 @@ function createFakeSession(cfg: Record, id: string): FakeSessio handlers.push(handler); listeners.set(eventType, handlers); }), - sendAndWait: vi.fn(async () => makeAssistantMessageEvent()), + sendAndWait: vi.fn(async () => makeAssistantMessageEvent()), sessionId: id, }; } diff --git a/extensions/discord/src/voice/audio.ts b/extensions/discord/src/voice/audio.ts index 81d7b4e44454..49e3c3237c1d 100644 --- a/extensions/discord/src/voice/audio.ts +++ b/extensions/discord/src/voice/audio.ts @@ -179,52 +179,52 @@ class DiscordOpusEncodeStream extends Transform { return this.#encoder; } - override async _transform( - chunk: Buffer, - _encoding: BufferEncoding, - done: TransformCallback, - ): Promise { - try { - const encoder = await this.#getEncoder(); - this.#buffer = - this.#buffer.length > 0 ? Buffer.concat([this.#buffer, chunk]) : Buffer.from(chunk); - while (this.#buffer.length >= DISCORD_OPUS_FRAME_BYTES) { - const frame = this.#buffer.subarray(0, DISCORD_OPUS_FRAME_BYTES); - this.#buffer = this.#buffer.subarray(DISCORD_OPUS_FRAME_BYTES); - this.push( - Buffer.from( - encoder.encode(frame, { - frameSize: DISCORD_OPUS_FRAME_SIZE, - }), - ), - ); + override _transform(chunk: Buffer, _encoding: BufferEncoding, done: TransformCallback): void { + void (async () => { + try { + const encoder = await this.#getEncoder(); + this.#buffer = + this.#buffer.length > 0 ? Buffer.concat([this.#buffer, chunk]) : Buffer.from(chunk); + while (this.#buffer.length >= DISCORD_OPUS_FRAME_BYTES) { + const frame = this.#buffer.subarray(0, DISCORD_OPUS_FRAME_BYTES); + this.#buffer = this.#buffer.subarray(DISCORD_OPUS_FRAME_BYTES); + this.push( + Buffer.from( + encoder.encode(frame, { + frameSize: DISCORD_OPUS_FRAME_SIZE, + }), + ), + ); + } + done(); + } catch (err) { + done(err instanceof Error ? err : new Error(formatErrorMessage(err))); } - done(); - } catch (err) { - done(err instanceof Error ? err : new Error(formatErrorMessage(err))); - } + })(); } - override async _final(done: TransformCallback): Promise { - try { - if (this.#buffer.length > 0) { - const encoder = await this.#getEncoder(); - const frame = Buffer.alloc(DISCORD_OPUS_FRAME_BYTES); - this.#buffer.copy(frame); - this.#buffer = Buffer.alloc(0); - this.push( - Buffer.from( - encoder.encode(frame, { - frameSize: DISCORD_OPUS_FRAME_SIZE, - }), - ), - ); + override _final(done: TransformCallback): void { + void (async () => { + try { + if (this.#buffer.length > 0) { + const encoder = await this.#getEncoder(); + const frame = Buffer.alloc(DISCORD_OPUS_FRAME_BYTES); + this.#buffer.copy(frame); + this.#buffer = Buffer.alloc(0); + this.push( + Buffer.from( + encoder.encode(frame, { + frameSize: DISCORD_OPUS_FRAME_SIZE, + }), + ), + ); + } + this.#freeEncoder(); + done(); + } catch (err) { + done(err instanceof Error ? err : new Error(formatErrorMessage(err))); } - this.#freeEncoder(); - done(); - } catch (err) { - done(err instanceof Error ? err : new Error(formatErrorMessage(err))); - } + })(); } override _destroy(err: Error | null, done: (error?: Error | null) => void): void { diff --git a/extensions/discord/src/voice/manager.ts b/extensions/discord/src/voice/manager.ts index c8efb5333150..9a1f2a0ae753 100644 --- a/extensions/discord/src/voice/manager.ts +++ b/extensions/discord/src/voice/manager.ts @@ -790,34 +790,36 @@ export class DiscordVoiceManager { this.scheduleCaptureFinalize(entry, userId, "speaker end"); }; - const disconnectedHandler: (() => Promise) | undefined = async () => { - try { - logVoiceVerbose( - `disconnected: attempting recovery guild ${guildId} channel ${channelId} grace=${reconnectGraceMs}ms`, - ); - await Promise.race([ - voiceSdk.entersState( - connection, - voiceSdk.VoiceConnectionStatus.Signalling, - reconnectGraceMs, - ), - voiceSdk.entersState( - connection, - voiceSdk.VoiceConnectionStatus.Connecting, - reconnectGraceMs, - ), - ]); - logVoiceVerbose(`disconnected: recovery started guild ${guildId} channel ${channelId}`); - } catch (err) { - logger.warn( - `discord voice: disconnect recovery failed: guild ${guildId} channel ${channelId} timeout=${reconnectGraceMs}ms error=${formatErrorMessage(err)}; destroying connection`, - ); - clearSessionIfCurrent(); - stopEntry(entry, { - destroyConnection: true, - reason: `disconnect recovery failed guild ${guildId} channel ${channelId}`, - }); - } + const disconnectedHandler: (() => void) | undefined = () => { + void (async () => { + try { + logVoiceVerbose( + `disconnected: attempting recovery guild ${guildId} channel ${channelId} grace=${reconnectGraceMs}ms`, + ); + await Promise.race([ + voiceSdk.entersState( + connection, + voiceSdk.VoiceConnectionStatus.Signalling, + reconnectGraceMs, + ), + voiceSdk.entersState( + connection, + voiceSdk.VoiceConnectionStatus.Connecting, + reconnectGraceMs, + ), + ]); + logVoiceVerbose(`disconnected: recovery started guild ${guildId} channel ${channelId}`); + } catch (err) { + logger.warn( + `discord voice: disconnect recovery failed: guild ${guildId} channel ${channelId} timeout=${reconnectGraceMs}ms error=${formatErrorMessage(err)}; destroying connection`, + ); + clearSessionIfCurrent(); + stopEntry(entry, { + destroyConnection: true, + reason: `disconnect recovery failed guild ${guildId} channel ${channelId}`, + }); + } + })(); }; const destroyedHandler: (() => void) | undefined = () => { clearSessionIfCurrent(); diff --git a/extensions/line/src/monitor.lifecycle.test.ts b/extensions/line/src/monitor.lifecycle.test.ts index fba59ebb78ec..52b140389753 100644 --- a/extensions/line/src/monitor.lifecycle.test.ts +++ b/extensions/line/src/monitor.lifecycle.test.ts @@ -8,6 +8,7 @@ import { WEBHOOK_IN_FLIGHT_DEFAULTS } from "openclaw/plugin-sdk/webhook-request- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; type LineNodeWebhookHandler = (req: IncomingMessage, res: ServerResponse) => Promise; +type LineHandleWebhook = (...args: unknown[]) => Promise; const { createLineBotMock, @@ -17,7 +18,7 @@ const { } = vi.hoisted(() => ({ createLineBotMock: vi.fn(() => ({ account: { accountId: "default" }, - handleWebhook: vi.fn(), + handleWebhook: vi.fn(), })), createLineNodeWebhookHandlerMock: vi.fn<() => LineNodeWebhookHandler>(() => vi.fn(async () => {}), @@ -163,7 +164,7 @@ describe("monitorLineProvider lifecycle", () => { createLineBotMock.mockReset(); createLineBotMock.mockImplementation(() => ({ account: { accountId: "default" }, - handleWebhook: vi.fn(), + handleWebhook: vi.fn(), })); innerLineWebhookHandlerMock = vi.fn(async () => {}); createLineNodeWebhookHandlerMock @@ -362,11 +363,11 @@ describe("monitorLineProvider lifecycle", () => { let releaseWebhook: (() => void) | undefined; const bot = createLineBotMock.mock.results[0]?.value as { - handleWebhook: ReturnType; + handleWebhook: ReturnType>; }; bot.handleWebhook.mockImplementation( - async () => - await new Promise((resolve) => { + () => + new Promise((resolve) => { releaseWebhook = resolve; }), ); diff --git a/extensions/matrix/src/matrix/monitor/auto-join.ts b/extensions/matrix/src/matrix/monitor/auto-join.ts index 3cc2ff6cb75b..1c2cc0af3a25 100644 --- a/extensions/matrix/src/matrix/monitor/auto-join.ts +++ b/extensions/matrix/src/matrix/monitor/auto-join.ts @@ -60,25 +60,27 @@ export function registerMatrixAutoJoin(params: { }; // Handle invites directly so both "always" and "allowlist" modes share the same path. - client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => { - if (autoJoin === "allowlist") { - const allowedAliasRoomIds = await resolveAllowedAliasRoomIds(); - const allowed = - autoJoinAllowlist.has("*") || - allowedRoomIds.has(roomId) || - allowedAliasRoomIds.some((resolvedRoomId) => resolvedRoomId === roomId); + client.on("room.invite", (roomId: string, _inviteEvent: unknown) => { + void (async () => { + if (autoJoin === "allowlist") { + const allowedAliasRoomIds = await resolveAllowedAliasRoomIds(); + const allowed = + autoJoinAllowlist.has("*") || + allowedRoomIds.has(roomId) || + allowedAliasRoomIds.some((resolvedRoomId) => resolvedRoomId === roomId); - if (!allowed) { - logVerbose(`matrix: invite ignored (not in allowlist) room=${roomId}`); - return; + if (!allowed) { + logVerbose(`matrix: invite ignored (not in allowlist) room=${roomId}`); + return; + } } - } - try { - await client.joinRoom(roomId); - logVerbose(`matrix: joined room ${roomId}`); - } catch (err) { - runtime.error?.(`matrix: failed to join room ${roomId}: ${String(err)}`); - } + try { + await client.joinRoom(roomId); + logVerbose(`matrix: joined room ${roomId}`); + } catch (err) { + runtime.error?.(`matrix: failed to join room ${roomId}: ${String(err)}`); + } + })(); }); } diff --git a/extensions/matrix/src/matrix/monitor/events.ts b/extensions/matrix/src/matrix/monitor/events.ts index 27274a673dd9..f8a012d9c9f2 100644 --- a/extensions/matrix/src/matrix/monitor/events.ts +++ b/extensions/matrix/src/matrix/monitor/events.ts @@ -271,9 +271,8 @@ export function registerMatrixMonitorEvents(params: { ); }); - client.on( - "room.failed_decryption", - async (roomId: string, event: MatrixRawEvent, error: Error) => { + client.on("room.failed_decryption", (roomId: string, event: MatrixRawEvent, error: Error) => { + void (async () => { const failureState = postHealthySyncDecryptFailureTracker.recordFailure(roomId, event, error); const selfUserId = await resolveMatrixSelfUserId(client, logVerboseMessage); const sender = typeof event.sender === "string" ? event.sender : null; @@ -320,8 +319,8 @@ export function registerMatrixMonitorEvents(params: { logVerboseMessage( `matrix: failed decrypt room=${roomId} id=${event.event_id ?? "unknown"} freshAfterHealthySync=${String(failureState.freshAfterHealthySync)} error=${error.message}`, ); - }, - ); + })(); + }); client.on("verification.summary", (summary) => { void runMonitorTask("verification summary handler", async () => { diff --git a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts index 257c76b9c354..453a37472840 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts @@ -370,7 +370,7 @@ export class MatrixCryptoBootstrapper { // Remote-user verifications are only auto-accepted. The human-operated // client must explicitly choose "Verify by emoji" so we do not race a // second SAS start from the bot side and end up with mismatched keys. - crypto.on(CryptoEvent.VerificationRequestReceived, async (request) => { + crypto.on(CryptoEvent.VerificationRequestReceived, (request) => { const verificationRequest = request as MatrixVerificationRequestLike; try { this.deps.verificationManager.trackVerificationRequest(verificationRequest); diff --git a/extensions/memory-core/src/memory/generic-embedding-provider.integration.test.ts b/extensions/memory-core/src/memory/generic-embedding-provider.integration.test.ts index ea0ffd7a2b01..e23c56de786f 100644 --- a/extensions/memory-core/src/memory/generic-embedding-provider.integration.test.ts +++ b/extensions/memory-core/src/memory/generic-embedding-provider.integration.test.ts @@ -43,33 +43,35 @@ async function readJsonBody(req: IncomingMessage): Promise { const requests: CapturedRequest[] = []; - const server = createServer(async (req: IncomingMessage, res: ServerResponse) => { - try { - const body = await readJsonBody(req); - requests.push({ - method: req.method, - url: req.url, - headers: req.headers, - body, - }); - const input = body.input; - const texts = Array.isArray(input) ? input : [input]; - res.writeHead(200, { "content-type": "application/json" }); - res.end( - JSON.stringify({ - object: "list", - data: texts.map((text, index) => ({ - object: "embedding", - embedding: [String(text).length, index + 0.5, 3], - index, - })), - model: body.model, - }), - ); - } catch (error) { - res.writeHead(500, { "content-type": "application/json" }); - res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); - } + const server = createServer((req: IncomingMessage, res: ServerResponse) => { + void (async () => { + try { + const body = await readJsonBody(req); + requests.push({ + method: req.method, + url: req.url, + headers: req.headers, + body, + }); + const input = body.input; + const texts = Array.isArray(input) ? input : [input]; + res.writeHead(200, { "content-type": "application/json" }); + res.end( + JSON.stringify({ + object: "list", + data: texts.map((text, index) => ({ + object: "embedding", + embedding: [String(text).length, index + 0.5, 3], + index, + })), + model: body.model, + }), + ); + } catch (error) { + res.writeHead(500, { "content-type": "application/json" }); + res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); + } + })(); }); await new Promise((resolve, reject) => { diff --git a/extensions/msteams/src/auth-coverage.test.ts b/extensions/msteams/src/auth-coverage.test.ts index 3efa19d1764d..8736ca5c1a8e 100644 --- a/extensions/msteams/src/auth-coverage.test.ts +++ b/extensions/msteams/src/auth-coverage.test.ts @@ -58,25 +58,18 @@ beforeAll(async () => { privateKey = priv; publicPem = await exportSPKI(publicKey); - // Patch `JwksClient.prototype.getSigningKey` so every JWKS lookup the SDK + // Patch `JwksClient.prototype.getSigningKeys` so every JWKS lookup the SDK // performs returns our in-memory test key instead of fetching from - // `login.botframework.com` / `login.microsoftonline.com`. We patch the - // prototype here (rather than mocking the `jwks-rsa` module) because - // `jwks-rsa`'s constructor captures the prototype method reference into a - // cache wrapper at construction time — patching the prototype before any - // `JwksClient` is constructed in the tests is sufficient and avoids the - // CJS `__importDefault` shaping headaches of mocking the package itself. - vi.spyOn(JwksClient.prototype, "getSigningKey").mockImplementation((async ( - kid?: string | null, - ) => { - const key: SigningKey = { - kid: kid ?? TEST_KID, + // `login.botframework.com` / `login.microsoftonline.com` while preserving + // the package's callback/promise getSigningKey wrapper behavior. + vi.spyOn(JwksClient.prototype, "getSigningKeys").mockResolvedValue([ + { + kid: TEST_KID, alg: "RS256", getPublicKey: () => publicPem, rsaPublicKey: publicPem, - }; - return key; - }) as JwksClient["getSigningKey"]); + } as SigningKey, + ]); }); // Logger that surfaces SDK validation failures so we can see *why* a token diff --git a/extensions/msteams/src/setup-surface.ts b/extensions/msteams/src/setup-surface.ts index 8b91d5d88a0e..c5a958d7a883 100644 --- a/extensions/msteams/src/setup-surface.ts +++ b/extensions/msteams/src/setup-surface.ts @@ -283,7 +283,9 @@ export const msteamsSetupWizard: ChannelSetupWizard = { { isRemote: true, openUrl: openDelegatedOAuthUrl, - log: (msg) => params.prompter.note(msg), + log: (msg) => { + void params.prompter.note(msg); + }, note: (msg, title) => params.prompter.note(msg, title), prompt: (msg) => params.prompter.text({ message: msg }), progress, diff --git a/extensions/nextcloud-talk/src/monitor.ts b/extensions/nextcloud-talk/src/monitor.ts index 468d8af69278..5c494bd00e09 100644 --- a/extensions/nextcloud-talk/src/monitor.ts +++ b/extensions/nextcloud-talk/src/monitor.ts @@ -257,101 +257,103 @@ export function createNextcloudTalkWebhookServer(opts: NextcloudTalkWebhookServe pruneIntervalMs: authRateLimitWindowMs, }); - const server = createServer(async (req: IncomingMessage, res: ServerResponse) => { - if (req.url === HEALTH_PATH) { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("ok"); - return; - } - - if (req.url !== path || req.method !== "POST") { - res.writeHead(404); - res.end(); - return; - } - - const clientIp = req.socket.remoteAddress ?? "unknown"; - if (!webhookAuthRateLimiter.check(clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE).allowed) { - res.writeHead(429); - res.end("Too Many Requests"); - return; - } - - try { - const headers = validateWebhookHeaders({ - req, - res, - isBackendAllowed, - }); - if (!headers) { + const server = createServer((req: IncomingMessage, res: ServerResponse) => { + void (async () => { + if (req.url === HEALTH_PATH) { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("ok"); return; } - const body = await readBody(req, maxBodyBytes); - - const hasValidSignature = verifyWebhookSignature({ - headers, - body, - secret, - res, - clientIp, - authRateLimiter: webhookAuthRateLimiter, - }); - if (!hasValidSignature) { + if (req.url !== path || req.method !== "POST") { + res.writeHead(404); + res.end(); return; } - const decoded = decodeWebhookCreateMessage({ - body, - res, - }); - if (decoded.kind === "invalid") { - return; - } - if (decoded.kind === "ignore") { - writeJsonResponse(res, 200); + const clientIp = req.socket.remoteAddress ?? "unknown"; + if (!webhookAuthRateLimiter.check(clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE).allowed) { + res.writeHead(429); + res.end("Too Many Requests"); return; } - const message = decoded.message; - if (processMessage) { - writeJsonResponse(res, 200); - try { - await processMessage(message); - } catch (err) { - onError?.(err instanceof Error ? err : new Error(formatError(err))); + try { + const headers = validateWebhookHeaders({ + req, + res, + isBackendAllowed, + }); + if (!headers) { + return; } - return; - } - if (shouldProcessMessage) { - const shouldProcess = await shouldProcessMessage(message); - if (!shouldProcess) { + const body = await readBody(req, maxBodyBytes); + + const hasValidSignature = verifyWebhookSignature({ + headers, + body, + secret, + res, + clientIp, + authRateLimiter: webhookAuthRateLimiter, + }); + if (!hasValidSignature) { + return; + } + + const decoded = decodeWebhookCreateMessage({ + body, + res, + }); + if (decoded.kind === "invalid") { + return; + } + if (decoded.kind === "ignore") { writeJsonResponse(res, 200); return; } - } - writeJsonResponse(res, 200); + const message = decoded.message; + if (processMessage) { + writeJsonResponse(res, 200); + try { + await processMessage(message); + } catch (err) { + onError?.(err instanceof Error ? err : new Error(formatError(err))); + } + return; + } - try { - await onMessage(message); + if (shouldProcessMessage) { + const shouldProcess = await shouldProcessMessage(message); + if (!shouldProcess) { + writeJsonResponse(res, 200); + return; + } + } + + writeJsonResponse(res, 200); + + try { + await onMessage(message); + } catch (err) { + onError?.(err instanceof Error ? err : new Error(formatError(err))); + } } catch (err) { - onError?.(err instanceof Error ? err : new Error(formatError(err))); + if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) { + writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge); + return; + } + if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) { + writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT")); + return; + } + const error = err instanceof Error ? err : new Error(formatError(err)); + onError?.(error); + writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError); } - } catch (err) { - if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) { - writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge); - return; - } - if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) { - writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT")); - return; - } - const error = err instanceof Error ? err : new Error(formatError(err)); - onError?.(error); - writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError); - } + })(); }); const start = (): Promise => { diff --git a/extensions/nostr/src/nostr-bus.ts b/extensions/nostr/src/nostr-bus.ts index e9f4cc8e4e97..b84cb00b41e7 100644 --- a/extensions/nostr/src/nostr-bus.ts +++ b/extensions/nostr/src/nostr-bus.ts @@ -619,7 +619,9 @@ export async function startNostrBus(options: NostrBusOptions): Promise[1]; const relayAbort = new AbortController(); const sub = pool.subscribeMany(relays, dmFilter, { - onevent: handleEvent, + onevent: (event) => { + void handleEvent(event); + }, oneose: () => { // EOSE handler - called when all stored events have been received for (const relay of relays) { @@ -766,10 +768,11 @@ async function sendEncryptedDm( const startTime = Date.now(); try { - const [publishPromise] = pool.publish([relay], reply); - if (!publishPromise) { + const publishPromises = pool.publish([relay], reply); + if (publishPromises.length === 0) { throw new Error(`Failed to create publish promise for relay ${relay}`); } + const publishPromise = publishPromises[0]; await publishPromise; const latency = Date.now() - startTime; diff --git a/extensions/openai/openai-chatgpt-oauth.runtime.ts b/extensions/openai/openai-chatgpt-oauth.runtime.ts index bd84bdc93053..97551c258955 100644 --- a/extensions/openai/openai-chatgpt-oauth.runtime.ts +++ b/extensions/openai/openai-chatgpt-oauth.runtime.ts @@ -321,9 +321,9 @@ export async function loginOpenAICodexOAuth(params: { localBrowserMessage: localBrowserMessage ?? "Complete sign-in in browser...", manualPromptMessage: manualInputPromptMessage, }); - const onAuth: typeof baseOnAuth = async (event) => { + const onAuth = (event: Parameters[0]) => { browserAuthStarted = true; - await baseOnAuth(event); + void baseOnAuth(event); }; const creds = await loginOpenAICodex({ diff --git a/extensions/qa-lab/src/bus-server.ts b/extensions/qa-lab/src/bus-server.ts index 54cf58c7aafe..cfceea1086e7 100644 --- a/extensions/qa-lab/src/bus-server.ts +++ b/extensions/qa-lab/src/bus-server.ts @@ -192,11 +192,13 @@ export async function handleQaBusRequest(params: { } export function createQaBusServer(state: QaBusState): Server { - return createServer(async (req, res) => { - const handled = await handleQaBusRequest({ req, res, state }); - if (!handled) { - writeError(res, 404, "not found"); - } + return createServer((req, res) => { + void (async () => { + const handled = await handleQaBusRequest({ req, res, state }); + if (!handled) { + writeError(res, 404, "not found"); + } + })(); }); } diff --git a/extensions/qa-lab/src/lab-server.ts b/extensions/qa-lab/src/lab-server.ts index 0f7815a7335f..898d292d983b 100644 --- a/extensions/qa-lab/src/lab-server.ts +++ b/extensions/qa-lab/src/lab-server.ts @@ -312,314 +312,318 @@ export async function startQaLabServer( return result; } - const server = createServer(async (req, res) => { - const url = new URL(req.url ?? "/", "http://127.0.0.1"); + const server = createServer((req, res) => { + void (async () => { + const url = new URL(req.url ?? "/", "http://127.0.0.1"); - if (await handleQaBusRequest({ req, res, state })) { - return; - } - - try { - if (controlUiProxyTarget && isControlUiProxyPath(url.pathname)) { - await proxyHttpRequest({ - req, - res, - target: controlUiProxyTarget, - pathname: url.pathname, - search: url.search, - authorizationToken: controlUiProxyToken, - }); + if (await handleQaBusRequest({ req, res, state })) { return; } - if (req.method === "GET" && url.pathname === "/api/bootstrap") { - void ensureRunnerModelCatalog(); - const resolvedControlUiUrl = controlUiProxyTarget - ? `${publicBaseUrl}/control-ui/` - : controlUiUrl; - const safeControlUiUrl = sanitizeControlUiPublicUrl(resolvedControlUiUrl); - writeJson(res, 200, { - baseUrl: publicBaseUrl, - latestReport, - controlUiUrl: safeControlUiUrl, - controlUiEmbeddedUrl: safeControlUiUrl, - kickoffTask: scenarioCatalog.kickoffTask, - scenarios: scenarioCatalog.scenarios, - defaults: bootstrapDefaults, - runner: runnerSnapshot, - runnerCatalog: { - status: runnerModelCatalogStatus, - real: runnerModelOptions, - }, - }); - return; - } - if (req.method === "GET" && (url.pathname === "/healthz" || url.pathname === "/readyz")) { - writeJson(res, 200, { ok: true, status: "live" }); - return; - } - if (req.method === "GET" && url.pathname === "/api/state") { - writeJson(res, 200, state.getSnapshot()); - return; - } - if (req.method === "GET" && url.pathname === "/api/report") { - writeJson(res, 200, { report: latestReport }); - return; - } - if (req.method === "GET" && url.pathname === "/api/ui-version") { - res.writeHead(200, { - "content-type": "application/json; charset=utf-8", - "cache-control": "no-store", - }); - res.end(JSON.stringify({ version: resolveUiAssetVersion(params?.uiDistDir) })); - return; - } - if (req.method === "GET" && url.pathname === "/api/outcomes") { - writeJson(res, 200, { run: latestScenarioRun }); - return; - } - if (req.method === "GET" && url.pathname === "/api/capture/sessions") { - writeJson(res, 200, { - sessions: captureStore.listSessions(50), - }); - return; - } - if (req.method === "GET" && url.pathname === "/api/capture/startup-status") { - const proxyUrl = captureSettings.proxyUrl || "http://127.0.0.1:7799"; - const gatewayUrl = controlUiUrl || "http://127.0.0.1:18789/"; - const [proxy, gatewayLocal] = await Promise.all([ - probeTcpReachability(proxyUrl), - probeTcpReachability(gatewayUrl), - ]); - writeJson(res, 200, { - status: { - proxy: { - ...proxy, - label: "Proxy", - }, - gateway: { - ...gatewayLocal, - label: "Gateway", - }, - qaLab: { - label: "QA Lab", - url: publicBaseUrl, - ok: true, - }, - }, - }); - return; - } - if (req.method === "GET" && url.pathname === "/api/capture/events") { - const sessionId = url.searchParams.get("sessionId")?.trim(); - writeJson(res, 200, { - events: sessionId - ? captureStore.getSessionEvents(sessionId, 200).map(mapCaptureEventForQa) - : [], - }); - return; - } - if (req.method === "GET" && url.pathname === "/api/capture/coverage") { - const sessionId = url.searchParams.get("sessionId")?.trim(); - if (!sessionId) { - writeError(res, 400, "Missing sessionId"); + try { + if (controlUiProxyTarget && isControlUiProxyPath(url.pathname)) { + await proxyHttpRequest({ + req, + res, + target: controlUiProxyTarget, + pathname: url.pathname, + search: url.search, + authorizationToken: controlUiProxyToken, + }); return; } - writeJson(res, 200, { - coverage: captureStore.summarizeSessionCoverage(sessionId), - }); - return; - } - if (req.method === "GET" && url.pathname === "/api/capture/query") { - const preset = url.searchParams.get("preset")?.trim(); - const sessionId = url.searchParams.get("sessionId")?.trim() || undefined; - if (!preset) { - writeError(res, 400, "Missing preset"); - return; - } - if (!isCaptureQueryPreset(preset)) { - writeError(res, 400, "Unknown preset"); - return; - } - writeJson(res, 200, { - rows: captureStore.queryPreset(preset, sessionId), - }); - return; - } - if (req.method === "GET" && url.pathname === "/api/capture/blob") { - const blobId = url.searchParams.get("id")?.trim(); - if (!blobId) { - writeError(res, 400, "Missing blob id"); - return; - } - const content = captureStore.readBlob(blobId); - if (content == null) { - writeError(res, 404, "Blob not found"); - return; - } - writeJson(res, 200, { id: blobId, content }); - return; - } - if (req.method === "POST" && url.pathname === "/api/capture/delete-sessions") { - const body = (await readQaJsonBody(req)) as { sessionIds?: unknown }; - const sessionIds = Array.isArray(body.sessionIds) - ? body.sessionIds.filter((value): value is string => typeof value === "string") - : []; - writeJson(res, 200, { - result: captureStore.deleteSessions(sessionIds), - }); - return; - } - if (req.method === "POST" && url.pathname === "/api/capture/purge") { - writeJson(res, 200, { - result: captureStore.purgeAll(), - }); - return; - } - if (req.method === "POST" && url.pathname === "/api/reset") { - if (activeSuiteRun) { - writeError(res, 409, "QA suite run already in progress"); - return; - } - state.reset(); - latestReport = null; - latestScenarioRun = null; - runnerSnapshot = { - ...runnerSnapshot, - status: "idle", - artifacts: null, - error: null, - startedAt: undefined, - finishedAt: undefined, - }; - writeJson(res, 200, { ok: true }); - return; - } - if (req.method === "POST" && url.pathname === "/api/inbound/message") { - const body = await readQaJsonBody(req); - writeJson(res, 200, { - message: state.addInboundMessage(body as Parameters[0]), - }); - return; - } - if (req.method === "POST" && url.pathname === "/api/kickoff") { - writeJson(res, 200, { - message: injectKickoffMessage({ - state, - defaults: bootstrapDefaults, + + if (req.method === "GET" && url.pathname === "/api/bootstrap") { + void ensureRunnerModelCatalog(); + const resolvedControlUiUrl = controlUiProxyTarget + ? `${publicBaseUrl}/control-ui/` + : controlUiUrl; + const safeControlUiUrl = sanitizeControlUiPublicUrl(resolvedControlUiUrl); + writeJson(res, 200, { + baseUrl: publicBaseUrl, + latestReport, + controlUiUrl: safeControlUiUrl, + controlUiEmbeddedUrl: safeControlUiUrl, kickoffTask: scenarioCatalog.kickoffTask, - }), - }); - return; - } - if (req.method === "POST" && url.pathname === "/api/scenario/self-check") { - if (activeSuiteRun) { - writeError(res, 409, "QA suite run already in progress"); + scenarios: scenarioCatalog.scenarios, + defaults: bootstrapDefaults, + runner: runnerSnapshot, + runnerCatalog: { + status: runnerModelCatalogStatus, + real: runnerModelOptions, + }, + }); return; } - const result = await runSelfCheck(); - writeJson(res, 200, serializeSelfCheck(result)); - return; - } - if (req.method === "POST" && url.pathname === "/api/scenario/suite") { - if (activeSuiteRun) { - writeError(res, 409, "QA suite run already in progress"); + if (req.method === "GET" && (url.pathname === "/healthz" || url.pathname === "/readyz")) { + writeJson(res, 200, { ok: true, status: "live" }); return; } - const selection = normalizeQaRunSelection( - await readQaJsonBody(req), - scenarioCatalog.scenarios, - ); - state.reset(); - latestReport = null; - latestScenarioRun = null; - const startedAt = new Date().toISOString(); - runnerSnapshot = { - status: "running", - selection, - startedAt, - finishedAt: undefined, - artifacts: null, - error: null, - }; - activeSuiteRun = (async () => { - try { - const { runQaSuite } = await import("./suite.js"); - const result = await runQaSuite({ - lab: labHandle ?? undefined, - startLab: startQaLabServer, - outputDir: createQaRunOutputDir(repoRoot), - providerMode: selection.providerMode, - primaryModel: selection.primaryModel, - alternateModel: selection.alternateModel, - scenarioIds: selection.scenarioIds, - }); - runnerSnapshot = { - status: "completed", - selection, - startedAt, - finishedAt: new Date().toISOString(), - artifacts: { - outputDir: result.outputDir, - reportPath: result.reportPath, - summaryPath: result.summaryPath, - watchUrl: result.watchUrl, + if (req.method === "GET" && url.pathname === "/api/state") { + writeJson(res, 200, state.getSnapshot()); + return; + } + if (req.method === "GET" && url.pathname === "/api/report") { + writeJson(res, 200, { report: latestReport }); + return; + } + if (req.method === "GET" && url.pathname === "/api/ui-version") { + res.writeHead(200, { + "content-type": "application/json; charset=utf-8", + "cache-control": "no-store", + }); + res.end(JSON.stringify({ version: resolveUiAssetVersion(params?.uiDistDir) })); + return; + } + if (req.method === "GET" && url.pathname === "/api/outcomes") { + writeJson(res, 200, { run: latestScenarioRun }); + return; + } + if (req.method === "GET" && url.pathname === "/api/capture/sessions") { + writeJson(res, 200, { + sessions: captureStore.listSessions(50), + }); + return; + } + if (req.method === "GET" && url.pathname === "/api/capture/startup-status") { + const proxyUrl = captureSettings.proxyUrl || "http://127.0.0.1:7799"; + const gatewayUrl = controlUiUrl || "http://127.0.0.1:18789/"; + const [proxy, gatewayLocal] = await Promise.all([ + probeTcpReachability(proxyUrl), + probeTcpReachability(gatewayUrl), + ]); + writeJson(res, 200, { + status: { + proxy: { + ...proxy, + label: "Proxy", }, - error: null, - }; - } catch (error) { - runnerSnapshot = { - status: "failed", - selection, - startedAt, - finishedAt: new Date().toISOString(), - artifacts: null, - error: formatErrorMessage(error), - }; - } finally { - activeSuiteRun = null; + gateway: { + ...gatewayLocal, + label: "Gateway", + }, + qaLab: { + label: "QA Lab", + url: publicBaseUrl, + ok: true, + }, + }, + }); + return; + } + if (req.method === "GET" && url.pathname === "/api/capture/events") { + const sessionId = url.searchParams.get("sessionId")?.trim(); + writeJson(res, 200, { + events: sessionId + ? captureStore.getSessionEvents(sessionId, 200).map(mapCaptureEventForQa) + : [], + }); + return; + } + if (req.method === "GET" && url.pathname === "/api/capture/coverage") { + const sessionId = url.searchParams.get("sessionId")?.trim(); + if (!sessionId) { + writeError(res, 400, "Missing sessionId"); + return; } - })(); - writeJson(res, 202, { - ok: true, - runner: runnerSnapshot, - }); - return; - } + writeJson(res, 200, { + coverage: captureStore.summarizeSessionCoverage(sessionId), + }); + return; + } + if (req.method === "GET" && url.pathname === "/api/capture/query") { + const preset = url.searchParams.get("preset")?.trim(); + const sessionId = url.searchParams.get("sessionId")?.trim() || undefined; + if (!preset) { + writeError(res, 400, "Missing preset"); + return; + } + if (!isCaptureQueryPreset(preset)) { + writeError(res, 400, "Unknown preset"); + return; + } + writeJson(res, 200, { + rows: captureStore.queryPreset(preset, sessionId), + }); + return; + } + if (req.method === "GET" && url.pathname === "/api/capture/blob") { + const blobId = url.searchParams.get("id")?.trim(); + if (!blobId) { + writeError(res, 400, "Missing blob id"); + return; + } + const content = captureStore.readBlob(blobId); + if (content == null) { + writeError(res, 404, "Blob not found"); + return; + } + writeJson(res, 200, { id: blobId, content }); + return; + } + if (req.method === "POST" && url.pathname === "/api/capture/delete-sessions") { + const body = (await readQaJsonBody(req)) as { sessionIds?: unknown }; + const sessionIds = Array.isArray(body.sessionIds) + ? body.sessionIds.filter((value): value is string => typeof value === "string") + : []; + writeJson(res, 200, { + result: captureStore.deleteSessions(sessionIds), + }); + return; + } + if (req.method === "POST" && url.pathname === "/api/capture/purge") { + writeJson(res, 200, { + result: captureStore.purgeAll(), + }); + return; + } + if (req.method === "POST" && url.pathname === "/api/reset") { + if (activeSuiteRun) { + writeError(res, 409, "QA suite run already in progress"); + return; + } + state.reset(); + latestReport = null; + latestScenarioRun = null; + runnerSnapshot = { + ...runnerSnapshot, + status: "idle", + artifacts: null, + error: null, + startedAt: undefined, + finishedAt: undefined, + }; + writeJson(res, 200, { ok: true }); + return; + } + if (req.method === "POST" && url.pathname === "/api/inbound/message") { + const body = await readQaJsonBody(req); + writeJson(res, 200, { + message: state.addInboundMessage( + body as Parameters[0], + ), + }); + return; + } + if (req.method === "POST" && url.pathname === "/api/kickoff") { + writeJson(res, 200, { + message: injectKickoffMessage({ + state, + defaults: bootstrapDefaults, + kickoffTask: scenarioCatalog.kickoffTask, + }), + }); + return; + } + if (req.method === "POST" && url.pathname === "/api/scenario/self-check") { + if (activeSuiteRun) { + writeError(res, 409, "QA suite run already in progress"); + return; + } + const result = await runSelfCheck(); + writeJson(res, 200, serializeSelfCheck(result)); + return; + } + if (req.method === "POST" && url.pathname === "/api/scenario/suite") { + if (activeSuiteRun) { + writeError(res, 409, "QA suite run already in progress"); + return; + } + const selection = normalizeQaRunSelection( + await readQaJsonBody(req), + scenarioCatalog.scenarios, + ); + state.reset(); + latestReport = null; + latestScenarioRun = null; + const startedAt = new Date().toISOString(); + runnerSnapshot = { + status: "running", + selection, + startedAt, + finishedAt: undefined, + artifacts: null, + error: null, + }; + activeSuiteRun = (async () => { + try { + const { runQaSuite } = await import("./suite.js"); + const result = await runQaSuite({ + lab: labHandle ?? undefined, + startLab: startQaLabServer, + outputDir: createQaRunOutputDir(repoRoot), + providerMode: selection.providerMode, + primaryModel: selection.primaryModel, + alternateModel: selection.alternateModel, + scenarioIds: selection.scenarioIds, + }); + runnerSnapshot = { + status: "completed", + selection, + startedAt, + finishedAt: new Date().toISOString(), + artifacts: { + outputDir: result.outputDir, + reportPath: result.reportPath, + summaryPath: result.summaryPath, + watchUrl: result.watchUrl, + }, + error: null, + }; + } catch (error) { + runnerSnapshot = { + status: "failed", + selection, + startedAt, + finishedAt: new Date().toISOString(), + artifacts: null, + error: formatErrorMessage(error), + }; + } finally { + activeSuiteRun = null; + } + })(); + writeJson(res, 202, { + ok: true, + runner: runnerSnapshot, + }); + return; + } - if (req.method !== "GET" && req.method !== "HEAD") { - writeError(res, 404, "not found"); - return; - } + if (req.method !== "GET" && req.method !== "HEAD") { + writeError(res, 404, "not found"); + return; + } - const asset = tryResolveUiAsset(url.pathname, params?.uiDistDir, repoRoot); - if (!asset) { - const html = missingUiHtml(); + const asset = tryResolveUiAsset(url.pathname, params?.uiDistDir, repoRoot); + if (!asset) { + const html = missingUiHtml(); + res.writeHead(200, { + "content-type": "text/html; charset=utf-8", + "content-length": Buffer.byteLength(html), + }); + if (req.method === "HEAD") { + res.end(); + return; + } + res.end(html); + return; + } + + const body = fs.readFileSync(asset); res.writeHead(200, { - "content-type": "text/html; charset=utf-8", - "content-length": Buffer.byteLength(html), + "content-type": detectContentType(asset), + "content-length": body.byteLength, }); if (req.method === "HEAD") { res.end(); return; } - res.end(html); - return; + res.end(body); + } catch (error) { + writeQaLabServerError(res, error); } - - const body = fs.readFileSync(asset); - res.writeHead(200, { - "content-type": detectContentType(asset), - "content-length": body.byteLength, - }); - if (req.method === "HEAD") { - res.end(); - return; - } - res.end(body); - } catch (error) { - writeQaLabServerError(res, error); - } + })(); }); await new Promise((resolve, reject) => { diff --git a/extensions/qa-lab/src/providers/mock-openai/server.ts b/extensions/qa-lab/src/providers/mock-openai/server.ts index fcd677995a6a..f9afa486712d 100644 --- a/extensions/qa-lab/src/providers/mock-openai/server.ts +++ b/extensions/qa-lab/src/providers/mock-openai/server.ts @@ -3046,163 +3046,165 @@ export async function startQaMockOpenAiServer(params?: { host?: string; port?: n let lastRequest: MockOpenAiRequestSnapshot | null = null; const requests: MockOpenAiRequestSnapshot[] = []; const imageGenerationRequests: Array> = []; - const server = createServer(async (req, res) => { - const url = new URL(req.url ?? "/", "http://127.0.0.1"); - if (req.method === "GET" && (url.pathname === "/healthz" || url.pathname === "/readyz")) { - writeJson(res, 200, { ok: true, status: "live" }); - return; - } - if (req.method === "GET" && url.pathname === "/v1/models") { - writeJson(res, 200, { - data: [ - { id: "gpt-5.5", object: "model" }, - { id: "gpt-5.5-alt", object: "model" }, - { id: "gpt-image-1", object: "model" }, - { id: "text-embedding-3-small", object: "model" }, - { id: "claude-opus-4-8", object: "model" }, - { id: "claude-sonnet-4-6", object: "model" }, - ], - }); - return; - } - if (req.method === "GET" && url.pathname === "/debug/last-request") { - writeJson(res, 200, lastRequest ?? { ok: false, error: "no request recorded" }); - return; - } - if (req.method === "GET" && url.pathname === "/debug/requests") { - writeJson(res, 200, requests); - return; - } - if (req.method === "GET" && url.pathname === "/debug/image-generations") { - writeJson(res, 200, imageGenerationRequests); - return; - } - if (req.method === "POST" && url.pathname === "/v1/images/generations") { - const raw = await readBody(req); - const body = raw ? (JSON.parse(raw) as Record) : {}; - imageGenerationRequests.push(body); - if (imageGenerationRequests.length > 20) { - imageGenerationRequests.splice(0, imageGenerationRequests.length - 20); - } - writeJson(res, 200, { - data: [ - { - b64_json: TINY_PNG_BASE64, - revised_prompt: "A QA lighthouse with protocol droid silhouette.", - }, - ], - }); - return; - } - if (req.method === "POST" && url.pathname === "/v1/embeddings") { - const raw = await readBody(req); - const body = raw ? (JSON.parse(raw) as Record) : {}; - const inputs = extractEmbeddingInputTexts(body.input); - writeJson(res, 200, { - object: "list", - data: inputs.map((text, index) => ({ - object: "embedding", - index, - embedding: buildDeterministicEmbedding(text), - })), - model: - typeof body.model === "string" && body.model.trim() - ? body.model - : "text-embedding-3-small", - usage: { - prompt_tokens: inputs.reduce((sum, text) => sum + countApproxTokens(text), 0), - total_tokens: inputs.reduce((sum, text) => sum + countApproxTokens(text), 0), - }, - }); - return; - } - if (req.method === "POST" && url.pathname === "/v1/responses") { - const raw = await readBody(req); - const body = raw ? (JSON.parse(raw) as Record) : {}; - const input = Array.isArray(body.input) ? (body.input as ResponsesInputItem[]) : []; - const events = await buildResponsesPayload(body, scenarioState); - const resolvedModel = typeof body.model === "string" ? body.model : ""; - lastRequest = { - raw, - body, - prompt: extractLastUserText(input), - allInputText: extractAllRequestTexts(input, body), - instructions: extractInstructionsText(body) || undefined, - toolOutput: extractToolOutput(input), - model: resolvedModel, - providerVariant: resolveProviderVariant(resolvedModel), - imageInputCount: countImageInputs(input), - plannedToolName: extractPlannedToolName(events), - plannedToolArgs: extractPlannedToolArgs(events), - }; - requests.push(lastRequest); - if (requests.length > MOCK_OPENAI_DEBUG_REQUEST_LIMIT) { - requests.splice(0, requests.length - MOCK_OPENAI_DEBUG_REQUEST_LIMIT); - } - if (body.stream === false) { - const completion = events.at(-1); - if (!completion || completion.type !== "response.completed") { - writeJson(res, 500, { error: "mock completion failed" }); - return; - } - writeJson(res, 200, completion.response); + const server = createServer((req, res) => { + void (async () => { + const url = new URL(req.url ?? "/", "http://127.0.0.1"); + if (req.method === "GET" && (url.pathname === "/healthz" || url.pathname === "/readyz")) { + writeJson(res, 200, { ok: true, status: "live" }); return; } - writeSse(res, events); - return; - } - if (req.method === "POST" && url.pathname === "/v1/messages") { - const raw = await readBody(req); - let body: AnthropicMessagesRequest = {}; - try { - body = raw ? (JSON.parse(raw) as AnthropicMessagesRequest) : {}; - } catch { - writeJson(res, 400, { - type: "error", - error: { - type: "invalid_request_error", - message: "Malformed JSON body for Anthropic Messages request.", + if (req.method === "GET" && url.pathname === "/v1/models") { + writeJson(res, 200, { + data: [ + { id: "gpt-5.5", object: "model" }, + { id: "gpt-5.5-alt", object: "model" }, + { id: "gpt-image-1", object: "model" }, + { id: "text-embedding-3-small", object: "model" }, + { id: "claude-opus-4-8", object: "model" }, + { id: "claude-sonnet-4-6", object: "model" }, + ], + }); + return; + } + if (req.method === "GET" && url.pathname === "/debug/last-request") { + writeJson(res, 200, lastRequest ?? { ok: false, error: "no request recorded" }); + return; + } + if (req.method === "GET" && url.pathname === "/debug/requests") { + writeJson(res, 200, requests); + return; + } + if (req.method === "GET" && url.pathname === "/debug/image-generations") { + writeJson(res, 200, imageGenerationRequests); + return; + } + if (req.method === "POST" && url.pathname === "/v1/images/generations") { + const raw = await readBody(req); + const body = raw ? (JSON.parse(raw) as Record) : {}; + imageGenerationRequests.push(body); + if (imageGenerationRequests.length > 20) { + imageGenerationRequests.splice(0, imageGenerationRequests.length - 20); + } + writeJson(res, 200, { + data: [ + { + b64_json: TINY_PNG_BASE64, + revised_prompt: "A QA lighthouse with protocol droid silhouette.", + }, + ], + }); + return; + } + if (req.method === "POST" && url.pathname === "/v1/embeddings") { + const raw = await readBody(req); + const body = raw ? (JSON.parse(raw) as Record) : {}; + const inputs = extractEmbeddingInputTexts(body.input); + writeJson(res, 200, { + object: "list", + data: inputs.map((text, index) => ({ + object: "embedding", + index, + embedding: buildDeterministicEmbedding(text), + })), + model: + typeof body.model === "string" && body.model.trim() + ? body.model + : "text-embedding-3-small", + usage: { + prompt_tokens: inputs.reduce((sum, text) => sum + countApproxTokens(text), 0), + total_tokens: inputs.reduce((sum, text) => sum + countApproxTokens(text), 0), }, }); return; } - const { - events, - input, - responseBody, - streamEvents, - model: normalizedModel, - } = await buildMessagesPayload(body, scenarioState); - // Record the adapted request snapshot so /debug/requests gives the QA - // suite the same plannedToolName / allInputText / toolOutput signals - // on the Anthropic route that the OpenAI route already exposes. This - // is what lets a single parity run diff assertions across both lanes. - // Reuse the normalized model so an empty-string body.model no longer - // leaks through to `lastRequest.model`. - lastRequest = { - raw, - body: body as Record, - prompt: extractLastUserText(input), - allInputText: extractAllInputTexts(input), - toolOutput: extractToolOutput(input), - model: normalizedModel, - providerVariant: resolveProviderVariant(normalizedModel), - imageInputCount: countImageInputs(input), - plannedToolName: extractPlannedToolName(events), - plannedToolArgs: extractPlannedToolArgs(events), - }; - requests.push(lastRequest); - if (requests.length > MOCK_OPENAI_DEBUG_REQUEST_LIMIT) { - requests.splice(0, requests.length - MOCK_OPENAI_DEBUG_REQUEST_LIMIT); - } - if (body.stream === true) { - writeAnthropicSse(res, streamEvents); + if (req.method === "POST" && url.pathname === "/v1/responses") { + const raw = await readBody(req); + const body = raw ? (JSON.parse(raw) as Record) : {}; + const input = Array.isArray(body.input) ? (body.input as ResponsesInputItem[]) : []; + const events = await buildResponsesPayload(body, scenarioState); + const resolvedModel = typeof body.model === "string" ? body.model : ""; + lastRequest = { + raw, + body, + prompt: extractLastUserText(input), + allInputText: extractAllRequestTexts(input, body), + instructions: extractInstructionsText(body) || undefined, + toolOutput: extractToolOutput(input), + model: resolvedModel, + providerVariant: resolveProviderVariant(resolvedModel), + imageInputCount: countImageInputs(input), + plannedToolName: extractPlannedToolName(events), + plannedToolArgs: extractPlannedToolArgs(events), + }; + requests.push(lastRequest); + if (requests.length > MOCK_OPENAI_DEBUG_REQUEST_LIMIT) { + requests.splice(0, requests.length - MOCK_OPENAI_DEBUG_REQUEST_LIMIT); + } + if (body.stream === false) { + const completion = events.at(-1); + if (!completion || completion.type !== "response.completed") { + writeJson(res, 500, { error: "mock completion failed" }); + return; + } + writeJson(res, 200, completion.response); + return; + } + writeSse(res, events); return; } - writeJson(res, 200, responseBody); - return; - } - writeJson(res, 404, { error: "not found" }); + if (req.method === "POST" && url.pathname === "/v1/messages") { + const raw = await readBody(req); + let body: AnthropicMessagesRequest = {}; + try { + body = raw ? (JSON.parse(raw) as AnthropicMessagesRequest) : {}; + } catch { + writeJson(res, 400, { + type: "error", + error: { + type: "invalid_request_error", + message: "Malformed JSON body for Anthropic Messages request.", + }, + }); + return; + } + const { + events, + input, + responseBody, + streamEvents, + model: normalizedModel, + } = await buildMessagesPayload(body, scenarioState); + // Record the adapted request snapshot so /debug/requests gives the QA + // suite the same plannedToolName / allInputText / toolOutput signals + // on the Anthropic route that the OpenAI route already exposes. This + // is what lets a single parity run diff assertions across both lanes. + // Reuse the normalized model so an empty-string body.model no longer + // leaks through to `lastRequest.model`. + lastRequest = { + raw, + body: body as Record, + prompt: extractLastUserText(input), + allInputText: extractAllInputTexts(input), + toolOutput: extractToolOutput(input), + model: normalizedModel, + providerVariant: resolveProviderVariant(normalizedModel), + imageInputCount: countImageInputs(input), + plannedToolName: extractPlannedToolName(events), + plannedToolArgs: extractPlannedToolArgs(events), + }; + requests.push(lastRequest); + if (requests.length > MOCK_OPENAI_DEBUG_REQUEST_LIMIT) { + requests.splice(0, requests.length - MOCK_OPENAI_DEBUG_REQUEST_LIMIT); + } + if (body.stream === true) { + writeAnthropicSse(res, streamEvents); + return; + } + writeJson(res, 200, responseBody); + return; + } + writeJson(res, 404, { error: "not found" }); + })(); }); await new Promise((resolve, reject) => { diff --git a/extensions/qa-lab/web/src/app.ts b/extensions/qa-lab/web/src/app.ts index 1668091f1527..78c64d635a26 100644 --- a/extensions/qa-lab/web/src/app.ts +++ b/extensions/qa-lab/web/src/app.ts @@ -958,28 +958,29 @@ export async function createQaLabApp(root: HTMLDivElement) { }); root .querySelector("#capture-delete-selected-sessions") - ?.addEventListener("click", async () => { - if (state.selectedCaptureSessionIds.length === 0) { - return; - } - const confirmed = window.confirm( - `Delete ${state.selectedCaptureSessionIds.length} selected capture session${ - state.selectedCaptureSessionIds.length === 1 ? "" : "s" - }?`, - ); - if (!confirmed) { - return; - } - await postJson("/api/capture/delete-sessions", { - sessionIds: state.selectedCaptureSessionIds, - }); - state.selectedCaptureSessionIds = []; - state.selectedCaptureEventKey = null; - await refresh(); + ?.addEventListener("click", () => { + void (async () => { + if (state.selectedCaptureSessionIds.length === 0) { + return; + } + const confirmed = window.confirm( + `Delete ${state.selectedCaptureSessionIds.length} selected capture session${ + state.selectedCaptureSessionIds.length === 1 ? "" : "s" + }?`, + ); + if (!confirmed) { + return; + } + await postJson("/api/capture/delete-sessions", { + sessionIds: state.selectedCaptureSessionIds, + }); + state.selectedCaptureSessionIds = []; + state.selectedCaptureEventKey = null; + await refresh(); + })(); }); - root - .querySelector("#capture-purge-all") - ?.addEventListener("click", async () => { + root.querySelector("#capture-purge-all")?.addEventListener("click", () => { + void (async () => { const confirmed = window.confirm("Purge all captured sessions, events, and blobs?"); if (!confirmed) { return; @@ -988,7 +989,8 @@ export async function createQaLabApp(root: HTMLDivElement) { state.selectedCaptureSessionIds = []; state.selectedCaptureEventKey = null; await refresh(); - }); + })(); + }); root.querySelector("#capture-preset")?.addEventListener("change", (e) => { state.captureQueryPreset = (e.currentTarget as HTMLSelectElement) .value as UiState["captureQueryPreset"]; @@ -1327,12 +1329,12 @@ export async function createQaLabApp(root: HTMLDivElement) { }); }); root.querySelectorAll("[data-copy-text]").forEach((node) => { - node.addEventListener("click", async () => { + node.addEventListener("click", () => { const text = node.dataset.copyText ?? ""; if (!text) { return; } - await navigator.clipboard.writeText(text).catch(() => undefined); + void navigator.clipboard.writeText(text).catch(() => undefined); }); }); root.querySelectorAll("[data-capture-sparkline-window]").forEach((node) => { diff --git a/extensions/qa-matrix/src/substrate/fault-proxy.test.ts b/extensions/qa-matrix/src/substrate/fault-proxy.test.ts index 52a2d533c8e9..4ac155086be6 100644 --- a/extensions/qa-matrix/src/substrate/fault-proxy.test.ts +++ b/extensions/qa-matrix/src/substrate/fault-proxy.test.ts @@ -11,19 +11,21 @@ async function startTargetServer(params?: { responseBody?: string }) { method: string; url: string; }> = []; - const server = createServer(async (req, res) => { - const chunks: Buffer[] = []; - for await (const chunk of req) { - chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); - } - requests.push({ - ...(req.headers.authorization ? { authorization: req.headers.authorization } : {}), - body: Buffer.concat(chunks).toString("utf8"), - method: req.method ?? "GET", - url: req.url ?? "/", - }); - res.writeHead(200, { "content-type": "application/json" }); - res.end(params?.responseBody ?? JSON.stringify({ forwarded: true })); + const server = createServer((req, res) => { + void (async () => { + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); + } + requests.push({ + ...(req.headers.authorization ? { authorization: req.headers.authorization } : {}), + body: Buffer.concat(chunks).toString("utf8"), + method: req.method ?? "GET", + url: req.url ?? "/", + }); + res.writeHead(200, { "content-type": "application/json" }); + res.end(params?.responseBody ?? JSON.stringify({ forwarded: true })); + })(); }); await new Promise((resolve, reject) => { server.once("error", reject); diff --git a/extensions/qa-matrix/src/substrate/fault-proxy.ts b/extensions/qa-matrix/src/substrate/fault-proxy.ts index 427925793615..404a1b53b7e5 100644 --- a/extensions/qa-matrix/src/substrate/fault-proxy.ts +++ b/extensions/qa-matrix/src/substrate/fault-proxy.ts @@ -299,65 +299,67 @@ export async function startMatrixQaFaultProxy(params: { const maxRequestBytes = params.maxRequestBytes ?? DEFAULT_FAULT_PROXY_REQUEST_MAX_BYTES; const maxResponseBytes = params.maxResponseBytes ?? DEFAULT_FAULT_PROXY_RESPONSE_MAX_BYTES; const hits: MatrixQaFaultProxyHit[] = []; - const server = createServer(async (req, res) => { - try { - const requestUrl = new URL(req.url ?? "/", targetBaseUrl); - const path = requestUrl.pathname; - const bearerToken = extractBearerToken(req.headers); - const request: MatrixQaFaultProxyRequest = { - ...(bearerToken ? { bearerToken } : {}), - headers: req.headers, - method: req.method ?? "GET", - path, - search: requestUrl.search, - }; - const body = await readRequestBody(req, maxRequestBytes); - const rule = params.rules.find((candidate) => candidate.match(request)); - if (rule) { - hits.push({ - method: request.method, - path: request.path, - ruleId: rule.id, + const server = createServer((req, res) => { + void (async () => { + try { + const requestUrl = new URL(req.url ?? "/", targetBaseUrl); + const path = requestUrl.pathname; + const bearerToken = extractBearerToken(req.headers); + const request: MatrixQaFaultProxyRequest = { + ...(bearerToken ? { bearerToken } : {}), + headers: req.headers, + method: req.method ?? "GET", + path, + search: requestUrl.search, + }; + const body = await readRequestBody(req, maxRequestBytes); + const rule = params.rules.find((candidate) => candidate.match(request)); + if (rule) { + hits.push({ + method: request.method, + path: request.path, + ruleId: rule.id, + }); + if (rule.response) { + writeJsonResponse(res, rule.response(request)); + return; + } + } + const forwarded = await forwardMatrixQaFaultProxyRequest({ + body, + maxResponseBytes, + req, + targetUrl: requestUrl, }); - if (rule.response) { - writeJsonResponse(res, rule.response(request)); + const response = + rule?.mutateResponse !== undefined + ? await rule.mutateResponse({ + request, + response: forwarded, + }) + : forwarded; + writeForwardedResponse(res, response); + } catch (error) { + if (error instanceof MatrixQaFaultProxyHttpError) { + writeJsonResponse(res, { + body: { + errcode: error.code, + error: error.message, + }, + ...(error.status === 413 ? { headers: { connection: "close" } } : {}), + status: error.status, + }); return; } - } - const forwarded = await forwardMatrixQaFaultProxyRequest({ - body, - maxResponseBytes, - req, - targetUrl: requestUrl, - }); - const response = - rule?.mutateResponse !== undefined - ? await rule.mutateResponse({ - request, - response: forwarded, - }) - : forwarded; - writeForwardedResponse(res, response); - } catch (error) { - if (error instanceof MatrixQaFaultProxyHttpError) { writeJsonResponse(res, { body: { - errcode: error.code, - error: error.message, + errcode: "MATRIX_QA_FAULT_PROXY_ERROR", + error: error instanceof Error ? error.message : String(error), }, - ...(error.status === 413 ? { headers: { connection: "close" } } : {}), - status: error.status, + status: 502, }); - return; } - writeJsonResponse(res, { - body: { - errcode: "MATRIX_QA_FAULT_PROXY_ERROR", - error: error instanceof Error ? error.message : String(error), - }, - status: 502, - }); - } + })(); }); await new Promise((resolve, reject) => { diff --git a/extensions/qqbot/src/engine/api/media-chunked.test.ts b/extensions/qqbot/src/engine/api/media-chunked.test.ts index 7ef3e884ffe5..e356c12d8878 100644 --- a/extensions/qqbot/src/engine/api/media-chunked.test.ts +++ b/extensions/qqbot/src/engine/api/media-chunked.test.ts @@ -29,10 +29,10 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ // ============ Test doubles ============ /** Build a minimal ApiClient stub whose `request` is fully mockable. */ -function mockApiClient(): ApiClient & { request: ReturnType } { +function mockApiClient(): ApiClient & { request: ReturnType> } { return { - request: vi.fn(), - } as unknown as ApiClient & { request: ReturnType }; + request: vi.fn(), + } as unknown as ApiClient & { request: ReturnType> }; } /** Minimal TokenManager stub returning a static token. */ @@ -200,22 +200,23 @@ describe("media-chunked: ChunkedMediaApi.uploadChunked", () => { // plus one complete. Because concurrency=2 the order of part_finish is // not strictly deterministic, so match on path + payload key. client.request.mockImplementation( - async (_token: string, _method: string, pathLocal: string, body: Record) => { + async (_token: string, _method: string, pathLocal: string, body: unknown) => { + const uploadBody = body as Record; if (pathLocal.endsWith("/upload_prepare")) { - expect(body.file_type).toBe(MediaFileType.FILE); - expect(typeof body.md5).toBe("string"); - expect(typeof body.sha1).toBe("string"); - expect(typeof body.md5_10m).toBe("string"); - expect(body.file_size).toBe(FIXTURE_BUFFER.length); + expect(uploadBody.file_type).toBe(MediaFileType.FILE); + expect(typeof uploadBody.md5).toBe("string"); + expect(typeof uploadBody.sha1).toBe("string"); + expect(typeof uploadBody.md5_10m).toBe("string"); + expect(uploadBody.file_size).toBe(FIXTURE_BUFFER.length); return prepareResp; } if (pathLocal.endsWith("/upload_part_finish")) { - expect(body.upload_id).toBe("uid-1"); - expect(typeof body.part_index).toBe("number"); + expect(uploadBody.upload_id).toBe("uid-1"); + expect(typeof uploadBody.part_index).toBe("number"); return {}; } if (pathLocal.endsWith("/files")) { - expect(body.upload_id).toBe("uid-1"); + expect(uploadBody.upload_id).toBe("uid-1"); return completeResp; } throw new Error(`unexpected path ${pathLocal}`); @@ -323,9 +324,7 @@ describe("media-chunked: ChunkedMediaApi.uploadChunked", () => { expect(result.file_info).toBe("fi"); // Verify prepare received the md5 of the on-disk bytes. - const prepareCall = client.request.mock.calls.find((c) => - String(c[2]).endsWith("/upload_prepare"), - )!; + const prepareCall = client.request.mock.calls.find((c) => c[2].endsWith("/upload_prepare"))!; const prepareBody = prepareCall[3] as { md5: string; file_name: string }; expect(prepareBody.md5).toBe(crypto.createHash("md5").update(FIXTURE_BUFFER).digest("hex")); expect(prepareBody.file_name).toBe("fixture.bin"); @@ -368,9 +367,7 @@ describe("media-chunked: ChunkedMediaApi.uploadChunked", () => { creds: { appId: "a", clientSecret: "s" }, }); - const prepareCall = client.request.mock.calls.find((c) => - String(c[2]).endsWith("/upload_prepare"), - )!; + const prepareCall = client.request.mock.calls.find((c) => c[2].endsWith("/upload_prepare"))!; const prepareBody = prepareCall[3] as { md5: string }; expect(prepareBody.md5).toBe(crypto.createHash("md5").update(FIXTURE_BUFFER).digest("hex")); } finally { diff --git a/extensions/qqbot/src/engine/gateway/gateway-connection.ts b/extensions/qqbot/src/engine/gateway/gateway-connection.ts index 5bbfedb93284..afa01693db5c 100644 --- a/extensions/qqbot/src/engine/gateway/gateway-connection.ts +++ b/extensions/qqbot/src/engine/gateway/gateway-connection.ts @@ -209,7 +209,7 @@ export class GatewayConnection { }); // ---- WebSocket: message ---- - ws.on("message", async (data) => { + ws.on("message", (data) => { try { const rawData = decodeGatewayMessageData(data); const payload = JSON.parse(rawData) as WSPayload; diff --git a/extensions/qqbot/src/engine/gateway/outbound-dispatch.ts b/extensions/qqbot/src/engine/gateway/outbound-dispatch.ts index 5eb17ab42c5b..3a0f27d5699b 100644 --- a/extensions/qqbot/src/engine/gateway/outbound-dispatch.ts +++ b/extensions/qqbot/src/engine/gateway/outbound-dispatch.ts @@ -196,12 +196,10 @@ export async function dispatchOutbound( clearTimeout(toolOnlyTimeoutId); toolRenewalCount++; } - toolOnlyTimeoutId = setTimeout(async () => { + toolOnlyTimeoutId = setTimeout(() => { if (!hasBlockResponse && !toolFallbackSent) { toolFallbackSent = true; - try { - await sendToolFallback(); - } catch {} + void sendToolFallback().catch(() => {}); } }, TOOL_ONLY_TIMEOUT); return true; diff --git a/extensions/slack/src/monitor/slash.test-harness.ts b/extensions/slack/src/monitor/slash.test-harness.ts index cf58259efe38..25560e6939c1 100644 --- a/extensions/slack/src/monitor/slash.test-harness.ts +++ b/extensions/slack/src/monitor/slash.test-harness.ts @@ -1,5 +1,7 @@ import { vi } from "vitest"; +type AsyncMock = ReturnType Promise>>; + const mocks = vi.hoisted(() => ({ dispatchMock: vi.fn(), readAllowFromStoreMock: vi.fn(), @@ -7,7 +9,7 @@ const mocks = vi.hoisted(() => ({ resolveAgentRouteMock: vi.fn(), finalizeInboundContextMock: vi.fn(), resolveConversationLabelMock: vi.fn(), - recordSessionMetaFromInboundMock: vi.fn(), + recordSessionMetaFromInboundMock: vi.fn<(...args: unknown[]) => Promise>(), resolveStorePathMock: vi.fn(), })); @@ -32,7 +34,7 @@ type SlashHarnessMocks = { resolveAgentRouteMock: ReturnType; finalizeInboundContextMock: ReturnType; resolveConversationLabelMock: ReturnType; - recordSessionMetaFromInboundMock: ReturnType; + recordSessionMetaFromInboundMock: AsyncMock; resolveStorePathMock: ReturnType; }; diff --git a/extensions/slack/src/send.upload.test.ts b/extensions/slack/src/send.upload.test.ts index bda7b20457a1..13693c113a8a 100644 --- a/extensions/slack/src/send.upload.test.ts +++ b/extensions/slack/src/send.upload.test.ts @@ -59,11 +59,11 @@ const { sendMessageSlack, clearSlackDmChannelCache, clearSlackSendQueuesForTest const SLACK_TEST_CFG = { channels: { slack: { botToken: "xoxb-test" } } }; type UploadTestClient = WebClient & { - conversations: { open: ReturnType }; - chat: { postMessage: ReturnType }; + conversations: { open: ReturnType Promise>> }; + chat: { postMessage: ReturnType Promise>> }; files: { - getUploadURLExternal: ReturnType; - completeUploadExternal: ReturnType; + getUploadURLExternal: ReturnType Promise>>; + completeUploadExternal: ReturnType Promise>>; }; }; @@ -135,18 +135,24 @@ function expectCompletedUpload(params: { function createUploadTestClient(): UploadTestClient { return { conversations: { - open: vi.fn(async () => ({ channel: { id: "D99RESOLVED" } })), + open: vi.fn<(...args: unknown[]) => Promise>(async () => ({ + channel: { id: "D99RESOLVED" }, + })), }, chat: { - postMessage: vi.fn(async () => ({ ts: "171234.567" })), + postMessage: vi.fn<(...args: unknown[]) => Promise>(async () => ({ + ts: "171234.567", + })), }, files: { - getUploadURLExternal: vi.fn(async () => ({ + getUploadURLExternal: vi.fn<(...args: unknown[]) => Promise>(async () => ({ ok: true, upload_url: "https://uploads.slack.test/upload", file_id: "F001", })), - completeUploadExternal: vi.fn(async () => ({ ok: true })), + completeUploadExternal: vi.fn<(...args: unknown[]) => Promise>(async () => ({ + ok: true, + })), }, } as unknown as UploadTestClient; } @@ -235,8 +241,12 @@ describe("sendMessageSlack file upload with user IDs", () => { it("serializes concurrent sends to the same Slack target", async () => { const client = createUploadTestClient(); let resolveFirst: (() => void) | undefined; - client.chat.postMessage.mockImplementation(async (payload: { text?: string }) => { - if (payload.text === "first") { + client.chat.postMessage.mockImplementation(async (payload: unknown) => { + const text = + typeof payload === "object" && payload !== null && "text" in payload + ? payload.text + : undefined; + if (text === "first") { await new Promise((resolve) => { resolveFirst = resolve; }); diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index f30a5f4adf77..4ed2e3ac469c 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -961,8 +961,8 @@ export const registerTelegramHandlers = ({ const scheduleTextFragmentFlush = (entry: TextFragmentEntry) => { clearTimeout(entry.timer); - entry.timer = setTimeout(async () => { - await runTextFragmentFlush(entry); + entry.timer = setTimeout(() => { + void runTextFragmentFlush(entry); }, TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS); }; @@ -1797,9 +1797,9 @@ export const registerTelegramHandlers = ({ existing.dispatchDedupeKeys, dispatchDedupeKeys, ); - existing.timer = setTimeout(async () => { + existing.timer = setTimeout(() => { mediaGroupBuffer.delete(mediaGroupKey); - await queueBufferedProcessing(mediaGroupProcessingByKey, mediaGroupKey, async () => { + void queueBufferedProcessing(mediaGroupProcessingByKey, mediaGroupKey, async () => { await processMediaGroup(existing); }); }, mediaGroupTimeoutMs); @@ -1818,9 +1818,9 @@ export const registerTelegramHandlers = ({ topicConfig, dispatchDedupeKeys, ...promptContextBoundaryOptions(promptContextMinTimestampMs), - timer: setTimeout(async () => { + timer: setTimeout(() => { mediaGroupBuffer.delete(mediaGroupKey); - await queueBufferedProcessing(mediaGroupProcessingByKey, mediaGroupKey, async () => { + void queueBufferedProcessing(mediaGroupProcessingByKey, mediaGroupKey, async () => { await processMediaGroup(entry); }); }, mediaGroupTimeoutMs), diff --git a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts index 58bab57aef9e..05ea188c7be7 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts @@ -8,7 +8,7 @@ import { beforeEach, vi } from "vitest"; import type { TelegramBotDeps } from "./bot-deps.js"; type AnyMock = ReturnType; -type AnyAsyncMock = ReturnType; +type AnyAsyncMock = ReturnType Promise>>; type GetRuntimeConfigFn = typeof import("openclaw/plugin-sdk/runtime-config-snapshot").getRuntimeConfig; type LoadSessionStoreFn = @@ -103,7 +103,7 @@ export function setSessionStoreEntriesForTest(entries: SessionStore) { const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted( (): { readChannelAllowFromStore: MockFn; - upsertChannelPairingRequest: AnyAsyncMock; + upsertChannelPairingRequest: MockFn; } => ({ readChannelAllowFromStore: vi.fn(async () => [] as string[]), upsertChannelPairingRequest: vi.fn(async () => ({ @@ -113,20 +113,26 @@ const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted( }), ); -export function getReadChannelAllowFromStoreMock(): AnyAsyncMock { +export function getReadChannelAllowFromStoreMock(): MockFn< + TelegramBotDeps["readChannelAllowFromStore"] +> { return readChannelAllowFromStore; } -export function getUpsertChannelPairingRequestMock(): AnyAsyncMock { +export function getUpsertChannelPairingRequestMock(): MockFn< + TelegramBotDeps["upsertChannelPairingRequest"] +> { return upsertChannelPairingRequest; } const skillCommandListHoisted = vi.hoisted(() => ({ listSkillCommandsForAgents: vi.fn(() => []), })); -const modelProviderDataHoisted = vi.hoisted(() => ({ - buildModelsProviderData: vi.fn(), -})); +const modelProviderDataHoisted = vi.hoisted( + (): { buildModelsProviderData: MockFn } => ({ + buildModelsProviderData: vi.fn(), + }), +); const replySpyHoisted = vi.hoisted(() => ({ replySpy: vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => { await opts?.onReplyStart?.(); @@ -163,7 +169,7 @@ async function dispatchHarnessReplies( await params.dispatcherOptions.deliver?.(finalPayload, { kind: "final" }); finalCount += 1; } catch (err) { - params.dispatcherOptions.onError?.(err, { kind: "final" }); + void params.dispatcherOptions.onError?.(err, { kind: "final" }); } } return { diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index ac37a10c70fc..742f823af16e 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -47,6 +47,9 @@ const { throttlerSpy, useSpy, } = harness; +type BuildModelsProviderDataMock = ReturnType< + typeof vi.fn> +>; const { resolveTelegramFetch } = await import("./fetch.js"); const { createTelegramBotCore: createTelegramBotBase, @@ -630,7 +633,7 @@ describe("createTelegramBot", () => { clearTimeout( setTimeoutSpy.mock.results[debounceCallIndex]?.value as ReturnType, ); - return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as (() => Promise) | undefined; + return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as (() => void) | undefined; }; try { @@ -650,7 +653,7 @@ describe("createTelegramBot", () => { }); const flushFirst = extractLatestDebounceFlush(); - const firstFlush = flushFirst?.(); + flushFirst?.(); await vi.waitFor( () => { @@ -674,7 +677,7 @@ describe("createTelegramBot", () => { }); const flushSecond = extractLatestDebounceFlush(); - const secondFlush = flushSecond?.(); + flushSecond?.(); await Promise.resolve(); expect(startedBodies).toHaveLength(1); @@ -684,7 +687,6 @@ describe("createTelegramBot", () => { throw new Error("Expected first Telegram run release callback to be initialized"); } releaseFirstRun(); - await Promise.all([firstFlush, secondFlush]); await vi.waitFor( () => { @@ -743,9 +745,7 @@ describe("createTelegramBot", () => { clearTimeout( setTimeoutSpy.mock.results[debounceCallIndex]?.value as ReturnType, ); - return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as - | (() => Promise) - | undefined; + return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as (() => void) | undefined; }; try { @@ -791,7 +791,8 @@ describe("createTelegramBot", () => { expect(startedBodies).toHaveLength(1); expect(startedBodies[0]).toContain("stop"); - await flushFirst?.(); + flushFirst?.(); + await Promise.resolve(); expect(startedBodies).toHaveLength(1); expect(sendMessageSpy.mock.calls.map((call) => String(call[1])).join("\n")).not.toContain( "reply:first", @@ -814,8 +815,13 @@ describe("createTelegramBot", () => { }); const flushReplay = extractLatestDebounceFlush(); - await flushReplay?.(); - expect(startedBodies).toHaveLength(2); + flushReplay?.(); + await vi.waitFor( + () => { + expect(startedBodies).toHaveLength(2); + }, + { interval: 1, timeout: 500 }, + ); expect(startedBodies[1]).toContain("first"); } finally { setTimeoutSpy.mockRestore(); @@ -858,7 +864,7 @@ describe("createTelegramBot", () => { clearTimeout( setTimeoutSpy.mock.results[debounceCallIndex]?.value as ReturnType, ); - return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as (() => Promise) | undefined; + return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as (() => void) | undefined; }; try { @@ -905,7 +911,8 @@ describe("createTelegramBot", () => { expect(startedBodies).toHaveLength(1); expect(startedBodies[0]).toContain("stop"); - await flushForward?.(); + flushForward?.(); + await Promise.resolve(); expect(startedBodies).toHaveLength(1); expect(sendMessageSpy.mock.calls.map((call) => String(call[1])).join("\n")).not.toContain( "reply:forwarded first", @@ -956,7 +963,7 @@ describe("createTelegramBot", () => { clearTimeout( setTimeoutSpy.mock.results[debounceCallIndex]?.value as ReturnType, ); - return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as (() => Promise) | undefined; + return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as (() => void) | undefined; }; try { @@ -999,7 +1006,7 @@ describe("createTelegramBot", () => { finalHandler: messageHandler, }); - await flushFirst?.(); + flushFirst?.(); await vi.waitFor(() => { expect(startedBodies.some((body) => body.includes("first"))).toBe(true); }); @@ -1213,7 +1220,7 @@ describe("createTelegramBot", () => { }); it("reloads callback model routing bindings without recreating the bot", async () => { const buildModelsProviderDataMock = - telegramBotDepsForTest.buildModelsProviderData as unknown as ReturnType; + telegramBotDepsForTest.buildModelsProviderData as unknown as BuildModelsProviderDataMock; let boundAgentId = "agent-a"; loadConfig.mockImplementation(() => ({ agents: { @@ -4166,7 +4173,7 @@ describe("createTelegramBot", () => { }); const buildModelsProviderDataMock = - telegramBotDepsForTest.buildModelsProviderData as unknown as ReturnType; + telegramBotDepsForTest.buildModelsProviderData as unknown as BuildModelsProviderDataMock; buildModelsProviderDataMock.mockClear(); editMessageTextSpy.mockClear(); @@ -4674,12 +4681,13 @@ describe("createTelegramBot", () => { } expect(editMessageTextSpy).toHaveBeenCalledTimes(1); - expect(String(editMessageTextSpy.mock.calls.at(-1)?.[2] ?? "")).toContain( + const finalEditMessageText = editMessageTextSpy.mock.calls.at(-1)?.[2]; + expect(typeof finalEditMessageText === "string" ? finalEditMessageText : "").toContain( "Session-only model selection. Runtime unchanged.", ); expect( editMessageTextSpy.mock.calls.some((call) => - String(call[2] ?? "").includes("Failed to change model"), + (typeof call[2] === "string" ? call[2] : "").includes("Failed to change model"), ), ).toBe(false); }); diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index 11f1bd373278..b6422d0b7a61 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -1092,7 +1092,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { + void handleChannelsFirehose(event); + }, err: (error) => { runtime.error?.(`[tlon] Channels firehose error: ${String(error)}`); }, @@ -1106,7 +1108,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { + void handleChatFirehose(event); + }, err: (error) => { runtime.error?.(`[tlon] Chat firehose error: ${String(error)}`); }, @@ -1196,81 +1200,36 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { - try { - const eventRecord = asRecord(event); - // Handle group/channel join events - // Event structure: { group: { flag: "~host/group-name", ... }, channels: { ... } } - if (eventRecord) { - // Check for new channels being added to groups - const channels = asRecord(eventRecord.channels); - if (channels) { - for (const [channelNest, _channelData] of Object.entries(channels)) { - // Only monitor chat channels - if (!channelNest.startsWith("chat/")) { - continue; - } - - // If this is a new channel we're not watching yet, add it - if (!watchedChannels.has(channelNest)) { - watchedChannels.add(channelNest); - runtime.log?.( - `[tlon] Auto-detected new channel (invite accepted): ${channelNest}`, - ); - - // Persist to settings store so it survives restarts - if (effectiveAutoAcceptGroupInvites) { - try { - const currentChannels = currentSettings.groupChannels || []; - if (!currentChannels.includes(channelNest)) { - const updatedChannels = [...currentChannels, channelNest]; - // Poke settings store to persist - await api.poke({ - app: "settings", - mark: "settings-event", - json: { - "put-entry": { - "bucket-key": "tlon", - "entry-key": "groupChannels", - value: updatedChannels, - desk: "moltbot", - }, - }, - }); - runtime.log?.(`[tlon] Persisted ${channelNest} to settings store`); - } - } catch (err) { - runtime.error?.( - `[tlon] Failed to persist channel to settings: ${String(err)}`, - ); - } - } - } - } - } - - // Also check for the "join" event structure - const join = asRecord(eventRecord.join); - if (join) { - const joinChannels = Array.isArray(join.channels) ? join.channels : []; - if (joinChannels.length > 0) { - for (const channelNest of joinChannels) { - if (typeof channelNest !== "string") { - continue; - } + event: (event: unknown) => { + void (async () => { + try { + const eventRecord = asRecord(event); + // Handle group/channel join events + // Event structure: { group: { flag: "~host/group-name", ... }, channels: { ... } } + if (eventRecord) { + // Check for new channels being added to groups + const channels = asRecord(eventRecord.channels); + if (channels) { + for (const [channelNest, _channelData] of Object.entries(channels)) { + // Only monitor chat channels if (!channelNest.startsWith("chat/")) { continue; } + + // If this is a new channel we're not watching yet, add it if (!watchedChannels.has(channelNest)) { watchedChannels.add(channelNest); - runtime.log?.(`[tlon] Auto-detected joined channel: ${channelNest}`); + runtime.log?.( + `[tlon] Auto-detected new channel (invite accepted): ${channelNest}`, + ); - // Persist to settings store + // Persist to settings store so it survives restarts if (effectiveAutoAcceptGroupInvites) { try { const currentChannels = currentSettings.groupChannels || []; if (!currentChannels.includes(channelNest)) { const updatedChannels = [...currentChannels, channelNest]; + // Poke settings store to persist await api.poke({ app: "settings", mark: "settings-event", @@ -1294,11 +1253,60 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise 0) { + for (const channelNest of joinChannels) { + if (typeof channelNest !== "string") { + continue; + } + if (!channelNest.startsWith("chat/")) { + continue; + } + if (!watchedChannels.has(channelNest)) { + watchedChannels.add(channelNest); + runtime.log?.(`[tlon] Auto-detected joined channel: ${channelNest}`); + + // Persist to settings store + if (effectiveAutoAcceptGroupInvites) { + try { + const currentChannels = currentSettings.groupChannels || []; + if (!currentChannels.includes(channelNest)) { + const updatedChannels = [...currentChannels, channelNest]; + await api.poke({ + app: "settings", + mark: "settings-event", + json: { + "put-entry": { + "bucket-key": "tlon", + "entry-key": "groupChannels", + value: updatedChannels, + desk: "moltbot", + }, + }, + }); + runtime.log?.(`[tlon] Persisted ${channelNest} to settings store`); + } + } catch (err) { + runtime.error?.( + `[tlon] Failed to persist channel to settings: ${String(err)}`, + ); + } + } + } + } + } + } } + } catch (error: unknown) { + runtime.error?.( + `[tlon] Error handling groups-ui event: ${formatErrorMessage(error)}`, + ); } - } catch (error: unknown) { - runtime.error?.(`[tlon] Error handling groups-ui event: ${formatErrorMessage(error)}`); - } + })(); }, err: (error) => { runtime.error?.(`[tlon] Groups-ui subscription error: ${String(error)}`); @@ -1469,22 +1477,24 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { - if (!opts.abortSignal?.aborted) { - try { - if (effectiveAutoDiscoverChannels) { - const discoveredChannels = await fetchAllChannels(api, runtime); - for (const channelNest of discoveredChannels) { - if (!watchedChannels.has(channelNest)) { - watchedChannels.add(channelNest); - runtime.log?.(`[tlon] Now watching new channel: ${channelNest}`); + () => { + void (async () => { + if (!opts.abortSignal?.aborted) { + try { + if (effectiveAutoDiscoverChannels) { + const discoveredChannels = await fetchAllChannels(api, runtime); + for (const channelNest of discoveredChannels) { + if (!watchedChannels.has(channelNest)) { + watchedChannels.add(channelNest); + runtime.log?.(`[tlon] Now watching new channel: ${channelNest}`); + } } } + } catch (error: unknown) { + runtime.error?.(`[tlon] Channel refresh error: ${formatErrorMessage(error)}`); } - } catch (error: unknown) { - runtime.error?.(`[tlon] Channel refresh error: ${formatErrorMessage(error)}`); } - } + })(); }, 2 * 60 * 1000, ); diff --git a/extensions/voice-call/src/manager/outbound.ts b/extensions/voice-call/src/manager/outbound.ts index 5d29e5654755..6d53290f973e 100644 --- a/extensions/voice-call/src/manager/outbound.ts +++ b/extensions/voice-call/src/manager/outbound.ts @@ -383,12 +383,14 @@ export async function speakInitialMessage( const delaySec = ctx.config.outbound.notifyHangupDelaySec; const delayMs = resolveVoiceCallSecondsTimerDelayMs(delaySec, 0); console.log(`[voice-call] Notify mode: auto-hangup in ${delaySec}s for call ${call.callId}`); - setTimeout(async () => { - const currentCall = ctx.activeCalls.get(call.callId); - if (currentCall && !TerminalStates.has(currentCall.state)) { - console.log(`[voice-call] Notify mode: hanging up call ${call.callId}`); - await endCall(ctx, call.callId); - } + setTimeout(() => { + void (async () => { + const currentCall = ctx.activeCalls.get(call.callId); + if (currentCall && !TerminalStates.has(currentCall.state)) { + console.log(`[voice-call] Notify mode: hanging up call ${call.callId}`); + await endCall(ctx, call.callId); + } + })(); }, delayMs); } else if ( mode === "conversation" && diff --git a/extensions/voice-call/src/manager/timers.ts b/extensions/voice-call/src/manager/timers.ts index 45568ac94d25..763cc53fed01 100644 --- a/extensions/voice-call/src/manager/timers.ts +++ b/extensions/voice-call/src/manager/timers.ts @@ -43,17 +43,19 @@ export function startMaxDurationTimer(params: { `[voice-call] Starting max duration timer (${Math.ceil(maxDurationMs / 1000)}s) for call ${params.callId}`, ); - const timer = setTimeout(async () => { - params.ctx.maxDurationTimers.delete(params.callId); - const call = params.ctx.activeCalls.get(params.callId); - if (call && !TerminalStates.has(call.state)) { - console.log( - `[voice-call] Max duration reached (${Math.ceil(maxDurationMs / 1000)}s), ending call ${params.callId}`, - ); - call.endReason = "timeout"; - persistCallRecord(params.ctx.storePath, call); - await params.onTimeout(params.callId); - } + const timer = setTimeout(() => { + void (async () => { + params.ctx.maxDurationTimers.delete(params.callId); + const call = params.ctx.activeCalls.get(params.callId); + if (call && !TerminalStates.has(call.state)) { + console.log( + `[voice-call] Max duration reached (${Math.ceil(maxDurationMs / 1000)}s), ending call ${params.callId}`, + ); + call.endReason = "timeout"; + persistCallRecord(params.ctx.storePath, call); + await params.onTimeout(params.callId); + } + })(); }, maxDurationMs); params.ctx.maxDurationTimers.set(params.callId, timer); diff --git a/extensions/voice-call/src/media-stream.ts b/extensions/voice-call/src/media-stream.ts index 471a28f767f1..00d9e676deee 100644 --- a/extensions/voice-call/src/media-stream.ts +++ b/extensions/voice-call/src/media-stream.ts @@ -176,7 +176,9 @@ export class MediaStreamHandler { // Reject oversized frames before app-level parsing runs on unauthenticated sockets. maxPayload: MAX_INBOUND_MESSAGE_BYTES, }); - this.wss.on("connection", (ws, req) => this.handleConnection(ws, req)); + this.wss.on("connection", (ws, req) => { + void this.handleConnection(ws, req); + }); } const currentConnections = this.getCurrentConnectionCount(); @@ -230,7 +232,7 @@ export class MediaStreamHandler { return; } - ws.on("message", async (data: RawData) => { + ws.on("message", (data: RawData) => { try { const message = parseTwilioMediaMessage(data); diff --git a/extensions/whatsapp/src/session.ts b/extensions/whatsapp/src/session.ts index ce5ad11d6b0e..fb141dffea6d 100644 --- a/extensions/whatsapp/src/session.ts +++ b/extensions/whatsapp/src/session.ts @@ -188,34 +188,36 @@ export async function createWaSocket( }); sock.ev.on("creds.update", () => enqueueSaveCreds(authDir, saveCreds, sessionLogger)); - sock.ev.on("connection.update", async (update: Partial) => { - try { - const { connection, lastDisconnect, qr } = update; - if (qr) { - opts.onQr?.(qr); - if (printQr) { - console.log("Open the WhatsApp app, go to Linked Devices, then scan this QR:"); - void printTerminalQr(qr).catch((err) => { - sessionLogger.warn({ error: String(err) }, "failed rendering WhatsApp QR"); - }); + sock.ev.on("connection.update", (update: Partial) => { + void (async () => { + try { + const { connection, lastDisconnect, qr } = update; + if (qr) { + opts.onQr?.(qr); + if (printQr) { + console.log("Open the WhatsApp app, go to Linked Devices, then scan this QR:"); + void printTerminalQr(qr).catch((err) => { + sessionLogger.warn({ error: String(err) }, "failed rendering WhatsApp QR"); + }); + } } - } - if (connection === "close") { - const status = getStatusCode(lastDisconnect?.error); - if (status === LOGGED_OUT_STATUS) { - console.error( - danger( - `WhatsApp session logged out. Run: ${formatCliCommand("openclaw channels login")}`, - ), - ); + if (connection === "close") { + const status = getStatusCode(lastDisconnect?.error); + if (status === LOGGED_OUT_STATUS) { + console.error( + danger( + `WhatsApp session logged out. Run: ${formatCliCommand("openclaw channels login")}`, + ), + ); + } } + if (connection === "open" && verbose) { + console.log(success("WhatsApp Web connected.")); + } + } catch (err) { + sessionLogger.error({ error: String(err) }, "connection.update handler error"); } - if (connection === "open" && verbose) { - console.log(success("WhatsApp Web connected.")); - } - } catch (err) { - sessionLogger.error({ error: String(err) }, "connection.update handler error"); - } + })(); }); // Handle WebSocket-level errors to prevent unhandled exceptions from crashing the process diff --git a/extensions/zalo/src/monitor.pairing.lifecycle.test.ts b/extensions/zalo/src/monitor.pairing.lifecycle.test.ts index 7abfb79bd385..cf011c86fedd 100644 --- a/extensions/zalo/src/monitor.pairing.lifecycle.test.ts +++ b/extensions/zalo/src/monitor.pairing.lifecycle.test.ts @@ -51,7 +51,9 @@ describe("Zalo pairing lifecycle", () => { try { await withServer( - (req, res) => monitor.route.handler(req, res), + (req, res) => { + void monitor.route.handler(req, res); + }, async (baseUrl) => { const { first, replay } = await postWebhookReplay({ baseUrl, @@ -108,7 +110,9 @@ describe("Zalo pairing lifecycle", () => { try { await withServer( - (req, res) => monitor.route.handler(req, res), + (req, res) => { + void monitor.route.handler(req, res); + }, async (baseUrl) => { const { first, replay } = await postWebhookReplay({ baseUrl, diff --git a/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts b/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts index 807606306b6e..e0e85e5c51a1 100644 --- a/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts +++ b/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts @@ -83,7 +83,9 @@ describe("Zalo reply-once lifecycle", () => { try { await withServer( - (req, res) => monitor.route.handler(req, res), + (req, res) => { + void monitor.route.handler(req, res); + }, async (baseUrl) => { const { first, replay } = await postWebhookReplay({ baseUrl, @@ -145,7 +147,9 @@ describe("Zalo reply-once lifecycle", () => { try { await withServer( - (req, res) => monitor.route.handler(req, res), + (req, res) => { + void monitor.route.handler(req, res); + }, async (baseUrl) => { const { first, replay } = await postWebhookReplay({ baseUrl, diff --git a/extensions/zalo/src/monitor.ts b/extensions/zalo/src/monitor.ts index 429fe3f495c5..806f3f4a7490 100644 --- a/extensions/zalo/src/monitor.ts +++ b/extensions/zalo/src/monitor.ts @@ -287,7 +287,9 @@ function startPollingLoop(params: ZaloPollingLoopParams) { } if (!isStopped() && !abortSignal.aborted) { - setImmediate(poll); + setImmediate(() => { + void poll(); + }); } }; diff --git a/extensions/zalo/src/monitor.webhook.test.ts b/extensions/zalo/src/monitor.webhook.test.ts index e36d949d454c..c59a91bb96c8 100644 --- a/extensions/zalo/src/monitor.webhook.test.ts +++ b/extensions/zalo/src/monitor.webhook.test.ts @@ -34,14 +34,16 @@ const DEFAULT_ACCOUNT: ResolvedZaloAccount = { }; function createWebhookRequestHandler(processUpdate?: ZaloWebhookProcessUpdate): RequestListener { - return async (req, res) => { - const handled = processUpdate - ? await handleZaloWebhookRequestInternal(req, res, processUpdate) - : await handleZaloWebhookRequest(req, res); - if (!handled) { - res.statusCode = 404; - res.end("not found"); - } + return (req, res) => { + void (async () => { + const handled = processUpdate + ? await handleZaloWebhookRequestInternal(req, res, processUpdate) + : await handleZaloWebhookRequest(req, res); + if (!handled) { + res.statusCode = 404; + res.end("not found"); + } + })(); }; } diff --git a/scripts/anthropic-prompt-probe.ts b/scripts/anthropic-prompt-probe.ts index ea56cc522204..ac648bed5d5a 100644 --- a/scripts/anthropic-prompt-probe.ts +++ b/scripts/anthropic-prompt-probe.ts @@ -311,59 +311,61 @@ function extractProxyCapture(rawBody: string, req: http.IncomingMessage): ProxyC async function startAnthropicProxy(params: { port: number; upstreamBaseUrl: string }) { let lastCapture: ProxyCapture | undefined; const sockets = new Set(); - const server = http.createServer(async (req, res) => { - try { - const method = req.method ?? "GET"; - const requestBody = await readRequestBody(req); - const rawBody = requestBody.toString("utf8"); - lastCapture = extractProxyCapture(rawBody, req); + const server = http.createServer((req, res) => { + void (async () => { + try { + const method = req.method ?? "GET"; + const requestBody = await readRequestBody(req); + const rawBody = requestBody.toString("utf8"); + lastCapture = extractProxyCapture(rawBody, req); - const upstreamUrl = resolveAnthropicUpstreamUrl(req.url, params.upstreamBaseUrl); - const headers = new Headers(); - for (const [key, value] of Object.entries(req.headers)) { - if (value === undefined) { - continue; + const upstreamUrl = resolveAnthropicUpstreamUrl(req.url, params.upstreamBaseUrl); + const headers = new Headers(); + for (const [key, value] of Object.entries(req.headers)) { + if (value === undefined) { + continue; + } + const lower = key.toLowerCase(); + if (lower === "host" || lower === "content-length") { + continue; + } + headers.set(key, Array.isArray(value) ? value.join(", ") : value); } - const lower = key.toLowerCase(); - if (lower === "host" || lower === "content-length") { - continue; + const upstreamRes = await fetch(upstreamUrl, { + method, + headers, + body: + method === "GET" || method === "HEAD" || requestBody.byteLength === 0 + ? undefined + : requestBody, + duplex: "half", + }); + const responseHeaders: Record = {}; + for (const [key, value] of upstreamRes.headers.entries()) { + const lower = key.toLowerCase(); + if ( + lower === "content-length" || + lower === "content-encoding" || + lower === "transfer-encoding" || + lower === "connection" || + lower === "keep-alive" + ) { + continue; + } + responseHeaders[key] = value; } - headers.set(key, Array.isArray(value) ? value.join(", ") : value); + res.writeHead(upstreamRes.status, responseHeaders); + if (upstreamRes.body) { + for await (const chunk of upstreamRes.body) { + res.write(Buffer.from(chunk)); + } + } + res.end(); + } catch (error) { + res.writeHead(502, { "content-type": "text/plain; charset=utf-8" }); + res.end(redactForDevToolLog(`proxy error: ${String(error)}`)); } - const upstreamRes = await fetch(upstreamUrl, { - method, - headers, - body: - method === "GET" || method === "HEAD" || requestBody.byteLength === 0 - ? undefined - : requestBody, - duplex: "half", - }); - const responseHeaders: Record = {}; - for (const [key, value] of upstreamRes.headers.entries()) { - const lower = key.toLowerCase(); - if ( - lower === "content-length" || - lower === "content-encoding" || - lower === "transfer-encoding" || - lower === "connection" || - lower === "keep-alive" - ) { - continue; - } - responseHeaders[key] = value; - } - res.writeHead(upstreamRes.status, responseHeaders); - if (upstreamRes.body) { - for await (const chunk of upstreamRes.body) { - res.write(Buffer.from(chunk)); - } - } - res.end(); - } catch (error) { - res.writeHead(502, { "content-type": "text/plain; charset=utf-8" }); - res.end(redactForDevToolLog(`proxy error: ${String(error)}`)); - } + })(); }); server.on("connection", (socket) => { sockets.add(socket); diff --git a/scripts/e2e/lib/openai-web-search-minimal/mock-server.mjs b/scripts/e2e/lib/openai-web-search-minimal/mock-server.mjs index 0a377859ac9a..1b56acdb3127 100644 --- a/scripts/e2e/lib/openai-web-search-minimal/mock-server.mjs +++ b/scripts/e2e/lib/openai-web-search-minimal/mock-server.mjs @@ -81,48 +81,50 @@ function responseEvents(text) { ]; } -const server = http.createServer(async (req, res) => { - const url = new URL(req.url ?? "/", "http://127.0.0.1"); - if (req.method === "GET" && url.pathname === "/health") { - writeJson(res, 200, { ok: true }); - return; - } - if (req.method === "GET" && url.pathname === "/v1/models") { - writeJson(res, 200, { - object: "list", - data: [{ id: "gpt-5", object: "model", owned_by: "openclaw-e2e" }], +const server = http.createServer((req, res) => { + void (async () => { + const url = new URL(req.url ?? "/", "http://127.0.0.1"); + if (req.method === "GET" && url.pathname === "/health") { + writeJson(res, 200, { ok: true }); + return; + } + if (req.method === "GET" && url.pathname === "/v1/models") { + writeJson(res, 200, { + object: "list", + data: [{ id: "gpt-5", object: "model", owned_by: "openclaw-e2e" }], + }); + return; + } + + const bodyText = await readBody(req); + let body = {}; + try { + body = bodyText ? JSON.parse(bodyText) : {}; + } catch { + body = {}; + } + fs.appendFileSync( + requestLog, + `${JSON.stringify({ method: req.method, path: url.pathname, body })}\n`, + ); + + if (req.method === "POST" && url.pathname === "/v1/responses") { + if (bodyContainsForceReject(body)) { + writeOpenAiReject(res); + return; + } + if (body?.reasoning?.effort === "minimal" && hasWebSearchTool(body.tools)) { + writeOpenAiReject(res); + return; + } + writeSse(res, responseEvents(successMarker)); + return; + } + + writeJson(res, 404, { + error: { message: `unhandled mock route: ${req.method} ${url.pathname}` }, }); - return; - } - - const bodyText = await readBody(req); - let body = {}; - try { - body = bodyText ? JSON.parse(bodyText) : {}; - } catch { - body = {}; - } - fs.appendFileSync( - requestLog, - `${JSON.stringify({ method: req.method, path: url.pathname, body })}\n`, - ); - - if (req.method === "POST" && url.pathname === "/v1/responses") { - if (bodyContainsForceReject(body)) { - writeOpenAiReject(res); - return; - } - if (body?.reasoning?.effort === "minimal" && hasWebSearchTool(body.tools)) { - writeOpenAiReject(res); - return; - } - writeSse(res, responseEvents(successMarker)); - return; - } - - writeJson(res, 404, { - error: { message: `unhandled mock route: ${req.method} ${url.pathname}` }, - }); + })(); }); server.listen(port, "127.0.0.1", () => { diff --git a/scripts/e2e/lib/release-user-journey/clickclack-fixture.mjs b/scripts/e2e/lib/release-user-journey/clickclack-fixture.mjs index ab7683230a1f..7e4e1441cc87 100644 --- a/scripts/e2e/lib/release-user-journey/clickclack-fixture.mjs +++ b/scripts/e2e/lib/release-user-journey/clickclack-fixture.mjs @@ -171,78 +171,80 @@ function broadcast(event) { } } -const server = http.createServer(async (req, res) => { - const url = new URL(req.url ?? "/", "http://127.0.0.1"); - if (!checkAuth(req, res)) { - return; - } - if (req.method === "GET" && url.pathname === "/health") { - json(res, 200, { ok: true }); - return; - } - if (req.method === "GET" && url.pathname === "/api/me") { - json(res, 200, { user: botUser }); - return; - } - if (req.method === "GET" && url.pathname === "/api/workspaces") { - json(res, 200, { workspaces: [workspace] }); - return; - } - if (req.method === "GET" && url.pathname === `/api/workspaces/${workspace.id}/channels`) { - json(res, 200, { channels: [channel] }); - return; - } - if (req.method === "GET" && url.pathname === `/api/channels/${channel.id}/messages`) { - const afterSeq = Number(url.searchParams.get("after_seq") ?? 0); - json(res, 200, { - messages: messages.filter((message) => (message.channel_seq ?? 0) > afterSeq), - }); - return; - } - if (req.method === "POST" && url.pathname === `/api/channels/${channel.id}/messages`) { - const body = await readBody(req); - const message = createMessage({ body: String(body.body ?? ""), author: botUser }); - outboundMessages.push(message); - persist(); - json(res, 200, { message }); - return; - } - const threadReplyMatch = url.pathname.match(/^\/api\/messages\/([^/]+)\/thread\/replies$/u); - if (req.method === "POST" && threadReplyMatch) { - const body = await readBody(req); - const message = createMessage({ - body: String(body.body ?? ""), - author: botUser, - parentMessageId: decodeURIComponent(threadReplyMatch[1]), - }); - json(res, 200, { message }); - return; - } - const threadMatch = url.pathname.match(/^\/api\/messages\/([^/]+)\/thread$/u); - if (req.method === "GET" && threadMatch) { - const rootId = decodeURIComponent(threadMatch[1]); - json(res, 200, { - root: messages.find((message) => message.id === rootId) ?? null, - replies: threadReplies.filter((message) => message.thread_root_id === rootId), - }); - return; - } - if (req.method === "GET" && url.pathname === "/api/realtime/events") { - json(res, 200, { events: [] }); - return; - } - if (req.method === "POST" && url.pathname === "/fixture/inbound") { - const body = await readBody(req); - const message = createMessage({ body: String(body.body ?? ""), author: humanUser }); - broadcast(eventFor(message)); - json(res, 200, { message }); - return; - } - if (req.method === "GET" && url.pathname === "/fixture/state") { - json(res, 200, { messages, threadReplies, outboundMessages, socketCount: sockets.size }); - return; - } - json(res, 404, { error: `unhandled ${req.method} ${url.pathname}` }); +const server = http.createServer((req, res) => { + void (async () => { + const url = new URL(req.url ?? "/", "http://127.0.0.1"); + if (!checkAuth(req, res)) { + return; + } + if (req.method === "GET" && url.pathname === "/health") { + json(res, 200, { ok: true }); + return; + } + if (req.method === "GET" && url.pathname === "/api/me") { + json(res, 200, { user: botUser }); + return; + } + if (req.method === "GET" && url.pathname === "/api/workspaces") { + json(res, 200, { workspaces: [workspace] }); + return; + } + if (req.method === "GET" && url.pathname === `/api/workspaces/${workspace.id}/channels`) { + json(res, 200, { channels: [channel] }); + return; + } + if (req.method === "GET" && url.pathname === `/api/channels/${channel.id}/messages`) { + const afterSeq = Number(url.searchParams.get("after_seq") ?? 0); + json(res, 200, { + messages: messages.filter((message) => (message.channel_seq ?? 0) > afterSeq), + }); + return; + } + if (req.method === "POST" && url.pathname === `/api/channels/${channel.id}/messages`) { + const body = await readBody(req); + const message = createMessage({ body: String(body.body ?? ""), author: botUser }); + outboundMessages.push(message); + persist(); + json(res, 200, { message }); + return; + } + const threadReplyMatch = url.pathname.match(/^\/api\/messages\/([^/]+)\/thread\/replies$/u); + if (req.method === "POST" && threadReplyMatch) { + const body = await readBody(req); + const message = createMessage({ + body: String(body.body ?? ""), + author: botUser, + parentMessageId: decodeURIComponent(threadReplyMatch[1]), + }); + json(res, 200, { message }); + return; + } + const threadMatch = url.pathname.match(/^\/api\/messages\/([^/]+)\/thread$/u); + if (req.method === "GET" && threadMatch) { + const rootId = decodeURIComponent(threadMatch[1]); + json(res, 200, { + root: messages.find((message) => message.id === rootId) ?? null, + replies: threadReplies.filter((message) => message.thread_root_id === rootId), + }); + return; + } + if (req.method === "GET" && url.pathname === "/api/realtime/events") { + json(res, 200, { events: [] }); + return; + } + if (req.method === "POST" && url.pathname === "/fixture/inbound") { + const body = await readBody(req); + const message = createMessage({ body: String(body.body ?? ""), author: humanUser }); + broadcast(eventFor(message)); + json(res, 200, { message }); + return; + } + if (req.method === "GET" && url.pathname === "/fixture/state") { + json(res, 200, { messages, threadReplies, outboundMessages, socketCount: sockets.size }); + return; + } + json(res, 404, { error: `unhandled ${req.method} ${url.pathname}` }); + })(); }); server.on("upgrade", (req, socket) => { diff --git a/scripts/e2e/mock-openai-server.mjs b/scripts/e2e/mock-openai-server.mjs index 81239677ce6f..8fc279b55485 100644 --- a/scripts/e2e/mock-openai-server.mjs +++ b/scripts/e2e/mock-openai-server.mjs @@ -115,90 +115,92 @@ function resolveResponseText(bodyText) { return matches.at(-1)?.[0] ?? successMarker; } -const server = http.createServer(async (req, res) => { - const url = new URL(req.url ?? "/", "http://127.0.0.1"); - if (req.method === "GET" && url.pathname === "/health") { - writeJson(res, 200, { ok: true }); - return; - } - if (req.method === "GET" && url.pathname === "/v1/models") { - writeJson(res, 200, { - object: "list", - data: [{ id: "gpt-5.5", object: "model", owned_by: "openclaw-e2e" }], - }); - return; - } - - const bodyText = await readBody(req); - if (requestLog) { - fs.appendFileSync( - requestLog, - `${JSON.stringify({ method: req.method, path: url.pathname, body: bodyText })}\n`, - ); - } - let body = {}; - try { - body = bodyText ? JSON.parse(bodyText) : {}; - } catch { - body = {}; - } - - if (req.method === "POST" && url.pathname === "/v1/responses") { - const responseText = resolveResponseText(bodyText); - if (body.stream === false) { +const server = http.createServer((req, res) => { + void (async () => { + const url = new URL(req.url ?? "/", "http://127.0.0.1"); + if (req.method === "GET" && url.pathname === "/health") { + writeJson(res, 200, { ok: true }); + return; + } + if (req.method === "GET" && url.pathname === "/v1/models") { writeJson(res, 200, { - id: "resp_e2e", - object: "response", - status: "completed", - output: [ - { - type: "message", - id: "msg_e2e_1", - role: "assistant", - status: "completed", - content: [{ type: "output_text", text: responseText, annotations: [] }], - }, - ], - usage: { input_tokens: 11, output_tokens: 7, total_tokens: 18 }, + object: "list", + data: [{ id: "gpt-5.5", object: "model", owned_by: "openclaw-e2e" }], }); return; } - writeSse(res, responseEvents(responseText)); - return; - } - if (req.method === "POST" && url.pathname === "/v1/chat/completions") { - const responseText = resolveResponseText(bodyText); - writeChatCompletion(res, body.stream !== false, responseText); - return; - } + const bodyText = await readBody(req); + if (requestLog) { + fs.appendFileSync( + requestLog, + `${JSON.stringify({ method: req.method, path: url.pathname, body: bodyText })}\n`, + ); + } + let body = {}; + try { + body = bodyText ? JSON.parse(bodyText) : {}; + } catch { + body = {}; + } - if (req.method === "POST" && url.pathname === "/v1/embeddings") { - const input = Array.isArray(body.input) ? body.input : [body.input ?? ""]; - writeJson(res, 200, { - object: "list", - data: input.map((_, index) => ({ - object: "embedding", - index, - embedding: [1, index / 100, 0, 0], - })), - model: body.model ?? "text-embedding-3-small", - usage: { prompt_tokens: input.length, total_tokens: input.length }, + if (req.method === "POST" && url.pathname === "/v1/responses") { + const responseText = resolveResponseText(bodyText); + if (body.stream === false) { + writeJson(res, 200, { + id: "resp_e2e", + object: "response", + status: "completed", + output: [ + { + type: "message", + id: "msg_e2e_1", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: responseText, annotations: [] }], + }, + ], + usage: { input_tokens: 11, output_tokens: 7, total_tokens: 18 }, + }); + return; + } + writeSse(res, responseEvents(responseText)); + return; + } + + if (req.method === "POST" && url.pathname === "/v1/chat/completions") { + const responseText = resolveResponseText(bodyText); + writeChatCompletion(res, body.stream !== false, responseText); + return; + } + + if (req.method === "POST" && url.pathname === "/v1/embeddings") { + const input = Array.isArray(body.input) ? body.input : [body.input ?? ""]; + writeJson(res, 200, { + object: "list", + data: input.map((_, index) => ({ + object: "embedding", + index, + embedding: [1, index / 100, 0, 0], + })), + model: body.model ?? "text-embedding-3-small", + usage: { prompt_tokens: input.length, total_tokens: input.length }, + }); + return; + } + + if ( + req.method === "POST" && + (url.pathname === "/v1/images/generations" || url.pathname === "/v1/images/edits") + ) { + writeImageGeneration(res); + return; + } + + writeJson(res, 404, { + error: { message: `unhandled mock route: ${req.method} ${url.pathname}` }, }); - return; - } - - if ( - req.method === "POST" && - (url.pathname === "/v1/images/generations" || url.pathname === "/v1/images/edits") - ) { - writeImageGeneration(res); - return; - } - - writeJson(res, 404, { - error: { message: `unhandled mock route: ${req.method} ${url.pathname}` }, - }); + })(); }); server.listen(port, "127.0.0.1", () => { diff --git a/scripts/e2e/openai-image-auth-docker-client.ts b/scripts/e2e/openai-image-auth-docker-client.ts index d59ef2b0ffbb..e6e5bd42b76e 100644 --- a/scripts/e2e/openai-image-auth-docker-client.ts +++ b/scripts/e2e/openai-image-auth-docker-client.ts @@ -67,71 +67,73 @@ async function startMockServer(records: RequestRecord[]): Promise<{ baseUrl: string; close: () => Promise; }> { - const server = http.createServer(async (req, res) => { - try { - const body = await readBody(req); - records.push({ - method: req.method, - url: req.url, - authorization: req.headers.authorization, - accept: req.headers.accept, - contentType: req.headers["content-type"], - body, - }); - - if (req.method === "POST" && req.url === "/v1/images/generations") { - assert( - req.headers.authorization === `Bearer ${DIRECT_TOKEN}`, - `direct image route used wrong auth: ${req.headers.authorization}`, - ); - const parsed = JSON.parse(body) as { model?: string; prompt?: string; size?: string }; - assert(parsed.model === "gpt-image-2", `direct route model mismatch: ${body}`); - assert( - parsed.prompt === "docker direct image auth", - `direct route prompt mismatch: ${body}`, - ); - assert(parsed.size === "1024x1024", `direct route size mismatch: ${body}`); - writeJson(res, 200, { - data: [ - { - b64_json: DIRECT_IMAGE_BYTES.toString("base64"), - revised_prompt: "docker direct revised prompt", - }, - ], + const server = http.createServer((req, res) => { + void (async () => { + try { + const body = await readBody(req); + records.push({ + method: req.method, + url: req.url, + authorization: req.headers.authorization, + accept: req.headers.accept, + contentType: req.headers["content-type"], + body, }); - return; - } - if (req.method === "POST" && req.url === "/backend-api/codex/responses") { - assert( - req.headers.authorization === `Bearer ${CODEX_TOKEN}`, - `codex image route used wrong auth: ${req.headers.authorization}`, - ); - const parsed = JSON.parse(body) as { - tools?: Array<{ type?: string; model?: string; size?: string }>; - input?: Array<{ content?: Array<{ type?: string; text?: string }> }>; - }; - assert( - parsed.tools?.[0]?.type === "image_generation" && - parsed.tools[0].model === "gpt-image-2" && - parsed.tools[0].size === "1024x1024", - `codex image tool mismatch: ${body}`, - ); - assert( - parsed.input?.[0]?.content?.some( - (entry) => - entry.type === "input_text" && entry.text === "docker codex oauth image auth", - ), - `codex prompt missing: ${body}`, - ); - writeCodexSse(res); - return; - } + if (req.method === "POST" && req.url === "/v1/images/generations") { + assert( + req.headers.authorization === `Bearer ${DIRECT_TOKEN}`, + `direct image route used wrong auth: ${req.headers.authorization}`, + ); + const parsed = JSON.parse(body) as { model?: string; prompt?: string; size?: string }; + assert(parsed.model === "gpt-image-2", `direct route model mismatch: ${body}`); + assert( + parsed.prompt === "docker direct image auth", + `direct route prompt mismatch: ${body}`, + ); + assert(parsed.size === "1024x1024", `direct route size mismatch: ${body}`); + writeJson(res, 200, { + data: [ + { + b64_json: DIRECT_IMAGE_BYTES.toString("base64"), + revised_prompt: "docker direct revised prompt", + }, + ], + }); + return; + } - writeJson(res, 404, { error: `unexpected ${req.method} ${req.url}` }); - } catch (error) { - writeJson(res, 500, { error: String(error instanceof Error ? error.message : error) }); - } + if (req.method === "POST" && req.url === "/backend-api/codex/responses") { + assert( + req.headers.authorization === `Bearer ${CODEX_TOKEN}`, + `codex image route used wrong auth: ${req.headers.authorization}`, + ); + const parsed = JSON.parse(body) as { + tools?: Array<{ type?: string; model?: string; size?: string }>; + input?: Array<{ content?: Array<{ type?: string; text?: string }> }>; + }; + assert( + parsed.tools?.[0]?.type === "image_generation" && + parsed.tools[0].model === "gpt-image-2" && + parsed.tools[0].size === "1024x1024", + `codex image tool mismatch: ${body}`, + ); + assert( + parsed.input?.[0]?.content?.some( + (entry) => + entry.type === "input_text" && entry.text === "docker codex oauth image auth", + ), + `codex prompt missing: ${body}`, + ); + writeCodexSse(res); + return; + } + + writeJson(res, 404, { error: `unexpected ${req.method} ${req.url}` }); + } catch (error) { + writeJson(res, 500, { error: String(error instanceof Error ? error.message : error) }); + } + })(); }); await new Promise((resolve) => { diff --git a/scripts/e2e/parallels/host-command.ts b/scripts/e2e/parallels/host-command.ts index 56df8f2df804..3b6871ef84ed 100644 --- a/scripts/e2e/parallels/host-command.ts +++ b/scripts/e2e/parallels/host-command.ts @@ -205,18 +205,20 @@ export async function runStreaming( }, options.timeoutMs); child.on("error", reject); - child.on("close", async (code, signal) => { - if (timer) { - clearTimeout(timer); - } - if (options.logPath) { - await writeFile(options.logPath, log, "utf8"); - } - if (timedOut) { - resolve(124); - } else { - resolve(code ?? (signal ? 128 : 1)); - } + child.on("close", (code, signal) => { + void (async () => { + if (timer) { + clearTimeout(timer); + } + if (options.logPath) { + await writeFile(options.logPath, log, "utf8"); + } + if (timedOut) { + resolve(124); + } else { + resolve(code ?? (signal ? 128 : 1)); + } + })(); }); }); } diff --git a/scripts/e2e/parallels/npm-update-smoke.ts b/scripts/e2e/parallels/npm-update-smoke.ts index c2fa116df514..09899f64659b 100755 --- a/scripts/e2e/parallels/npm-update-smoke.ts +++ b/scripts/e2e/parallels/npm-update-smoke.ts @@ -654,9 +654,11 @@ class NpmUpdateSmoke { onOutput(text); }); child.on("error", reject); - child.on("close", async (code) => { - await writeFile(logPath, log, "utf8"); - resolve(code ?? 1); + child.on("close", (code) => { + void (async () => { + await writeFile(logPath, log, "utf8"); + resolve(code ?? 1); + })(); }); }); } diff --git a/scripts/qa-otel-smoke.ts b/scripts/qa-otel-smoke.ts index a33654ad58eb..cc0448b4efdb 100644 --- a/scripts/qa-otel-smoke.ts +++ b/scripts/qa-otel-smoke.ts @@ -693,69 +693,71 @@ function startLocalOtlpReceiver(disallowedBodyNeedlesLocal: string[] = []) { const capturedMetrics: CapturedMetric[] = []; const capturedLogRecords: CapturedLogRecord[] = []; const capturedBodyText: Partial> = {}; - const server = createServer(async (req: IncomingMessage, res: ServerResponse) => { - if (req.method !== "POST" || !req.url) { - res.writeHead(404, { "content-type": "text/plain" }); - res.end("not found"); - return; - } - const requestPath = req.url; - const signal = OTLP_SIGNAL_PATHS.get(requestPath); - if (!signal) { - res.writeHead(404, { "content-type": "text/plain" }); - res.end("not found"); - return; - } + const server = createServer((req: IncomingMessage, res: ServerResponse) => { + void (async () => { + if (req.method !== "POST" || !req.url) { + res.writeHead(404, { "content-type": "text/plain" }); + res.end("not found"); + return; + } + const requestPath = req.url; + const signal = OTLP_SIGNAL_PATHS.get(requestPath); + if (!signal) { + res.writeHead(404, { "content-type": "text/plain" }); + res.end("not found"); + return; + } - const contentEncoding = headerValue(req.headers["content-encoding"]); - let body: Buffer; - try { - const compressedBody = await readRequestBody(req); - body = decodeRequestBody(compressedBody, contentEncoding); - } catch (error) { - const statusCode = - typeof (error as { statusCode?: unknown }).statusCode === "number" - ? (error as { statusCode: number }).statusCode - : 400; + const contentEncoding = headerValue(req.headers["content-encoding"]); + let body: Buffer; + try { + const compressedBody = await readRequestBody(req); + body = decodeRequestBody(compressedBody, contentEncoding); + } catch (error) { + const statusCode = + typeof (error as { statusCode?: unknown }).statusCode === "number" + ? (error as { statusCode: number }).statusCode + : 400; + capturedRequests.push({ + path: requestPath, + signal, + bytes: 0, + contentEncoding, + status: statusCode, + spanCount: 0, + metricCount: 0, + logCount: 0, + }); + res.writeHead(statusCode, { "content-type": "text/plain" }); + res.end(error instanceof Error ? error.message : String(error)); + return; + } + const spans = signal === "traces" ? decodeTraceRequest(body) : []; + const metrics = signal === "metrics" ? decodeMetricRequest(body) : []; + const logRecords = signal === "logs" ? decodeLogRequest(body) : []; + if (spans.length > 0) { + capturedSpans.push(...spans); + } + if (metrics.length > 0) { + capturedMetrics.push(...metrics); + } + if (logRecords.length > 0) { + capturedLogRecords.push(...logRecords); + } + appendCapturedBodyText(capturedBodyText, signal, body, undefined, disallowedBodyNeedlesLocal); capturedRequests.push({ path: requestPath, signal, - bytes: 0, + bytes: body.length, contentEncoding, - status: statusCode, - spanCount: 0, - metricCount: 0, - logCount: 0, + status: 200, + spanCount: spans.length, + metricCount: metrics.length, + logCount: logRecords.length, }); - res.writeHead(statusCode, { "content-type": "text/plain" }); - res.end(error instanceof Error ? error.message : String(error)); - return; - } - const spans = signal === "traces" ? decodeTraceRequest(body) : []; - const metrics = signal === "metrics" ? decodeMetricRequest(body) : []; - const logRecords = signal === "logs" ? decodeLogRequest(body) : []; - if (spans.length > 0) { - capturedSpans.push(...spans); - } - if (metrics.length > 0) { - capturedMetrics.push(...metrics); - } - if (logRecords.length > 0) { - capturedLogRecords.push(...logRecords); - } - appendCapturedBodyText(capturedBodyText, signal, body, undefined, disallowedBodyNeedlesLocal); - capturedRequests.push({ - path: requestPath, - signal, - bytes: body.length, - contentEncoding, - status: 200, - spanCount: spans.length, - metricCount: metrics.length, - logCount: logRecords.length, - }); - res.writeHead(200, { "content-type": "application/x-protobuf" }); - res.end(); + res.writeHead(200, { "content-type": "application/x-protobuf" }); + res.end(); + })(); }); return { diff --git a/src/acp/client.ts b/src/acp/client.ts index 50734b090681..aa8150620c74 100644 --- a/src/acp/client.ts +++ b/src/acp/client.ts @@ -202,29 +202,31 @@ export async function runAcpClientInteractive(opts: AcpClientOptions = {}): Prom console.log('Type a prompt, or "exit" to quit.\n'); const prompt = () => { - rl.question("> ", async (input) => { - const text = input.trim(); - if (!text) { + rl.question("> ", (input) => { + void (async () => { + const text = input.trim(); + if (!text) { + prompt(); + return; + } + if (text === "exit" || text === "quit") { + agent.kill(); + rl.close(); + process.exit(0); + } + + try { + const response = await client.prompt({ + sessionId, + prompt: [{ type: "text", text }], + }); + console.log(`\n[${response.stopReason}]\n`); + } catch (err) { + console.error(`\n[error] ${String(err)}\n`); + } + prompt(); - return; - } - if (text === "exit" || text === "quit") { - agent.kill(); - rl.close(); - process.exit(0); - } - - try { - const response = await client.prompt({ - sessionId, - prompt: [{ type: "text", text }], - }); - console.log(`\n[${response.stopReason}]\n`); - } catch (err) { - console.error(`\n[error] ${String(err)}\n`); - } - - prompt(); + })(); }); }; diff --git a/src/acp/control-plane/manager.test-helpers.ts b/src/acp/control-plane/manager.test-helpers.ts index 98b9b8dd288f..78bc4a94de89 100644 --- a/src/acp/control-plane/manager.test-helpers.ts +++ b/src/acp/control-plane/manager.test-helpers.ts @@ -136,17 +136,17 @@ export function expectNoMockCallFields( export function createRuntime(): { runtime: AcpRuntime; - ensureSession: ReturnType; - runTurn: ReturnType; - prepareFreshSession: ReturnType; - cancel: ReturnType; - close: ReturnType; - getCapabilities: ReturnType; - getStatus: ReturnType; - setMode: ReturnType; - setConfigOption: ReturnType; + ensureSession: ReturnType>; + runTurn: ReturnType>; + prepareFreshSession: ReturnType>>; + cancel: ReturnType>; + close: ReturnType>; + getCapabilities: ReturnType>>; + getStatus: ReturnType>>; + setMode: ReturnType>>; + setConfigOption: ReturnType>>; } { - const ensureSession = vi.fn( + const ensureSession = vi.fn( async (input: { sessionKey: string; agent: string; @@ -161,23 +161,23 @@ export function createRuntime(): { runtimeSessionName: `${input.sessionKey}:${input.mode}:runtime`, }), ); - const runTurn = vi.fn(async function* () { + const runTurn = vi.fn(async function* () { yield { type: "done" as const }; }); - const prepareFreshSession = vi.fn(async () => {}); - const cancel = vi.fn(async () => {}); - const close = vi.fn(async () => {}); - const getCapabilities = vi.fn( + const prepareFreshSession = vi.fn>(async () => {}); + const cancel = vi.fn(async () => {}); + const close = vi.fn(async () => {}); + const getCapabilities = vi.fn>( async (): Promise => ({ controls: ["session/set_mode", "session/set_config_option", "session/status"], }), ); - const getStatus = vi.fn(async () => ({ + const getStatus = vi.fn>(async () => ({ summary: "status=alive", details: { status: "alive" }, })); - const setMode = vi.fn(async () => {}); - const setConfigOption = vi.fn(async () => {}); + const setMode = vi.fn>(async () => {}); + const setConfigOption = vi.fn>(async () => {}); return { runtime: { ensureSession, diff --git a/src/agents/cli-runner/bundle-mcp.gemini.live.test.ts b/src/agents/cli-runner/bundle-mcp.gemini.live.test.ts index 0af9491d684d..79b867d898e4 100644 --- a/src/agents/cli-runner/bundle-mcp.gemini.live.test.ts +++ b/src/agents/cli-runner/bundle-mcp.gemini.live.test.ts @@ -31,12 +31,14 @@ async function startLocalStreamableHttpMcpServer(): Promise<{ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); await mcpServer.connect(transport); - const httpServer = http.createServer(async (req, res) => { - if (!req.url?.startsWith("/mcp")) { - res.writeHead(404).end(); - return; - } - await transport.handleRequest(req, res); + const httpServer = http.createServer((req, res) => { + void (async () => { + if (!req.url?.startsWith("/mcp")) { + res.writeHead(404).end(); + return; + } + await transport.handleRequest(req, res); + })(); }); await new Promise((resolve) => { diff --git a/src/agents/embedded-agent-runner/compact.hooks.test.ts b/src/agents/embedded-agent-runner/compact.hooks.test.ts index 746cc6e32171..493b00fb9abe 100644 --- a/src/agents/embedded-agent-runner/compact.hooks.test.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.test.ts @@ -1110,7 +1110,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => { it("forwards internal compaction hook messages to the caller", async () => { const onHookMessages = vi.fn(); - triggerInternalHook.mockImplementation(async (event: unknown) => { + triggerInternalHook.mockImplementation((event: unknown) => { const hookEvent = event as { action?: string; messages?: string[] }; hookEvent.messages?.push(`${hookEvent.action} notice`); }); diff --git a/src/agents/embedded-agent-runner/context-engine-maintenance.ts b/src/agents/embedded-agent-runner/context-engine-maintenance.ts index 2e51b2917423..4ffd14daa5e5 100644 --- a/src/agents/embedded-agent-runner/context-engine-maintenance.ts +++ b/src/agents/embedded-agent-runner/context-engine-maintenance.ts @@ -642,6 +642,24 @@ function scheduleDeferredTurnMaintenance( }); return undefined; } + const cleanupDeferredTurnMaintenance = async () => { + schedulerAbort.dispose(); + const current = activeDeferredTurnMaintenanceRuns.get(sessionKey); + if (current !== state) { + return; + } + const shutdownTriggered = schedulerAbort.abortSignal?.aborted === true; + const rerunParams = + current.rerunRequested && !shutdownTriggered ? current.latestParams : undefined; + const discardedRerunParams = + current.rerunRequested && shutdownTriggered ? current.latestParams : undefined; + activeDeferredTurnMaintenanceRuns.delete(sessionKey); + if (rerunParams) { + await scheduleDeferredTurnMaintenance(rerunParams); + } else if (discardedRerunParams?.disposeContextEngineAfterMaintenance) { + await disposeDeferredMaintenanceContextEngine(discardedRerunParams.contextEngine); + } + }; const trackedPromise = runPromise .catch((err) => { params.onScheduleFailure?.(err); @@ -651,23 +669,9 @@ function scheduleDeferredTurnMaintenance( error: err, }); }) - .finally(async () => { - schedulerAbort.dispose(); - const current = activeDeferredTurnMaintenanceRuns.get(sessionKey); - if (current !== state) { - return; - } - const shutdownTriggered = schedulerAbort.abortSignal?.aborted === true; - const rerunParams = - current.rerunRequested && !shutdownTriggered ? current.latestParams : undefined; - const discardedRerunParams = - current.rerunRequested && shutdownTriggered ? current.latestParams : undefined; - activeDeferredTurnMaintenanceRuns.delete(sessionKey); - if (rerunParams) { - await scheduleDeferredTurnMaintenance(rerunParams); - } else if (discardedRerunParams?.disposeContextEngineAfterMaintenance) { - await disposeDeferredMaintenanceContextEngine(discardedRerunParams.contextEngine); - } + .then(cleanupDeferredTurnMaintenance, async (err) => { + await cleanupDeferredTurnMaintenance(); + throw err; }); const state: DeferredTurnMaintenanceRunState = { promise: trackedPromise, diff --git a/src/agents/embedded-agent-runner/tool-result-context-guard.test.ts b/src/agents/embedded-agent-runner/tool-result-context-guard.test.ts index 78f19732ba63..354f71ebb0c3 100644 --- a/src/agents/embedded-agent-runner/tool-result-context-guard.test.ts +++ b/src/agents/embedded-agent-runner/tool-result-context-guard.test.ts @@ -409,10 +409,10 @@ describe("installToolResultContextGuard", () => { }); type MockedEngine = ContextEngine & { - afterTurn: ReturnType; - assemble: ReturnType; - ingest: ReturnType; - ingestBatch?: ReturnType; + afterTurn: ReturnType>>; + assemble: ReturnType>; + ingest: ReturnType>; + ingestBatch?: ReturnType>>; }; function makeMockEngine( @@ -429,13 +429,15 @@ function makeMockEngine( omitIngestBatch?: boolean; } = {}, ): MockedEngine { - const defaultAfterTurn = vi.fn(async () => {}); - const defaultAssemble = vi.fn(async (params: Parameters[0]) => ({ - messages: params.messages, - estimatedTokens: 0, - })); - const defaultIngest = vi.fn(async () => ({ ingested: true })); - const defaultIngestBatch = vi.fn( + const defaultAfterTurn = vi.fn>(async () => {}); + const defaultAssemble = vi.fn( + async (params: Parameters[0]) => ({ + messages: params.messages, + estimatedTokens: 0, + }), + ); + const defaultIngest = vi.fn(async () => ({ ingested: true })); + const defaultIngestBatch = vi.fn>( async (params: Parameters>[0]) => ({ ingestedCount: params.messages.length, }), @@ -443,14 +445,18 @@ function makeMockEngine( const afterTurn = overrides.omitAfterTurn ? undefined : overrides.afterTurn - ? vi.fn(overrides.afterTurn) + ? vi.fn>(overrides.afterTurn) : defaultAfterTurn; - const assemble = overrides.assemble ? vi.fn(overrides.assemble) : defaultAssemble; - const ingest = overrides.ingest ? vi.fn(overrides.ingest) : defaultIngest; + const assemble = overrides.assemble + ? vi.fn(overrides.assemble) + : defaultAssemble; + const ingest = overrides.ingest + ? vi.fn(overrides.ingest) + : defaultIngest; const ingestBatch = overrides.omitIngestBatch ? undefined : overrides.ingestBatch - ? vi.fn(overrides.ingestBatch) + ? vi.fn>(overrides.ingestBatch) : defaultIngestBatch; const engine = { info: { diff --git a/src/agents/embedded-agent-subscribe.handlers.tools.test.ts b/src/agents/embedded-agent-subscribe.handlers.tools.test.ts index 92e6419453c6..d18025c5c56e 100644 --- a/src/agents/embedded-agent-subscribe.handlers.tools.test.ts +++ b/src/agents/embedded-agent-subscribe.handlers.tools.test.ts @@ -21,13 +21,13 @@ type ToolExecutionEndEvent = Extract function createTestContext(): { ctx: ToolHandlerContext; warn: ReturnType; - onBlockReplyFlush: ReturnType; + onBlockReplyFlush: ReturnType Promise>>; onAgentEvent: ReturnType; onExecutionPhase: ReturnType; trace: ReturnType; isEnabled: ReturnType; } { - const onBlockReplyFlush = vi.fn(); + const onBlockReplyFlush = vi.fn<() => Promise>(); const onAgentEvent = vi.fn(); const onExecutionPhase = vi.fn(); const warn = vi.fn(); diff --git a/src/agents/model-catalog.test.ts b/src/agents/model-catalog.test.ts index 23aaf94d4276..5b888e7fb29d 100644 --- a/src/agents/model-catalog.test.ts +++ b/src/agents/model-catalog.test.ts @@ -17,7 +17,7 @@ let augmentCatalogMock: ReturnType; let ensureOpenClawModelsJsonMock: ReturnType; let currentPluginMetadataSnapshotMock: ReturnType unknown>>; let loadPluginMetadataSnapshotMock: ReturnType unknown>>; -let readFileMock: ReturnType; +let readFileMock: ReturnType Promise>>; vi.mock("./model-suppression.runtime.js", () => ({ shouldSuppressBuiltInModel: (params: { provider?: string; id?: string }) => @@ -230,7 +230,7 @@ function requireMockCallParam( describe("loadModelCatalog", () => { beforeAll(async () => { vi.resetModules(); - readFileMock = vi.fn(); + readFileMock = vi.fn<(pathname: string) => Promise>(); vi.doMock("node:fs/promises", async (importOriginal) => ({ ...(await importOriginal()), readFile: readFileMock, diff --git a/src/agents/sessions/tools/grep.ts b/src/agents/sessions/tools/grep.ts index 343719b385a6..e204b4576d35 100644 --- a/src/agents/sessions/tools/grep.ts +++ b/src/agents/sessions/tools/grep.ts @@ -339,78 +339,80 @@ export function createGrepToolDefinition( cleanup(); settle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`))); }); - child.on("close", async (code) => { - cleanup(); - if (aborted) { - settle(() => reject(new Error("Operation aborted"))); - return; - } - if (!killedDueToLimit && code !== 0 && code !== 1) { - const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`; - settle(() => reject(new Error(errorMsg))); - return; - } - if (matchCount === 0) { + child.on("close", (code) => { + void (async () => { + cleanup(); + if (aborted) { + settle(() => reject(new Error("Operation aborted"))); + return; + } + if (!killedDueToLimit && code !== 0 && code !== 1) { + const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`; + settle(() => reject(new Error(errorMsg))); + return; + } + if (matchCount === 0) { + settle(() => + resolve({ + content: [{ type: "text", text: "No matches found" }], + details: undefined, + }), + ); + return; + } + + // Format matches after streaming finishes so custom readFile() backends can be async. + for (const match of matches) { + if (contextValue === 0 && match.lineText !== undefined) { + const relativePath = formatPath(match.filePath); + const sanitized = match.lineText + .replace(/\r\n/g, "\n") + .replace(/\r/g, "") + .replace(/\n$/, ""); + const { text: truncatedText, wasTruncated } = truncateLine(sanitized); + if (wasTruncated) { + linesTruncated = true; + } + outputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`); + } else { + const block = await formatBlock(match.filePath, match.lineNumber); + outputLines.push(...block); + } + } + + const rawOutput = outputLines.join("\n"); + // Apply byte truncation. There is no line limit here because the match limit already capped rows. + const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); + let output = truncation.content; + const details: GrepToolDetails = {}; + // Build actionable notices for truncation and match limits. + const notices: string[] = []; + if (matchLimitReached) { + notices.push( + `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, + ); + details.matchLimitReached = effectiveLimit; + } + if (truncation.truncated) { + notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); + details.truncation = truncation; + } + if (linesTruncated) { + notices.push( + `Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`, + ); + details.linesTruncated = true; + } + if (notices.length > 0) { + output += `\n\n[${notices.join(". ")}]`; + } settle(() => resolve({ - content: [{ type: "text", text: "No matches found" }], - details: undefined, + content: [{ type: "text", text: output }], + details: Object.keys(details).length > 0 ? details : undefined, }), ); - return; - } - - // Format matches after streaming finishes so custom readFile() backends can be async. - for (const match of matches) { - if (contextValue === 0 && match.lineText !== undefined) { - const relativePath = formatPath(match.filePath); - const sanitized = match.lineText - .replace(/\r\n/g, "\n") - .replace(/\r/g, "") - .replace(/\n$/, ""); - const { text: truncatedText, wasTruncated } = truncateLine(sanitized); - if (wasTruncated) { - linesTruncated = true; - } - outputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`); - } else { - const block = await formatBlock(match.filePath, match.lineNumber); - outputLines.push(...block); - } - } - - const rawOutput = outputLines.join("\n"); - // Apply byte truncation. There is no line limit here because the match limit already capped rows. - const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); - let output = truncation.content; - const details: GrepToolDetails = {}; - // Build actionable notices for truncation and match limits. - const notices: string[] = []; - if (matchLimitReached) { - notices.push( - `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, - ); - details.matchLimitReached = effectiveLimit; - } - if (truncation.truncated) { - notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); - details.truncation = truncation; - } - if (linesTruncated) { - notices.push( - `Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`, - ); - details.linesTruncated = true; - } - if (notices.length > 0) { - output += `\n\n[${notices.join(". ")}]`; - } - settle(() => - resolve({ - content: [{ type: "text", text: output }], - details: Object.keys(details).length > 0 ? details : undefined, - }), - ); + })(); }); } catch (err) { settle(() => reject(err as Error)); diff --git a/src/auto-reply/inbound-debounce.ts b/src/auto-reply/inbound-debounce.ts index 1fa8a52e64b1..7d039730a010 100644 --- a/src/auto-reply/inbound-debounce.ts +++ b/src/auto-reply/inbound-debounce.ts @@ -199,8 +199,8 @@ export function createInboundDebouncer(params: InboundDebounceCreateParams if (buffer.timeout) { clearTimeout(buffer.timeout); } - buffer.timeout = setTimeout(async () => { - await flushBuffer(key, buffer); + buffer.timeout = setTimeout(() => { + void flushBuffer(key, buffer); }, buffer.debounceMs); buffer.timeout.unref?.(); }; diff --git a/src/auto-reply/reply/reply-dispatcher.ts b/src/auto-reply/reply/reply-dispatcher.ts index 48918b227267..ecb01121220f 100644 --- a/src/auto-reply/reply/reply-dispatcher.ts +++ b/src/auto-reply/reply/reply-dispatcher.ts @@ -19,7 +19,10 @@ import type { TypingController } from "./typing.js"; export type { ReplyDispatchKind, ReplyDispatcher } from "./reply-dispatcher.types.js"; -type ReplyDispatchErrorHandler = (err: unknown, info: { kind: ReplyDispatchKind }) => void; +type ReplyDispatchErrorHandler = ( + err: unknown, + info: { kind: ReplyDispatchKind }, +) => Promise | void; type ReplyDispatchSkipHandler = ( payload: ReplyPayload, @@ -69,7 +72,7 @@ export type ReplyDispatcherOptions = { * Called at normalization time, after model selection is complete. */ responsePrefixContextProvider?: () => ResponsePrefixContext; onHeartbeatStrip?: () => void; - onIdle?: () => void; + onIdle?: () => Promise | void; onError?: ReplyDispatchErrorHandler; // AIDEV-NOTE: onSkip lets channels detect silent/empty drops (e.g. Telegram empty-response fallback). onSkip?: ReplyDispatchSkipHandler; @@ -81,7 +84,7 @@ export type ReplyDispatcherOptions = { export type ReplyDispatcherWithTypingOptions = Omit & { typingCallbacks?: TypingCallbacks; onReplyStart?: () => Promise | void; - onIdle?: () => void; + onIdle?: () => Promise | void; onSettled?: () => unknown; onFreshSettledDelivery?: () => unknown; /** Called when the typing controller is cleaned up (e.g., on NO_REPLY). */ @@ -206,7 +209,7 @@ export function createReplyDispatcher(options: ReplyDispatcherOptions): ReplyDis }) .catch((err) => { failedCounts[kind] += 1; - options.onError?.(err, { kind }); + void options.onError?.(err, { kind }); }) .finally(() => { pending -= 1; @@ -220,7 +223,7 @@ export function createReplyDispatcher(options: ReplyDispatcherOptions): ReplyDis if (pending === 0) { // Unregister from global tracking when idle. unregister(); - options.onIdle?.(); + void options.onIdle?.(); } }); return true; @@ -240,7 +243,7 @@ export function createReplyDispatcher(options: ReplyDispatcherOptions): ReplyDis pending -= 1; if (pending === 0) { unregister(); - options.onIdle?.(); + void options.onIdle?.(); } } }); @@ -311,7 +314,7 @@ export function createReplyDispatcherWithTyping( ...dispatcherOptions, onIdle: () => { typingController?.markDispatchIdle(); - resolvedOnIdle?.(); + return resolvedOnIdle?.(); }, }); diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index df8273f55871..953ab68ec4f5 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -1739,49 +1739,51 @@ describe("update-cli", () => { once: EventEmitter["once"]; }; const env = (options as { env?: NodeJS.ProcessEnv }).env; - queueMicrotask(async () => { - const resultPath = env?.OPENCLAW_UPDATE_POST_CORE_RESULT_PATH; - if (resultPath) { - await fs.writeFile( - resultPath, - JSON.stringify({ - status: "warning", - changed: false, - warnings: [ - { - pluginId: "demo", - reason: "Failed to update demo: registry timeout", - message: - 'Plugin "demo" could not be processed after the core update: Failed to update demo: registry timeout Run openclaw doctor --fix to attempt automatic repair. Run openclaw plugins inspect demo --runtime --json for details.', - guidance: [ - "Run openclaw doctor --fix to attempt automatic repair.", - "Run openclaw plugins inspect demo --runtime --json for details.", - ], - }, - ], - sync: { + queueMicrotask(() => { + void (async () => { + const resultPath = env?.OPENCLAW_UPDATE_POST_CORE_RESULT_PATH; + if (resultPath) { + await fs.writeFile( + resultPath, + JSON.stringify({ + status: "warning", changed: false, - switchedToBundled: [], - switchedToNpm: [], - warnings: [], - errors: [], - }, - npm: { - changed: false, - outcomes: [ + warnings: [ { pluginId: "demo", - status: "error", - message: "Failed to update demo: registry timeout", + reason: "Failed to update demo: registry timeout", + message: + 'Plugin "demo" could not be processed after the core update: Failed to update demo: registry timeout Run openclaw doctor --fix to attempt automatic repair. Run openclaw plugins inspect demo --runtime --json for details.', + guidance: [ + "Run openclaw doctor --fix to attempt automatic repair.", + "Run openclaw plugins inspect demo --runtime --json for details.", + ], }, ], - }, - integrityDrifts: [], - }), - "utf-8", - ); - } - child.emit("exit", 0, null); + sync: { + changed: false, + switchedToBundled: [], + switchedToNpm: [], + warnings: [], + errors: [], + }, + npm: { + changed: false, + outcomes: [ + { + pluginId: "demo", + status: "error", + message: "Failed to update demo: registry timeout", + }, + ], + }, + integrityDrifts: [], + }), + "utf-8", + ); + } + child.emit("exit", 0, null); + })(); }); return child; }); diff --git a/src/gateway/call.ts b/src/gateway/call.ts index f24c2e831fde..2abf5887ec7f 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -907,30 +907,32 @@ async function executeGatewayRequestWithScopes(params: { deviceIdentity, minProtocol: opts.minProtocol ?? MIN_CLIENT_PROTOCOL_VERSION, maxProtocol: opts.maxProtocol ?? PROTOCOL_VERSION, - onHelloOk: async (hello) => { - try { - ensureGatewaySupportsRequiredMethods({ - requiredMethods: opts.requiredMethods, - methods: hello.features?.methods, - attemptedMethod: opts.method, - }); - const activeClient = client; - if (!activeClient) { - throw new Error("gateway client not initialized"); + onHelloOk: (hello) => { + void (async () => { + try { + ensureGatewaySupportsRequiredMethods({ + requiredMethods: opts.requiredMethods, + methods: hello.features?.methods, + attemptedMethod: opts.method, + }); + const activeClient = client; + if (!activeClient) { + throw new Error("gateway client not initialized"); + } + primaryRequestStarted = true; + const result = await activeClient.request(opts.method, opts.params, { + expectFinal: opts.expectFinal, + timeoutMs: opts.timeoutMs, + signal: opts.signal, + onAccepted: opts.onAccepted, + }); + ignoreClose = true; + stop(undefined, result); + } catch (err) { + ignoreClose = true; + stop(err as Error); } - primaryRequestStarted = true; - const result = await activeClient.request(opts.method, opts.params, { - expectFinal: opts.expectFinal, - timeoutMs: opts.timeoutMs, - signal: opts.signal, - onAccepted: opts.onAccepted, - }); - ignoreClose = true; - stop(undefined, result); - } catch (err) { - ignoreClose = true; - stop(err as Error); - } + })(); }, onClose: (code, reason) => { if (settled || ignoreClose) { diff --git a/src/gateway/managed-image-attachments.test.ts b/src/gateway/managed-image-attachments.test.ts index 146a08b4cb75..a791dc4bd2a2 100644 --- a/src/gateway/managed-image-attachments.test.ts +++ b/src/gateway/managed-image-attachments.test.ts @@ -181,17 +181,19 @@ async function requestManagedImage(params: { ); const auth = { mode: "test" } as never; - const server = http.createServer(async (req, res) => { - const handled = await handleManagedOutgoingImageHttpRequest(req, res, { - auth, - trustedProxies: ["127.0.0.1/32"], - allowRealIpFallback: false, - stateDir: params.stateDir, - }); - if (!handled) { - res.statusCode = 404; - res.end("unhandled"); - } + const server = http.createServer((req, res) => { + void (async () => { + const handled = await handleManagedOutgoingImageHttpRequest(req, res, { + auth, + trustedProxies: ["127.0.0.1/32"], + allowRealIpFallback: false, + stateDir: params.stateDir, + }); + if (!handled) { + res.statusCode = 404; + res.end("unhandled"); + } + })(); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); @@ -207,16 +209,18 @@ async function requestManagedImage(params: { method: params.method ?? "GET", headers: params.headers, }, - async (res) => { - const chunks: Buffer[] = []; - for await (const chunk of res) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - } - resolve({ - statusCode: res.statusCode ?? 0, - headers: res.headers, - body: Buffer.concat(chunks), - }); + (res) => { + void (async () => { + const chunks: Buffer[] = []; + for await (const chunk of res) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + resolve({ + statusCode: res.statusCode ?? 0, + headers: res.headers, + body: Buffer.concat(chunks), + }); + })(); }, ); req.on("error", reject); diff --git a/src/gateway/probe.ts b/src/gateway/probe.ts index bf91b2a16744..ec8051665929 100644 --- a/src/gateway/probe.ts +++ b/src/gateway/probe.ts @@ -386,84 +386,86 @@ export async function probeGateway(opts: { }); } }, - onHelloOk: async (hello) => { - connectLatencyMs = Date.now() - startedAt; - authMetadataPresent = typeof hello?.auth === "object" && hello.auth !== null; - server = { - version: typeof hello?.server?.version === "string" ? hello.server.version : null, - connId: typeof hello?.server?.connId === "string" ? hello.server.connId : null, - }; - auth = resolveProbeAuthSummary({ - role: typeof hello?.auth?.role === "string" ? hello.auth.role : null, - scopes: Array.isArray(hello?.auth?.scopes) - ? hello.auth.scopes.filter((scope): scope is string => typeof scope === "string") - : [], - authMetadataPresent, - }); - if (detailLevel === "none") { - settleProbe({ - ok: true, - error: null, - verifiedRead: false, - health: null, - status: null, - presence: null, - configSnapshot: null, + onHelloOk: (hello) => { + void (async () => { + connectLatencyMs = Date.now() - startedAt; + authMetadataPresent = typeof hello?.auth === "object" && hello.auth !== null; + server = { + version: typeof hello?.server?.version === "string" ? hello.server.version : null, + connId: typeof hello?.server?.connId === "string" ? hello.server.connId : null, + }; + auth = resolveProbeAuthSummary({ + role: typeof hello?.auth?.role === "string" ? hello.auth.role : null, + scopes: Array.isArray(hello?.auth?.scopes) + ? hello.auth.scopes.filter((scope): scope is string => typeof scope === "string") + : [], + authMetadataPresent, }); - return; - } - // Once the gateway has accepted the session, a slow follow-up RPC should no longer - // downgrade the probe to "unreachable". Give detail fetching its own budget. - armProbeTimer(() => { - settleProbe({ - ok: false, - error: "timeout", - health: null, - status: null, - presence: null, - configSnapshot: null, - }); - }); - try { - if (detailLevel === "presence") { - const presence = await client.request("system-presence"); + if (detailLevel === "none") { settleProbe({ ok: true, error: null, - verifiedRead: true, + verifiedRead: false, health: null, status: null, - presence: Array.isArray(presence) ? (presence as SystemPresence[]) : null, + presence: null, configSnapshot: null, }); return; } - const [health, status, presence, configSnapshot] = await Promise.all([ - client.request("health"), - client.request("status"), - client.request("system-presence"), - client.request("config.get", {}), - ]); - settleProbe({ - ok: true, - error: null, - verifiedRead: true, - health, - status, - presence: Array.isArray(presence) ? (presence as SystemPresence[]) : null, - configSnapshot, + // Once the gateway has accepted the session, a slow follow-up RPC should no longer + // downgrade the probe to "unreachable". Give detail fetching its own budget. + armProbeTimer(() => { + settleProbe({ + ok: false, + error: "timeout", + health: null, + status: null, + presence: null, + configSnapshot: null, + }); }); - } catch (err) { - const error = formatErrorMessage(err); - settleProbe({ - ok: false, - error, - health: null, - status: null, - presence: null, - configSnapshot: null, - }); - } + try { + if (detailLevel === "presence") { + const presence = await client.request("system-presence"); + settleProbe({ + ok: true, + error: null, + verifiedRead: true, + health: null, + status: null, + presence: Array.isArray(presence) ? (presence as SystemPresence[]) : null, + configSnapshot: null, + }); + return; + } + const [health, status, presence, configSnapshot] = await Promise.all([ + client.request("health"), + client.request("status"), + client.request("system-presence"), + client.request("config.get", {}), + ]); + settleProbe({ + ok: true, + error: null, + verifiedRead: true, + health, + status, + presence: Array.isArray(presence) ? (presence as SystemPresence[]) : null, + configSnapshot, + }); + } catch (err) { + const error = formatErrorMessage(err); + settleProbe({ + ok: false, + error, + health: null, + status: null, + presence: null, + configSnapshot: null, + }); + } + })(); }, }); diff --git a/src/gateway/server-channels.ts b/src/gateway/server-channels.ts index 860d256bf86f..57087c78b6bd 100644 --- a/src/gateway/server-channels.ts +++ b/src/gateway/server-channels.ts @@ -582,7 +582,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage setRuntime(channelId, id, { accountId: id, lastError: message }); log.error?.(`[${id}] channel exited: ${message}`); }) - .finally(async () => { + .then(async () => { await cleanupTaskScopedApprovalRuntime("channel cleanup failed"); setRuntime(channelId, id, { accountId: id, diff --git a/src/infra/heartbeat-wake.ts b/src/infra/heartbeat-wake.ts index 8c8ea367f9d4..120232a17223 100644 --- a/src/infra/heartbeat-wake.ts +++ b/src/infra/heartbeat-wake.ts @@ -205,37 +205,52 @@ function schedule(coalesceMs: number, kind: WakeTimerKind = "normal") { } timerDueAt = dueAt; timerKind = kind; - timer = setTimeout(async () => { - timer = null; - timerDueAt = null; - timerKind = null; - scheduled = false; - const active = handler; - if (!active) { - return; - } - if (running) { - scheduled = true; - schedule(delay, kind); - return; - } + timer = setTimeout(() => { + void (async () => { + timer = null; + timerDueAt = null; + timerKind = null; + scheduled = false; + const active = handler; + if (!active) { + return; + } + if (running) { + scheduled = true; + schedule(delay, kind); + return; + } - const pendingBatch = Array.from(pendingWakes.values()); - pendingWakes.clear(); - running = true; - try { - for (const pendingWake of pendingBatch) { - const wakeOpts = { - source: pendingWake.source, - intent: pendingWake.intent, - reason: pendingWake.reason ?? undefined, - ...(pendingWake.agentId ? { agentId: pendingWake.agentId } : {}), - ...(pendingWake.sessionKey ? { sessionKey: pendingWake.sessionKey } : {}), - ...(pendingWake.heartbeat ? { heartbeat: pendingWake.heartbeat } : {}), - }; - const res = await active(wakeOpts); - if (res.status === "skipped" && isRetryableHeartbeatBusySkipReason(res.reason)) { - // The target runtime is busy; retry this wake target soon. + const pendingBatch = Array.from(pendingWakes.values()); + pendingWakes.clear(); + running = true; + try { + for (const pendingWake of pendingBatch) { + const wakeOpts = { + source: pendingWake.source, + intent: pendingWake.intent, + reason: pendingWake.reason ?? undefined, + ...(pendingWake.agentId ? { agentId: pendingWake.agentId } : {}), + ...(pendingWake.sessionKey ? { sessionKey: pendingWake.sessionKey } : {}), + ...(pendingWake.heartbeat ? { heartbeat: pendingWake.heartbeat } : {}), + }; + const res = await active(wakeOpts); + if (res.status === "skipped" && isRetryableHeartbeatBusySkipReason(res.reason)) { + // The target runtime is busy; retry this wake target soon. + queuePendingWakeReason({ + source: pendingWake.source, + intent: pendingWake.intent, + reason: pendingWake.reason ?? "retry", + agentId: pendingWake.agentId, + sessionKey: pendingWake.sessionKey, + heartbeat: pendingWake.heartbeat, + }); + schedule(DEFAULT_RETRY_MS, "retry"); + } + } + } catch { + // Error is already logged by the heartbeat runner; schedule a retry. + for (const pendingWake of pendingBatch) { queuePendingWakeReason({ source: pendingWake.source, intent: pendingWake.intent, @@ -244,28 +259,15 @@ function schedule(coalesceMs: number, kind: WakeTimerKind = "normal") { sessionKey: pendingWake.sessionKey, heartbeat: pendingWake.heartbeat, }); - schedule(DEFAULT_RETRY_MS, "retry"); + } + schedule(DEFAULT_RETRY_MS, "retry"); + } finally { + running = false; + if (pendingWakes.size > 0 || scheduled) { + schedule(delay, "normal"); } } - } catch { - // Error is already logged by the heartbeat runner; schedule a retry. - for (const pendingWake of pendingBatch) { - queuePendingWakeReason({ - source: pendingWake.source, - intent: pendingWake.intent, - reason: pendingWake.reason ?? "retry", - agentId: pendingWake.agentId, - sessionKey: pendingWake.sessionKey, - heartbeat: pendingWake.heartbeat, - }); - } - schedule(DEFAULT_RETRY_MS, "retry"); - } finally { - running = false; - if (pendingWakes.size > 0 || scheduled) { - schedule(delay, "normal"); - } - } + })(); }, delay); timer.unref?.(); } diff --git a/src/plugins/openai-compatible-embedding-provider.test.ts b/src/plugins/openai-compatible-embedding-provider.test.ts index e37f3174d7f0..235a5a33ee17 100644 --- a/src/plugins/openai-compatible-embedding-provider.test.ts +++ b/src/plugins/openai-compatible-embedding-provider.test.ts @@ -57,37 +57,39 @@ async function startEmbeddingServer(params?: { status?: number; }): Promise<{ baseUrl: string; requests: CapturedRequest[] }> { const requests: CapturedRequest[] = []; - const server = createServer(async (req: IncomingMessage, res: ServerResponse) => { - try { - const body = await readJsonBody(req); - const captured: CapturedRequest = { - method: req.method, - url: req.url, - headers: req.headers, - body, - }; - requests.push(captured); + const server = createServer((req: IncomingMessage, res: ServerResponse) => { + void (async () => { + try { + const body = await readJsonBody(req); + const captured: CapturedRequest = { + method: req.method, + url: req.url, + headers: req.headers, + body, + }; + requests.push(captured); - if (params?.token) { - expect(req.headers.authorization).toBe(`Bearer ${params.token}`); - } else { - expect(req.headers.authorization).toBeUndefined(); + if (params?.token) { + expect(req.headers.authorization).toBe(`Bearer ${params.token}`); + } else { + expect(req.headers.authorization).toBeUndefined(); + } + + res.writeHead(params?.status ?? 200, { "content-type": "application/json" }); + res.end( + JSON.stringify( + params?.respond?.(captured) ?? { + object: "list", + data: [{ object: "embedding", embedding: [0.1, 0.2, 0.3], index: 0 }], + model: body.model, + }, + ), + ); + } catch (error) { + res.writeHead(500, { "content-type": "application/json" }); + res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); } - - res.writeHead(params?.status ?? 200, { "content-type": "application/json" }); - res.end( - JSON.stringify( - params?.respond?.(captured) ?? { - object: "list", - data: [{ object: "embedding", embedding: [0.1, 0.2, 0.3], index: 0 }], - model: body.model, - }, - ), - ); - } catch (error) { - res.writeHead(500, { "content-type": "application/json" }); - res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); - } + })(); }); await new Promise((resolve, reject) => { diff --git a/src/proxy-capture/proxy-server.ts b/src/proxy-capture/proxy-server.ts index 4d5457494e74..6e22c2df68a5 100644 --- a/src/proxy-capture/proxy-server.ts +++ b/src/proxy-capture/proxy-server.ts @@ -121,110 +121,112 @@ export async function startDebugProxyServer(params: { const recordProxyEvent = createProxyCaptureRecorder({ store, settings: params.settings }); const host = params.host?.trim() || "127.0.0.1"; - const server = createServer(async (req: IncomingMessage, res: ServerResponse) => { - const flowId = randomUUID(); - let target: URL; - try { - target = normalizeTargetUrl(req); - } catch (error) { - const message = "Invalid proxy target URL"; - recordProxyEvent({ - protocol: "http", - direction: "local", - kind: "error", - flowId, - method: req.method, - host: req.headers.host, - path: req.url ?? "", - errorText: error instanceof Error ? error.message : String(error), - }); - const responseBody = `${message}\n`; - res.writeHead(400, { - Connection: "close", - "Content-Type": "text/plain; charset=utf-8", - "Content-Length": Buffer.byteLength(responseBody), - }); - res.end(responseBody); - return; - } - const targetProtocol = target.protocol === "https:" ? "https" : "http"; - const targetPath = `${target.pathname}${target.search}`; - const recordTargetEvent = ( - event: Omit, - ) => - recordProxyEvent({ - protocol: targetProtocol, - flowId, - method: req.method, - host: target.host, - path: targetPath, - ...event, - }); - try { - assertDebugProxyDirectUpstreamAllowed(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - recordTargetEvent({ - direction: "local", - kind: "error", - errorText: message, - }); - const responseBody = `${message}\n`; - res.writeHead(403, { - Connection: "close", - "Content-Type": "text/plain; charset=utf-8", - "Content-Length": Buffer.byteLength(responseBody), - }); - res.end(responseBody); - return; - } - const body = await readBody(req); - recordTargetEvent({ - direction: "outbound", - kind: "request", - headersJson: JSON.stringify(req.headers), - dataText: body.subarray(0, 8192).toString("utf8"), - }); - const upstream = (target.protocol === "https:" ? httpsRequest : httpRequest)( - target, - { - method: req.method, - headers: req.headers, - }, - (upstreamRes) => { - const chunks: Buffer[] = []; - upstreamRes.on("data", (chunk) => { - const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); - chunks.push(buffer); - res.write(buffer); + const server = createServer((req: IncomingMessage, res: ServerResponse) => { + void (async () => { + const flowId = randomUUID(); + let target: URL; + try { + target = normalizeTargetUrl(req); + } catch (error) { + const message = "Invalid proxy target URL"; + recordProxyEvent({ + protocol: "http", + direction: "local", + kind: "error", + flowId, + method: req.method, + host: req.headers.host, + path: req.url ?? "", + errorText: error instanceof Error ? error.message : String(error), }); - upstreamRes.on("end", () => { - const responseBody = Buffer.concat(chunks); - recordTargetEvent({ - direction: "inbound", - kind: "response", - status: upstreamRes.statusCode ?? undefined, - headersJson: JSON.stringify(upstreamRes.headers), - dataText: responseBody.subarray(0, 8192).toString("utf8"), + const responseBody = `${message}\n`; + res.writeHead(400, { + Connection: "close", + "Content-Type": "text/plain; charset=utf-8", + "Content-Length": Buffer.byteLength(responseBody), + }); + res.end(responseBody); + return; + } + const targetProtocol = target.protocol === "https:" ? "https" : "http"; + const targetPath = `${target.pathname}${target.search}`; + const recordTargetEvent = ( + event: Omit, + ) => + recordProxyEvent({ + protocol: targetProtocol, + flowId, + method: req.method, + host: target.host, + path: targetPath, + ...event, + }); + try { + assertDebugProxyDirectUpstreamAllowed(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + recordTargetEvent({ + direction: "local", + kind: "error", + errorText: message, + }); + const responseBody = `${message}\n`; + res.writeHead(403, { + Connection: "close", + "Content-Type": "text/plain; charset=utf-8", + "Content-Length": Buffer.byteLength(responseBody), + }); + res.end(responseBody); + return; + } + const body = await readBody(req); + recordTargetEvent({ + direction: "outbound", + kind: "request", + headersJson: JSON.stringify(req.headers), + dataText: body.subarray(0, 8192).toString("utf8"), + }); + const upstream = (target.protocol === "https:" ? httpsRequest : httpRequest)( + target, + { + method: req.method, + headers: req.headers, + }, + (upstreamRes) => { + const chunks: Buffer[] = []; + upstreamRes.on("data", (chunk) => { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + chunks.push(buffer); + res.write(buffer); }); - res.end(); + upstreamRes.on("end", () => { + const responseBody = Buffer.concat(chunks); + recordTargetEvent({ + direction: "inbound", + kind: "response", + status: upstreamRes.statusCode ?? undefined, + headersJson: JSON.stringify(upstreamRes.headers), + dataText: responseBody.subarray(0, 8192).toString("utf8"), + }); + res.end(); + }); + res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers); + }, + ); + upstream.on("error", (error) => { + recordTargetEvent({ + direction: "local", + kind: "error", + errorText: error.message, }); - res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers); - }, - ); - upstream.on("error", (error) => { - recordTargetEvent({ - direction: "local", - kind: "error", - errorText: error.message, + res.statusCode = 502; + res.end(error.message); }); - res.statusCode = 502; - res.end(error.message); - }); - if (body.byteLength > 0) { - upstream.write(body); - } - upstream.end(); + if (body.byteLength > 0) { + upstream.write(body); + } + upstream.end(); + })(); }); server.on("connect", (req, clientSocket, head) => { diff --git a/src/tui/tui-pty-local.e2e.test.ts b/src/tui/tui-pty-local.e2e.test.ts index 9c5a7f988d85..14beff83ea1a 100644 --- a/src/tui/tui-pty-local.e2e.test.ts +++ b/src/tui/tui-pty-local.e2e.test.ts @@ -97,23 +97,25 @@ function writeResponsesSse(res: ServerResponse, text: string) { async function startMockModelServer(replyText: string): Promise { const requests: Array> = []; - const server = createServer(async (req, res) => { - const url = new URL(req.url ?? "/", "http://127.0.0.1"); - if (req.method === "GET" && (url.pathname === "/healthz" || url.pathname === "/readyz")) { - writeJson(res, 200, { ok: true }); - return; - } - if (req.method === "GET" && url.pathname === "/v1/models") { - writeJson(res, 200, { data: [{ id: "gpt-5.5", object: "model" }] }); - return; - } - if (req.method === "POST" && url.pathname === "/v1/responses") { - const raw = await readRequestBody(req); - requests.push(raw ? (JSON.parse(raw) as Record) : {}); - writeResponsesSse(res, replyText); - return; - } - writeJson(res, 404, { error: "not found" }); + const server = createServer((req, res) => { + void (async () => { + const url = new URL(req.url ?? "/", "http://127.0.0.1"); + if (req.method === "GET" && (url.pathname === "/healthz" || url.pathname === "/readyz")) { + writeJson(res, 200, { ok: true }); + return; + } + if (req.method === "GET" && url.pathname === "/v1/models") { + writeJson(res, 200, { data: [{ id: "gpt-5.5", object: "model" }] }); + return; + } + if (req.method === "POST" && url.pathname === "/v1/responses") { + const raw = await readRequestBody(req); + requests.push(raw ? (JSON.parse(raw) as Record) : {}); + writeResponsesSse(res, replyText); + return; + } + writeJson(res, 404, { error: "not found" }); + })(); }); await new Promise((resolve, reject) => { diff --git a/src/wizard/setup.finalize.ts b/src/wizard/setup.finalize.ts index 2fa59121f69c..2718d7b869b8 100644 --- a/src/wizard/setup.finalize.ts +++ b/src/wizard/setup.finalize.ts @@ -266,7 +266,9 @@ export async function finalizeSetupWizard( env: process.env, port: settings.port, runtime: daemonRuntime, - warn: (message, title) => prompter.note(message, title), + warn: (message, title) => { + void prompter.note(message, title); + }, config: nextConfig, }, ); diff --git a/ui/src/ui/app-gateway.node.test.ts b/ui/src/ui/app-gateway.node.test.ts index aa502db27fec..45d8f5a47451 100644 --- a/ui/src/ui/app-gateway.node.test.ts +++ b/ui/src/ui/app-gateway.node.test.ts @@ -9,10 +9,12 @@ import type { GatewayHelloOk } from "./gateway.ts"; const loadChatHistoryMock = vi.hoisted(() => vi.fn(async () => undefined)); const loadControlUiBootstrapConfigMock = vi.hoisted(() => vi.fn(async () => undefined)); +type GatewayRequest = (method: string, payload?: unknown) => Promise; + type GatewayClientMock = { start: ReturnType; stop: ReturnType; - request: ReturnType; + request: ReturnType>; options: { clientVersion?: string }; emitHello: (hello?: GatewayHelloOk) => void; emitClose: (info: { @@ -43,7 +45,7 @@ vi.mock("./gateway.ts", async (importOriginal) => { class GatewayBrowserClient { readonly start = vi.fn(); readonly stop = vi.fn(); - readonly request = vi.fn(async (method: string) => { + readonly request = vi.fn(async (method: string) => { if (method === "update.status") { return { sentinel: null }; } diff --git a/ui/src/ui/app-render-usage-tab.ts b/ui/src/ui/app-render-usage-tab.ts index 57f0813d981e..c8e2adb3f102 100644 --- a/ui/src/ui/app-render-usage-tab.ts +++ b/ui/src/ui/app-render-usage-tab.ts @@ -134,7 +134,7 @@ export function renderUsageTab(state: AppViewState) { state.usageSessionLogs = null; void loadUsage(state); }, - onRefresh: () => loadUsage(state), + onRefresh: () => void loadUsage(state), onTimeZoneChange: (zone) => { state.usageTimeZone = zone; state.usageSelectedDays = []; diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 911e4e5dbdbd..6efc95adfb83 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -203,6 +203,14 @@ let pendingUpdate: (() => void) | undefined; const notifyLazyViewChanged = () => pendingUpdate?.(); +function runUiTask( + task: (...args: Args) => Promise, +): (...args: Args) => void { + return (...args) => { + void task(...args); + }; +} + function renderSettingsSectionNav(state: AppViewState) { if (!isSettingsTab(state.tab)) { return nothing; @@ -1250,12 +1258,12 @@ export function renderApp(state: AppViewState) { onRequestUpdate: requestHostUpdate, onFormPatch: (path: Array, value: unknown) => updateConfigFormValue(state, path, value), - onReload: () => loadConfig(state, { discardPendingChanges: true }), + onReload: () => void loadConfig(state, { discardPendingChanges: true }), onReset: () => resetConfigPendingChanges(state), - onSave: () => saveConfig(state), - onApply: () => applyConfig(state), - onUpdate: () => runUpdate(state), - onOpenFile: () => openConfigFile(state), + onSave: () => void saveConfig(state), + onApply: () => void applyConfig(state), + onUpdate: () => void runUpdate(state), + onOpenFile: () => void openConfigFile(state), version: state.hello?.server?.version ?? "", theme: state.theme, themeMode: state.themeMode, @@ -1501,8 +1509,8 @@ export function renderApp(state: AppViewState) { requestHostUpdate?.(); }, onResetConfig: () => resetConfigPendingChanges(state), - onSaveConfig: () => saveConfig(state), - onApplyConfig: () => applyConfig(state), + onSaveConfig: () => void saveConfig(state), + onApplyConfig: () => void applyConfig(state), onAdvancedSettings: () => { state.configSettingsMode = "advanced"; requestHostUpdate?.(); @@ -1562,20 +1570,20 @@ export function renderApp(state: AppViewState) { configFormDirty: state.configFormDirty, nostrProfileFormState: state.nostrProfileFormState, nostrProfileAccountId: state.nostrProfileAccountId, - onRefresh: (probe) => loadChannels(state, probe), - onWhatsAppStart: (force) => state.handleWhatsAppStart(force), - onWhatsAppWait: () => state.handleWhatsAppWait(), - onWhatsAppLogout: () => state.handleWhatsAppLogout(), + onRefresh: (probe) => void loadChannels(state, probe), + onWhatsAppStart: (force) => void state.handleWhatsAppStart(force), + onWhatsAppWait: () => void state.handleWhatsAppWait(), + onWhatsAppLogout: () => void state.handleWhatsAppLogout(), onConfigPatch: (path, value) => updateConfigFormValue(state, path, value), - onConfigSave: () => state.handleChannelConfigSave(), - onConfigReload: () => state.handleChannelConfigReload(), + onConfigSave: () => void state.handleChannelConfigSave(), + onConfigReload: () => void state.handleChannelConfigReload(), onNostrProfileEdit: (accountId, profile) => state.handleNostrProfileEdit(accountId, profile), onNostrProfileCancel: () => state.handleNostrProfileCancel(), onNostrProfileFieldChange: (field, value) => state.handleNostrProfileFieldChange(field, value), - onNostrProfileSave: () => state.handleNostrProfileSave(), - onNostrProfileImport: () => state.handleNostrProfileImport(), + onNostrProfileSave: () => void state.handleNostrProfileSave(), + onNostrProfileImport: () => void state.handleNostrProfileImport(), onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(), }), ); @@ -1601,9 +1609,9 @@ export function renderApp(state: AppViewState) { subscribed: state.webPushSubscribed, loading: state.webPushLoading, }, - onWebPushSubscribe: () => state.handleWebPushSubscribe(), - onWebPushUnsubscribe: () => state.handleWebPushUnsubscribe(), - onWebPushTest: () => state.handleWebPushTest(), + onWebPushSubscribe: () => void state.handleWebPushSubscribe(), + onWebPushUnsubscribe: () => void state.handleWebPushUnsubscribe(), + onWebPushTest: () => void state.handleWebPushTest(), }); case "appearance": return renderConfigTab({ @@ -1647,8 +1655,8 @@ export function renderApp(state: AppViewState) { configSaving: state.configSaving, configApplying: state.configApplying, connected: state.connected, - onSaveConfig: () => saveConfig(state), - onApplyConfig: () => applyConfig(state), + onSaveConfig: () => void saveConfig(state), + onApplyConfig: () => void applyConfig(state), onServerEnabledChange: (name, enabled) => { updateMcpServerEnabled(state, name, enabled); requestHostUpdate?.(); @@ -2072,9 +2080,9 @@ export function renderApp(state: AppViewState) { state.overviewShowGatewayPassword = !state.overviewShowGatewayPassword; }, onConnect: () => state.connect(), - onRefresh: () => state.loadOverview({ refresh: true }), + onRefresh: () => void state.loadOverview({ refresh: true }), onNavigate: (tab) => state.setTab(tab as import("./navigation.ts").Tab), - onRefreshLogs: () => state.loadOverview({ refresh: true }), + onRefreshLogs: () => void state.loadOverview({ refresh: true }), }) : nothing} ${state.tab === "activity" @@ -2133,7 +2141,7 @@ export function renderApp(state: AppViewState) { entries: state.presenceEntries, lastError: state.presenceError, statusMessage: state.presenceStatus, - onRefresh: () => loadPresence(state), + onRefresh: () => void loadPresence(state), }), ) : nothing} @@ -2232,8 +2240,8 @@ export function renderApp(state: AppViewState) { state.sessionsPageSize = s; state.sessionsPage = 0; }, - onRefresh: () => loadSessions(state), - onPatch: (key, patch) => patchSession(state, key, patch), + onRefresh: () => void loadSessions(state), + onPatch: (key, patch) => void patchSession(state, key, patch), onToggleSelect: (key) => { const next = new Set(state.sessionsSelectedKeys); if (next.has(key)) { @@ -2260,7 +2268,7 @@ export function renderApp(state: AppViewState) { onDeselectAll: () => { state.sessionsSelectedKeys = new Set(); }, - onDeleteSelected: async () => { + onDeleteSelected: runUiTask(async () => { const keys = [...state.sessionsSelectedKeys]; const deleted = await deleteSessionsAndRefresh(state, keys); if (deleted.length > 0) { @@ -2270,14 +2278,14 @@ export function renderApp(state: AppViewState) { } state.sessionsSelectedKeys = next; } - }, + }), onNavigateToChat: (sessionKey) => { switchChatSession(state, sessionKey); state.setTab("chat" as import("./navigation.ts").Tab); }, onAddToWorkboard: workboardEnabled && operatorCanWrite - ? async (session) => { + ? runUiTask(async (session) => { await captureSessionToWorkboard({ host: state, client: state.client, @@ -2285,11 +2293,11 @@ export function renderApp(state: AppViewState) { requestUpdate: requestHostUpdate, }); state.setTab("workboard" as import("./navigation.ts").Tab); - } + }) : undefined, onToggleCheckpointDetails: (sessionKey) => - toggleSessionCompactionCheckpoints(state, sessionKey), - onBranchFromCheckpoint: async (sessionKey, checkpointId) => { + void toggleSessionCompactionCheckpoints(state, sessionKey), + onBranchFromCheckpoint: runUiTask(async (sessionKey, checkpointId) => { const nextKey = await branchSessionFromCheckpoint( state, sessionKey, @@ -2299,9 +2307,9 @@ export function renderApp(state: AppViewState) { switchChatSession(state, nextKey); state.setTab("chat" as import("./navigation.ts").Tab); } - }, + }), onRestoreCheckpoint: (sessionKey, checkpointId) => - restoreSessionFromCheckpoint(state, sessionKey, checkpointId), + void restoreSessionFromCheckpoint(state, sessionKey, checkpointId), }); }) : nothing} @@ -2379,7 +2387,7 @@ export function renderApp(state: AppViewState) { state.cronForm = normalizeCronFormState({ ...state.cronForm, ...patch }); state.cronFieldErrors = validateCronForm(state.cronForm); }, - onRefresh: () => state.loadCron(), + onRefresh: () => void state.loadCron(), onAdd: () => { void (async () => { const saved = await addCronJob(state); @@ -2406,21 +2414,22 @@ export function renderApp(state: AppViewState) { state.cronFormCollapsed = collapsed; requestHostUpdate?.(); }, - onToggle: (job, enabled) => toggleCronJob(state, job, enabled), - onRun: (job, mode) => runCronJob(state, job, mode ?? "force"), - onRemove: (job) => removeCronJob(state, job), + onToggle: (job, enabled) => void toggleCronJob(state, job, enabled), + onRun: (job, mode) => void runCronJob(state, job, mode ?? "force"), + onRemove: (job) => void removeCronJob(state, job), onQuickCreate: () => { state.cronQuickCreateOpen = true; state.cronQuickCreateStep = "what"; state.cronQuickCreateDraft = createDefaultDraft(); requestHostUpdate?.(); }, - onLoadRuns: async (jobId) => { + onLoadRuns: runUiTask(async (jobId) => { updateCronRunsFilter(state, { cronRunsScope: "job" }); await loadCronRuns(state, jobId); - }, - onLoadMoreJobs: () => loadCronJobsPage(state, { append: true, tableFilters: true }), - onJobsFiltersChange: async (patch) => { + }), + onLoadMoreJobs: () => + void loadCronJobsPage(state, { append: true, tableFilters: true }), + onJobsFiltersChange: runUiTask(async (patch) => { updateCronJobsFilter(state, patch); const shouldReload = typeof patch.cronJobsQuery === "string" || @@ -2432,8 +2441,8 @@ export function renderApp(state: AppViewState) { if (shouldReload) { await loadCronJobsPage(state, { append: false, tableFilters: true }); } - }, - onJobsFiltersReset: async () => { + }), + onJobsFiltersReset: runUiTask(async () => { updateCronJobsFilter(state, { cronJobsQuery: "", cronJobsEnabledFilter: "all", @@ -2443,16 +2452,16 @@ export function renderApp(state: AppViewState) { cronJobsSortDir: "asc", }); await loadCronJobsPage(state, { append: false, tableFilters: true }); - }, - onLoadMoreRuns: () => loadMoreCronRuns(state), - onRunsFiltersChange: async (patch) => { + }), + onLoadMoreRuns: () => void loadMoreCronRuns(state), + onRunsFiltersChange: runUiTask(async (patch) => { updateCronRunsFilter(state, patch); if (state.cronRunsScope === "all") { await loadCronRuns(state, null); return; } await loadCronRuns(state, state.cronRunsJobId); - }, + }), onNavigateToChat: (sessionKey) => { switchChatSession(state, sessionKey); state.setTab("chat" as import("./navigation.ts").Tab); @@ -2519,7 +2528,7 @@ export function renderApp(state: AppViewState) { runtimeSessionKey: state.sessionKey, runtimeSessionMatchesSelectedAgent: toolsPanelUsesActiveSession, modelCatalog: state.chatModelCatalog ?? [], - onRefresh: async () => { + onRefresh: runUiTask(async () => { await loadAgents(state); const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? []; if (agentIds.length > 0) { @@ -2527,7 +2536,7 @@ export function renderApp(state: AppViewState) { } loadAgentPanelDataForSelectedAgent(resolveSelectedAgentId()); refreshAgentsPanelSupplementalData(state.agentsPanel); - }, + }), onSelectAgent: (agentId) => { if (state.agentsSelectedId === agentId) { return; @@ -2577,7 +2586,7 @@ export function renderApp(state: AppViewState) { } refreshAgentsPanelSupplementalData(panel); }, - onLoadFiles: (agentId) => loadAgentFiles(state, agentId), + onLoadFiles: (agentId) => void loadAgentFiles(state, agentId), onSelectFile: (name) => { state.agentFileActive = name; if (!resolvedAgentId) { @@ -2636,10 +2645,10 @@ export function renderApp(state: AppViewState) { removeConfigFormValue(state, [...basePathCandidate, "deny"]); } }, - onConfigReload: () => loadConfig(state, { discardPendingChanges: true }), - onConfigSave: () => saveAgentsConfig(state), - onChannelsRefresh: () => loadChannels(state, false), - onCronRefresh: () => state.loadCron(), + onConfigReload: () => void loadConfig(state, { discardPendingChanges: true }), + onConfigSave: () => void saveAgentsConfig(state), + onChannelsRefresh: () => void loadChannels(state, false), + onCronRefresh: () => void state.loadCron(), onCronRunNow: (jobId) => { const job = state.cronJobs.find((entry) => entry.id === jobId); if (!job) { @@ -2805,12 +2814,12 @@ export function renderApp(state: AppViewState) { clawhubInstallMessage: state.clawhubInstallMessage, onFilterChange: (next) => (state.skillsFilter = next), onStatusFilterChange: (next) => (state.skillsStatusFilter = next), - onRefresh: () => loadSkills(state, { clearMessages: true }), - onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled), + onRefresh: () => void loadSkills(state, { clearMessages: true }), + onToggle: (key, enabled) => void updateSkillEnabled(state, key, enabled), onEdit: (key, value) => updateSkillEdit(state, key, value), - onSaveKey: (key) => saveSkillApiKey(state, key), + onSaveKey: (key) => void saveSkillApiKey(state, key), onInstall: (skillKey, name, installId) => - installSkill(state, skillKey, name, installId), + void installSkill(state, skillKey, name, installId), onDetailOpen: (key) => { state.skillsDetailKey = key; state.skillsDetailTab = "overview"; @@ -2827,11 +2836,13 @@ export function renderApp(state: AppViewState) { if (clawhubSearchTimer) { clearTimeout(clawhubSearchTimer); } - clawhubSearchTimer = setTimeout(() => searchClawHub(state, query), 300); + clawhubSearchTimer = setTimeout(() => { + void searchClawHub(state, query); + }, 300); }, - onClawHubDetailOpen: (slug) => loadClawHubDetail(state, slug), + onClawHubDetailOpen: (slug) => void loadClawHubDetail(state, slug), onClawHubDetailClose: () => closeClawHubDetail(state), - onClawHubInstall: (slug) => installFromClawHub(state, slug), + onClawHubInstall: (slug) => void installFromClawHub(state, slug), }), ) : nothing} @@ -2858,20 +2869,21 @@ export function renderApp(state: AppViewState) { execApprovalsSelectedAgent: state.execApprovalsSelectedAgent, execApprovalsTarget: state.execApprovalsTarget, execApprovalsTargetNodeId: state.execApprovalsTargetNodeId, - onRefresh: () => loadNodes(state), - onDevicesRefresh: () => loadDevices(state), - onDeviceApprove: (requestId) => approveDevicePairing(state, requestId), - onDeviceReject: (requestId) => rejectDevicePairing(state, requestId), + onRefresh: () => void loadNodes(state), + onDevicesRefresh: () => void loadDevices(state), + onDeviceApprove: (requestId) => void approveDevicePairing(state, requestId), + onDeviceReject: (requestId) => void rejectDevicePairing(state, requestId), onDeviceRotate: (deviceId, role, scopes) => - rotateDeviceToken(state, { deviceId, role, scopes }), - onDeviceRevoke: (deviceId, role) => revokeDeviceToken(state, { deviceId, role }), - onLoadConfig: () => loadConfig(state, { discardPendingChanges: true }), + void rotateDeviceToken(state, { deviceId, role, scopes }), + onDeviceRevoke: (deviceId, role) => + void revokeDeviceToken(state, { deviceId, role }), + onLoadConfig: () => void loadConfig(state, { discardPendingChanges: true }), onLoadExecApprovals: () => { const target = state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } : { kind: "gateway" as const }; - return loadExecApprovals(state, target); + void loadExecApprovals(state, target); }, onBindDefault: (nodeId) => { if (nodeId) { @@ -2888,7 +2900,7 @@ export function renderApp(state: AppViewState) { removeConfigFormValue(state, basePathLocal); } }, - onSaveBindings: () => saveConfig(state), + onSaveBindings: () => void saveConfig(state), onExecApprovalsTargetChange: (kind, nodeId) => { state.execApprovalsTarget = kind; state.execApprovalsTargetNodeId = nodeId; @@ -2908,7 +2920,7 @@ export function renderApp(state: AppViewState) { state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } : { kind: "gateway" as const }; - return saveExecApprovals(state, target); + void saveExecApprovals(state, target); }, }), ) @@ -2964,7 +2976,7 @@ export function renderApp(state: AppViewState) { onRefresh: () => { state.chatSideResult = null; state.resetToolStream(); - return refreshChat(state, { awaitHistory: true, scheduleScroll: false }); + void refreshChat(state, { awaitHistory: true, scheduleScroll: false }); }, onToggleFocusMode: () => { if (state.onboarding) { @@ -2982,8 +2994,8 @@ export function renderApp(state: AppViewState) { onHistoryKeydown: (input) => state.handleChatInputHistoryKey(input), attachments: state.chatAttachments, onAttachmentsChange: (next) => (state.chatAttachments = next), - onSend: () => state.handleSendChat(), - onCompact: () => state.handleSendChat("/compact", { restoreDraft: true }), + onSend: () => void state.handleSendChat(), + onCompact: () => void state.handleSendChat("/compact", { restoreDraft: true }), onOpenSessionCheckpoints: () => { state.sessionsExpandedCheckpointKey = state.sessionKey; state.setTab("sessions" as import("./navigation.ts").Tab); @@ -2992,7 +3004,7 @@ export function renderApp(state: AppViewState) { ...scopedAgentListParamsForSession(state, state.sessionKey), }); }, - onToggleRealtimeTalk: () => state.toggleRealtimeTalk(), + onToggleRealtimeTalk: () => void state.toggleRealtimeTalk(), onToggleRealtimeTalkOptions: () => { state.realtimeTalkOptionsOpen = !state.realtimeTalkOptionsOpen; }, @@ -3006,7 +3018,7 @@ export function renderApp(state: AppViewState) { state.chatSideResult = null; }, onNewSession: () => void createChatSession(state), - onClearHistory: async () => { + onClearHistory: runUiTask(async () => { if (!state.client || !state.connected) { return; } @@ -3037,7 +3049,7 @@ export function renderApp(state: AppViewState) { state.lastError = String(err); state.chatError = state.lastError; } - }, + }), agentsList: state.agentsList, currentAgentId: resolvedAgentId ?? "main", fullMessageAgentId: scopedAgentParamsForSession(state, state.sessionKey).agentId, @@ -3095,8 +3107,8 @@ export function renderApp(state: AppViewState) { callError: state.debugCallError, onCallMethodChange: (next) => (state.debugCallMethod = next), onCallParamsChange: (next) => (state.debugCallParams = next), - onRefresh: () => loadDebug(state), - onCall: () => callDebugMethod(state), + onRefresh: () => void loadDebug(state), + onCall: () => void callDebugMethod(state), }), ), ) @@ -3119,7 +3131,7 @@ export function renderApp(state: AppViewState) { state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled }; }, onToggleAutoFollow: (next) => (state.logsAutoFollow = next), - onRefresh: () => loadLogs(state, { reset: true }), + onRefresh: () => void loadLogs(state, { reset: true }), onExport: (lines, label) => state.exportLogs(lines, label), onScroll: (event) => state.handleLogsScroll(event), }), @@ -3185,7 +3197,7 @@ export function renderApp(state: AppViewState) { await loadWikiMemoryPalace(state); })(); }, - onOpenConfig: () => openConfigFile(state), + onOpenConfig: () => void openConfigFile(state), onOpenWikiPage: (lookup: string) => openWikiPage(lookup), onBackfillDiary: () => { syncDreamingSelectedAgent(); diff --git a/ui/src/ui/controllers/agents.test.ts b/ui/src/ui/controllers/agents.test.ts index d44efe08d581..323ab83425bd 100644 --- a/ui/src/ui/controllers/agents.test.ts +++ b/ui/src/ui/controllers/agents.test.ts @@ -2,8 +2,10 @@ import { describe, expect, it, vi } from "vitest"; import { loadAgents, loadToolsCatalog, loadToolsEffective, saveAgentsConfig } from "./agents.ts"; import type { AgentsConfigSaveState, AgentsState } from "./agents.ts"; -function createState(): { state: AgentsState; request: ReturnType } { - const request = vi.fn(); +type TestRequest = (method: string, payload?: unknown) => Promise; + +function createState(): { state: AgentsState; request: ReturnType> } { + const request = vi.fn(); const state: AgentsState = { client: { request, @@ -46,7 +48,7 @@ function createState(): { state: AgentsState; request: ReturnType function createSaveState(): { state: AgentsConfigSaveState; - request: ReturnType; + request: ReturnType>; } { const { state, request } = createState(); return { diff --git a/ui/src/ui/controllers/dreaming.test.ts b/ui/src/ui/controllers/dreaming.test.ts index 1ab7834ac2a1..e55758b1b4ba 100644 --- a/ui/src/ui/controllers/dreaming.test.ts +++ b/ui/src/ui/controllers/dreaming.test.ts @@ -15,8 +15,10 @@ import { type DreamingState, } from "./dreaming.ts"; -function createState(): { state: DreamingState; request: ReturnType } { - const request = vi.fn(); +type TestRequest = (method: string, payload?: unknown) => Promise; + +function createState(): { state: DreamingState; request: ReturnType> } { + const request = vi.fn(); const state: DreamingState = { client: { request, @@ -61,7 +63,9 @@ function createDeferred() { return { promise, resolve, reject }; } -function getConfigPatchRawPayload(request: ReturnType): Record { +function getConfigPatchRawPayload( + request: ReturnType>, +): Record { const patchCall = request.mock.calls.find((entry) => entry[0] === "config.patch"); if (!patchCall) { throw new Error("Expected config.patch request"); @@ -71,7 +75,7 @@ function getConfigPatchRawPayload(request: ReturnType): Record, + request: ReturnType>, method: string, ): Record { const call = request.mock.calls.find((entry) => entry[0] === method); @@ -246,8 +250,12 @@ describe("dreaming controller", () => { const { state, request } = createState(); const agentA = createDeferred(); const agentB = createDeferred(); - request.mockImplementation(async (_method: string, payload?: { agentId?: string }) => { - return payload?.agentId === "agent-b" ? agentB.promise : agentA.promise; + request.mockImplementation(async (_method: string, payload?: unknown) => { + const agentId = + typeof payload === "object" && payload !== null && "agentId" in payload + ? payload.agentId + : undefined; + return agentId === "agent-b" ? agentB.promise : agentA.promise; }); state.selectedAgentId = "agent-a"; @@ -967,8 +975,12 @@ describe("dreaming controller", () => { const { state, request } = createState(); const agentA = createDeferred(); const agentB = createDeferred(); - request.mockImplementation(async (_method: string, payload?: { agentId?: string }) => { - return payload?.agentId === "agent-b" ? agentB.promise : agentA.promise; + request.mockImplementation(async (_method: string, payload?: unknown) => { + const agentId = + typeof payload === "object" && payload !== null && "agentId" in payload + ? payload.agentId + : undefined; + return agentId === "agent-b" ? agentB.promise : agentA.promise; }); state.selectedAgentId = "agent-a"; diff --git a/ui/src/ui/controllers/skills.test.ts b/ui/src/ui/controllers/skills.test.ts index ad5f06ae873d..2080f3ead17a 100644 --- a/ui/src/ui/controllers/skills.test.ts +++ b/ui/src/ui/controllers/skills.test.ts @@ -11,8 +11,10 @@ import { type SkillsState, } from "./skills.ts"; -function createState(): { state: SkillsState; request: ReturnType } { - const request = vi.fn(); +type TestRequest = (method: string, payload?: unknown) => Promise; + +function createState(): { state: SkillsState; request: ReturnType> } { + const request = vi.fn(); const state: SkillsState = { client: { request, @@ -53,7 +55,7 @@ function createState(): { state: SkillsState; request: ReturnType return { state, request }; } -function createDeferredRequestQueue(request: ReturnType) { +function createDeferredRequestQueue(request: ReturnType>) { const resolvers: Array<(value: unknown) => void> = []; request.mockImplementation( () => @@ -68,7 +70,10 @@ function createDeferredRequestQueue(request: ReturnType) { }; } -function mockSkillMutationRequests(request: ReturnType, installMessage?: string) { +function mockSkillMutationRequests( + request: ReturnType>, + installMessage?: string, +) { request.mockImplementation(async (method: string) => { if (method === "skills.install" && installMessage) { return { message: installMessage };