diff --git a/CHANGELOG.md b/CHANGELOG.md index a4597251f68..bb50e132a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai - Telegram: pass agent-scoped media roots through gateway message actions so workspace-local media from the active agent is not rejected as cross-agent access. Thanks @frankekn. - CLI/gateway: keep `gateway status --deep` plugin-aware so configured plugin manifest warnings, including missing channel config metadata, stay visible during install and update smoke checks. +- Feishu: fall back to a top-level group send when normal group quoted replies target a withdrawn or missing message, preventing replies from disappearing silently while preserving native topic safety. Fixes #79349. Thanks @arlen8411. - Doctor: stop flagging the live compatibility agent directory as orphaned when the configured default agent is not `main`. Fixes #74313. (#74438) Thanks @carlos4s. - Auth/Claude CLI: persist fresher managed external CLI OAuth credentials back to `auth-profiles.json`, preventing stale `anthropic:claude-cli` profiles from repeatedly bootstrapping and flooding debug logs. Fixes #80129. Thanks @Caulderein. - Context: render `/context map` only from actual run context and persist Codex app-server run reports without counting deferred tool-search schemas as prompt-loaded tool schemas. diff --git a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts index 6392fd5a03d..45b2310eb87 100644 --- a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts +++ b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts @@ -206,12 +206,16 @@ function requireRecord(value: unknown, label: string): Record { return value as Record; } -function requireFirstCallArg(mock: unknown, label: string, _type?: (value: T) => T): T { +function optionalString(value: unknown): string { + return typeof value === "string" ? value : ""; +} + +function requireFirstCallArg(mock: unknown, label: string): unknown { const call = (mock as MockCallReader).mock.calls.at(0); if (!call) { throw new Error(`expected ${label} to be called`); } - return call[0] as T; + return call[0]; } function requireRequestParams( @@ -238,7 +242,7 @@ function expectRequestInputTextContains( expect( input.some((entry) => { const item = requireRecord(entry, "turn/start input entry"); - return item.type === "text" && typeof item.text === "string" && item.text.includes(expected); + return item.type === "text" && optionalString(item.text).includes(expected); }), ).toBe(true); } @@ -275,18 +279,17 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => { throw new Error("expected bootstrap hook"); } expect(contextEngine.bootstrap).toHaveBeenCalledTimes(1); - const bootstrapParams = requireFirstCallArg< - Parameters>[0] - >(contextEngine.bootstrap, "bootstrap"); + const bootstrapParams = requireFirstCallArg(contextEngine.bootstrap, "bootstrap") as Parameters< + NonNullable + >[0]; expect(bootstrapParams.sessionId).toBe("session-1"); expect(bootstrapParams.sessionKey).toBe("agent:main:session-1"); expect(bootstrapParams.sessionFile).toBe(sessionFile); expect(contextEngine.assemble).toHaveBeenCalledTimes(1); - const assembleParams = requireFirstCallArg[0]>( - contextEngine.assemble, - "assemble", - ); + const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters< + ContextEngine["assemble"] + >[0]; expect(assembleParams.sessionId).toBe("session-1"); expect(assembleParams.sessionKey).toBe("agent:main:session-1"); expect(assembleParams.tokenBudget).toBe(321); @@ -297,8 +300,7 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => { expect(assembleParams.availableTools).toEqual(new Set()); const threadStartParams = requireRequestParams(harness, "thread/start"); - const developerInstructions = threadStartParams.developerInstructions; - expect(typeof developerInstructions === "string" ? developerInstructions : "").toContain( + expect(optionalString(threadStartParams.developerInstructions)).toContain( "context-engine system", ); expectRequestInputTextContains(harness, "OpenClaw assembled context for this turn:"); @@ -327,9 +329,9 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => { await run; expect(afterTurn).toHaveBeenCalledTimes(1); - const afterTurnCall = requireFirstCallArg< - Parameters>[0] - >(afterTurn, "afterTurn"); + const afterTurnCall = requireFirstCallArg(afterTurn, "afterTurn") as Parameters< + NonNullable + >[0]; expect(afterTurnCall.sessionId).toBe("session-1"); expect(afterTurnCall.sessionKey).toBe("agent:main:session-1"); expect(afterTurnCall.prePromptMessageCount).toBe(0); @@ -370,17 +372,16 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => { await harness.completeTurn(); await run; - const assembleParams = requireFirstCallArg[0]>( - contextEngine.assemble, - "assemble", - ); + const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters< + ContextEngine["assemble"] + >[0]; expect(assembleParams.messages.map((message) => message.role)).toEqual([ "assistant", "assistant", ]); - const afterTurnParams = requireFirstCallArg< - Parameters>[0] - >(afterTurn, "afterTurn"); + const afterTurnParams = requireFirstCallArg(afterTurn, "afterTurn") as Parameters< + NonNullable + >[0]; expect(afterTurnParams.prePromptMessageCount).toBe(2); expectRequestInputTextContains(harness, "bootstrap context"); }); diff --git a/extensions/diffs/src/browser.test.ts b/extensions/diffs/src/browser.test.ts index 4a851346617..de585a807c8 100644 --- a/extensions/diffs/src/browser.test.ts +++ b/extensions/diffs/src/browser.test.ts @@ -81,9 +81,9 @@ describe("PlaywrightDiffScreenshotter", () => { expect(launchMock).toHaveBeenCalledTimes(1); expect(browser.newPage).toHaveBeenCalledTimes(2); - const firstPageParams = browser.newPage.mock.calls[0]?.[0] as - | { deviceScaleFactor?: number } - | undefined; + const firstPageParams = ( + browser.newPage.mock.calls as Array<[{ deviceScaleFactor?: number }?]> + )[0]?.[0]; expect(firstPageParams?.deviceScaleFactor).toBe(2); expect(pages).toHaveLength(2); expect(pages[0]?.close).toHaveBeenCalledTimes(1); diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index 0ab42878b4f..8af0e7c1d42 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -105,9 +105,10 @@ function createConfigWithDiscordAccount(overrides: Record = {}) type MockCallReader = { mock: { calls: unknown[][] } }; function mockMessages(mock: unknown): string[] { - return (mock as MockCallReader).mock.calls.map((call) => - typeof call[0] === "string" ? call[0] : "", - ); + return (mock as MockCallReader).mock.calls.map((call) => { + const message = call[0]; + return typeof message === "string" ? message : ""; + }); } function expectMockLogContains(mock: unknown, expected: string): void { diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index c5ae0b93234..edcc4e4bfaf 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -926,6 +926,38 @@ describe("createFeishuReplyDispatcher streaming behavior", () => { }); }); + it("allows top-level fallback for normal group quoted replies", async () => { + const { options } = createDispatcherHarness({ + replyToMessageId: "om_quote_reply", + replyInThread: true, + threadReply: true, + rootId: "om_original_msg", + }); + await options.deliver({ text: "plain text" }, { kind: "final" }); + + expectMockArgFields(sendMessageFeishuMock, "message send params", { + replyToMessageId: "om_quote_reply", + replyInThread: true, + allowTopLevelReplyFallback: true, + }); + }); + + it("keeps native topic replies opted out of top-level fallback", async () => { + const { options } = createDispatcherHarness({ + replyToMessageId: "om_topic_root", + replyInThread: true, + threadReply: true, + rootId: "om_topic_root", + }); + await options.deliver({ text: "plain text" }, { kind: "final" }); + + expectMockArgFields(sendMessageFeishuMock, "message send params", { + replyToMessageId: "om_topic_root", + replyInThread: true, + allowTopLevelReplyFallback: false, + }); + }); + it("passes replyInThread to sendStructuredCardFeishu for card text", async () => { resolveFeishuAccountMock.mockReturnValue({ accountId: "main", diff --git a/extensions/feishu/src/reply-dispatcher.ts b/extensions/feishu/src/reply-dispatcher.ts index 71c9cbf8452..6888c819b57 100644 --- a/extensions/feishu/src/reply-dispatcher.ts +++ b/extensions/feishu/src/reply-dispatcher.ts @@ -147,6 +147,12 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP const sendReplyToMessageId = skipReplyToInMessages ? undefined : replyToMessageId; const threadReplyMode = threadReply === true; const effectiveReplyInThread = threadReplyMode ? true : replyInThread; + const allowTopLevelReplyFallback = + effectiveReplyInThread === true && + threadReplyMode && + rootId !== undefined && + sendReplyToMessageId !== undefined && + sendReplyToMessageId !== rootId; const account = resolveFeishuRuntimeAccount({ cfg, accountId }); const prefixContext = createReplyPrefixContext({ cfg, agentId }); @@ -465,6 +471,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP text: chunk, replyToMessageId: sendReplyToMessageId, replyInThread: effectiveReplyInThread, + allowTopLevelReplyFallback, accountId, }); }, @@ -491,6 +498,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP text: chunk, replyToMessageId: sendReplyToMessageId, replyInThread: effectiveReplyInThread, + allowTopLevelReplyFallback, accountId, }); }, @@ -605,6 +613,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP text: chunk, replyToMessageId: sendReplyToMessageId, replyInThread: effectiveReplyInThread, + allowTopLevelReplyFallback, accountId, header: cardHeader, note: cardNote, @@ -623,6 +632,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP text: chunk, replyToMessageId: sendReplyToMessageId, replyInThread: effectiveReplyInThread, + allowTopLevelReplyFallback, accountId, }); }, diff --git a/extensions/feishu/src/send.reply-fallback.test.ts b/extensions/feishu/src/send.reply-fallback.test.ts index 2b444005853..6675af3444e 100644 --- a/extensions/feishu/src/send.reply-fallback.test.ts +++ b/extensions/feishu/src/send.reply-fallback.test.ts @@ -211,7 +211,91 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => { expect(createMock).not.toHaveBeenCalled(); }); - it("fails thread replies instead of falling back to a top-level send", async () => { + it("falls back to a top-level group send when normal quoted replies target withdrawn messages", async () => { + resolveFeishuSendTargetMock.mockReturnValue({ + client: { + im: { + message: { + reply: replyMock, + create: createMock, + }, + }, + }, + receiveId: "oc_group_1", + receiveIdType: "chat_id", + }); + replyMock.mockResolvedValue({ + code: 230011, + msg: "The message was withdrawn.", + }); + createMock.mockResolvedValue({ + code: 0, + data: { message_id: "om_group_fallback" }, + }); + + await expectFallbackResult( + () => + sendMessageFeishu({ + cfg: {} as never, + to: "chat:oc_group_1", + text: "hello", + replyToMessageId: "om_parent", + replyInThread: true, + allowTopLevelReplyFallback: true, + }), + "om_group_fallback", + ); + + expect(replyMock).toHaveBeenCalledWith({ + path: { message_id: "om_parent" }, + data: expect.objectContaining({ + reply_in_thread: true, + }), + }); + expect(createMock).toHaveBeenCalledWith({ + params: { receive_id_type: "chat_id" }, + data: expect.objectContaining({ + receive_id: "oc_group_1", + msg_type: "post", + }), + }); + }); + + it("falls back to create when normal quoted replies throw withdrawn errors", async () => { + resolveFeishuSendTargetMock.mockReturnValue({ + client: { + im: { + message: { + reply: replyMock, + create: createMock, + }, + }, + }, + receiveId: "oc_group_1", + receiveIdType: "chat_id", + }); + const sdkError = Object.assign(new Error("request failed"), { code: 230011 }); + replyMock.mockRejectedValue(sdkError); + createMock.mockResolvedValue({ + code: 0, + data: { message_id: "om_thrown_thread_fallback" }, + }); + + await expectFallbackResult( + () => + sendMessageFeishu({ + cfg: {} as never, + to: "chat:oc_group_1", + text: "hello", + replyToMessageId: "om_parent", + replyInThread: true, + allowTopLevelReplyFallback: true, + }), + "om_thrown_thread_fallback", + ); + }); + + it("fails native thread replies instead of falling back to a top-level send", async () => { replyMock.mockResolvedValue({ code: 230011, msg: "The message was withdrawn.", @@ -230,15 +314,9 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => { ); expect(createMock).not.toHaveBeenCalled(); - expect(replyMock).toHaveBeenCalledWith({ - path: { message_id: "om_parent" }, - data: expect.objectContaining({ - reply_in_thread: true, - }), - }); }); - it("fails thrown withdrawn thread replies instead of falling back to create", async () => { + it("fails thrown withdrawn native thread replies instead of falling back to create", async () => { const sdkError = Object.assign(new Error("request failed"), { code: 230011 }); replyMock.mockRejectedValue(sdkError); @@ -257,6 +335,44 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => { expect(createMock).not.toHaveBeenCalled(); }); + it("preserves non-withdrawn thread reply failures", async () => { + replyMock.mockResolvedValue({ + code: 999999, + msg: "unknown failure", + }); + + await expect( + sendMessageFeishu({ + cfg: {} as never, + to: "chat:oc_group_1", + text: "hello", + replyToMessageId: "om_parent", + replyInThread: true, + allowTopLevelReplyFallback: true, + }), + ).rejects.toThrow("Feishu reply failed"); + + expect(createMock).not.toHaveBeenCalled(); + }); + + it("preserves thrown non-withdrawn thread reply failures", async () => { + const sdkError = Object.assign(new Error("rate limited"), { code: 99991400 }); + replyMock.mockRejectedValue(sdkError); + + await expect( + sendMessageFeishu({ + cfg: {} as never, + to: "chat:oc_group_1", + text: "hello", + replyToMessageId: "om_parent", + replyInThread: true, + allowTopLevelReplyFallback: true, + }), + ).rejects.toThrow("rate limited"); + + expect(createMock).not.toHaveBeenCalled(); + }); + it("still falls back for non-thread replies to withdrawn targets", async () => { replyMock.mockResolvedValue({ code: 230011, diff --git a/extensions/feishu/src/send.ts b/extensions/feishu/src/send.ts index f1c873e01ac..6143302d8b2 100644 --- a/extensions/feishu/src/send.ts +++ b/extensions/feishu/src/send.ts @@ -144,6 +144,7 @@ async function sendReplyOrFallbackDirect( params: { replyToMessageId?: string; replyInThread?: boolean; + allowTopLevelReplyFallback?: boolean; content: string; msgType: string; directParams: { @@ -160,11 +161,12 @@ async function sendReplyOrFallbackDirect( return sendFallbackDirect(client, params.directParams, params.directErrorPrefix); } - const threadReplyFallbackError = params.replyInThread - ? new Error( - "Feishu thread reply failed: reply target is unavailable and cannot safely fall back to a top-level send.", - ) - : null; + const replyTargetFallbackError = + params.replyInThread && params.allowTopLevelReplyFallback !== true + ? new Error( + "Feishu thread reply failed: reply target is unavailable and cannot safely fall back to a top-level send.", + ) + : null; let response: { code?: number; msg?: string; data?: { message_id?: string } }; try { @@ -180,14 +182,14 @@ async function sendReplyOrFallbackDirect( if (!isWithdrawnReplyError(err)) { throw createFeishuApiError(err, params.replyErrorPrefix, { includeNestedErrorLogId: true }); } - if (threadReplyFallbackError) { - throw threadReplyFallbackError; + if (replyTargetFallbackError) { + throw replyTargetFallbackError; } return sendFallbackDirect(client, params.directParams, params.directErrorPrefix); } if (shouldFallbackFromReplyTarget(response)) { - if (threadReplyFallbackError) { - throw threadReplyFallbackError; + if (replyTargetFallbackError) { + throw replyTargetFallbackError; } return sendFallbackDirect(client, params.directParams, params.directErrorPrefix); } @@ -526,6 +528,7 @@ export type SendFeishuMessageParams = { replyToMessageId?: string; /** When true, reply creates a Feishu topic thread instead of an inline reply */ replyInThread?: boolean; + allowTopLevelReplyFallback?: boolean; /** Mention target users */ mentions?: MentionTarget[]; /** Account ID (optional, uses default if not specified) */ @@ -557,7 +560,16 @@ export function buildFeishuPostMessagePayload(params: { messageText: string }): export async function sendMessageFeishu( params: SendFeishuMessageParams, ): Promise { - const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params; + const { + cfg, + to, + text, + replyToMessageId, + replyInThread, + allowTopLevelReplyFallback, + mentions, + accountId, + } = params; const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId }); const tableMode = resolveMarkdownTableMode({ cfg, @@ -577,6 +589,7 @@ export async function sendMessageFeishu( return sendReplyOrFallbackDirect(client, { replyToMessageId, replyInThread, + allowTopLevelReplyFallback, content, msgType, directParams, @@ -592,11 +605,13 @@ export type SendFeishuCardParams = { replyToMessageId?: string; /** When true, reply creates a Feishu topic thread instead of an inline reply */ replyInThread?: boolean; + allowTopLevelReplyFallback?: boolean; accountId?: string; }; export async function sendCardFeishu(params: SendFeishuCardParams): Promise { - const { cfg, to, card, replyToMessageId, replyInThread, accountId } = params; + const { cfg, to, card, replyToMessageId, replyInThread, allowTopLevelReplyFallback, accountId } = + params; const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId }); const content = JSON.stringify(card); @@ -604,6 +619,7 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise { - const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId, header, note } = - params; + const { + cfg, + to, + text, + replyToMessageId, + replyInThread, + allowTopLevelReplyFallback, + mentions, + accountId, + header, + note, + } = params; let cardText = text; if (mentions && mentions.length > 0) { cardText = buildMentionedCardContent(mentions, text); } const card = buildStructuredCard(cardText, { header, note }); - return sendCardFeishu({ cfg, to, card, replyToMessageId, replyInThread, accountId }); + return sendCardFeishu({ + cfg, + to, + card, + replyToMessageId, + replyInThread, + allowTopLevelReplyFallback, + accountId, + }); } /** @@ -794,15 +829,33 @@ export async function sendMarkdownCardFeishu(params: { replyToMessageId?: string; /** When true, reply creates a Feishu topic thread instead of an inline reply */ replyInThread?: boolean; + allowTopLevelReplyFallback?: boolean; /** Mention target users */ mentions?: MentionTarget[]; accountId?: string; }): Promise { - const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params; + const { + cfg, + to, + text, + replyToMessageId, + replyInThread, + allowTopLevelReplyFallback, + mentions, + accountId, + } = params; let cardText = text; if (mentions && mentions.length > 0) { cardText = buildMentionedCardContent(mentions, text); } const card = buildMarkdownCard(cardText); - return sendCardFeishu({ cfg, to, card, replyToMessageId, replyInThread, accountId }); + return sendCardFeishu({ + cfg, + to, + card, + replyToMessageId, + replyInThread, + allowTopLevelReplyFallback, + accountId, + }); } diff --git a/extensions/googlechat/src/channel.test.ts b/extensions/googlechat/src/channel.test.ts index df94ac0bc08..b9611e97270 100644 --- a/extensions/googlechat/src/channel.test.ts +++ b/extensions/googlechat/src/channel.test.ts @@ -215,29 +215,20 @@ function setupRuntimeMediaMocks(params: { loadFileName: string; loadBytes: strin return { loadOutboundMediaFromUrl, fetchRemoteMedia }; } -function requireMockArg( - mock: ReturnType, - callIndex = 0, - argIndex = 0, - _type?: (value: T) => T, -): T { +function requireMockArg(mock: ReturnType, callIndex = 0, argIndex = 0): unknown { const call = mock.mock.calls[callIndex]; if (!call) { throw new Error(`expected mock call ${callIndex}`); } - return call[argIndex] as T; + return call[argIndex]; } -function requireMockArgs( - mock: ReturnType, - callIndex = 0, - _type?: (value: T) => T, -): T { +function requireMockArgs(mock: ReturnType, callIndex = 0): unknown[] { const call = mock.mock.calls[callIndex]; if (!call) { throw new Error(`expected mock call ${callIndex}`); } - return call as T; + return call; } describe("googlechatPlugin outbound sendMedia", () => { @@ -282,9 +273,10 @@ describe("googlechatPlugin outbound sendMedia", () => { text: "threaded", threadId: "thread-1", }); - const request = requireMockArg<{ space?: string; thread?: string }>( - sendGoogleChatMessageMock, - ); + const request = requireMockArg(sendGoogleChatMessageMock) as { + space?: string; + thread?: string; + }; expect(request.space).toBe("spaces/AAA"); expect(request.thread).toBe("thread-1"); }, @@ -329,22 +321,25 @@ describe("googlechatPlugin outbound sendMedia", () => { accountId: "default", }); - const [mediaUrl, mediaOptions] = - requireMockArgs<[string, { mediaLocalRoots?: string[] }]>(loadOutboundMediaFromUrl); + const [mediaUrl, mediaOptions] = requireMockArgs(loadOutboundMediaFromUrl) as [ + string, + { mediaLocalRoots?: string[] }, + ]; expect(mediaUrl).toBe("/tmp/workspace/image.png"); expect(mediaOptions.mediaLocalRoots).toEqual(["/tmp/workspace"]); expect(fetchRemoteMedia).not.toHaveBeenCalled(); - const uploadRequest = requireMockArg<{ + const uploadRequest = requireMockArg(uploadGoogleChatAttachmentMock) as { space?: string; filename?: string; contentType?: string; - }>(uploadGoogleChatAttachmentMock); + }; expect(uploadRequest.space).toBe("spaces/AAA"); expect(uploadRequest.filename).toBe("image.png"); expect(uploadRequest.contentType).toBe("image/png"); - const sendRequest = requireMockArg<{ space?: string; text?: string }>( - sendGoogleChatMessageMock, - ); + const sendRequest = requireMockArg(sendGoogleChatMessageMock) as { + space?: string; + text?: string; + }; expect(sendRequest.space).toBe("spaces/AAA"); expect(sendRequest.text).toBe("caption"); expect(result.messageId).toBe("spaces/AAA/messages/msg-1"); @@ -375,21 +370,25 @@ describe("googlechatPlugin outbound sendMedia", () => { accountId: "default", }); - const remoteRequest = requireMockArg<{ url?: string; maxBytes?: number }>(fetchRemoteMedia); + const remoteRequest = requireMockArg(fetchRemoteMedia) as { + url?: string; + maxBytes?: number; + }; expect(remoteRequest.url).toBe("https://example.com/image.png"); expect(remoteRequest.maxBytes).toBe(20 * 1024 * 1024); expect(loadOutboundMediaFromUrl).not.toHaveBeenCalled(); - const uploadRequest = requireMockArg<{ + const uploadRequest = requireMockArg(uploadGoogleChatAttachmentMock) as { space?: string; filename?: string; contentType?: string; - }>(uploadGoogleChatAttachmentMock); + }; expect(uploadRequest.space).toBe("spaces/AAA"); expect(uploadRequest.filename).toBe("remote.png"); expect(uploadRequest.contentType).toBe("image/png"); - const sendRequest = requireMockArg<{ space?: string; text?: string }>( - sendGoogleChatMessageMock, - ); + const sendRequest = requireMockArg(sendGoogleChatMessageMock) as { + space?: string; + text?: string; + }; expect(sendRequest.space).toBe("spaces/AAA"); expect(sendRequest.text).toBe("caption"); expect(result.messageId).toBe("spaces/AAA/messages/msg-2"); @@ -525,11 +524,11 @@ describe("googlechatPlugin outbound cfg threading", () => { cfg, accountId: "work", }); - const request = requireMockArg<{ + const request = requireMockArg(sendGoogleChatMessageMock) as { account?: unknown; space?: string; text?: string; - }>(sendGoogleChatMessageMock); + }; expect(request.account).toBe(account); expect(request.space).toBe("spaces/WORK"); expect(request.text).toBe(googlechatPairingTextAdapter.message); @@ -567,11 +566,11 @@ describe("googlechatPlugin outbound cfg threading", () => { cfg, accountId: "default", }); - const request = requireMockArg<{ + const request = requireMockArg(sendGoogleChatMessageMock) as { account?: unknown; space?: string; text?: string; - }>(sendGoogleChatMessageMock); + }; expect(request.account).toBe(account); expect(request.space).toBe("spaces/AAA"); expect(request.text).toBe("hello"); @@ -619,21 +618,24 @@ describe("googlechatPlugin outbound cfg threading", () => { cfg, accountId: "default", }); - const remoteRequest = requireMockArg<{ url?: string; maxBytes?: number }>(fetchRemoteMedia); + const remoteRequest = requireMockArg(fetchRemoteMedia) as { + url?: string; + maxBytes?: number; + }; expect(remoteRequest.url).toBe("https://example.com/file.png"); expect(remoteRequest.maxBytes).toBe(8 * 1024 * 1024); - const uploadRequest = requireMockArg<{ + const uploadRequest = requireMockArg(uploadGoogleChatAttachmentMock) as { account?: unknown; space?: string; filename?: string; - }>(uploadGoogleChatAttachmentMock); + }; expect(uploadRequest.account).toBe(account); expect(uploadRequest.space).toBe("spaces/AAA"); expect(uploadRequest.filename).toBe("remote.png"); - const sendRequest = requireMockArg<{ + const sendRequest = requireMockArg(sendGoogleChatMessageMock) as { account?: unknown; attachments?: Array<{ attachmentUploadToken: string; contentName: string }>; - }>(sendGoogleChatMessageMock); + }; expect(sendRequest.account).toBe(account); expect(sendRequest.attachments).toEqual([ { attachmentUploadToken: "token-1", contentName: "remote.png" }, @@ -666,8 +668,10 @@ describe("googlechatPlugin outbound cfg threading", () => { expect(result.messageId).toBe("spaces/AAA/messages/msg-cold"); expect(result.chatId).toBe("spaces/AAA"); - const [mediaUrl, mediaOptions] = - requireMockArgs<[string, { mediaLocalRoots?: string[] }]>(loadOutboundMediaFromUrl); + const [mediaUrl, mediaOptions] = requireMockArgs(loadOutboundMediaFromUrl) as [ + string, + { mediaLocalRoots?: string[] }, + ]; expect(mediaUrl).toBe("/tmp/workspace/image.png"); expect(mediaOptions.mediaLocalRoots).toEqual(["/tmp/workspace"]); }); diff --git a/extensions/irc/src/inbound.behavior.test.ts b/extensions/irc/src/inbound.behavior.test.ts index 4b2916b7798..2c9c2a1f955 100644 --- a/extensions/irc/src/inbound.behavior.test.ts +++ b/extensions/irc/src/inbound.behavior.test.ts @@ -102,7 +102,9 @@ describe("irc inbound behavior", () => { }); it("issues a DM pairing challenge and sends the reply to the sender nick", async () => { - const sendReply = vi.fn(async () => {}); + const sendReply = vi.fn<(target: string, text: string, replyToId?: string) => Promise>( + async () => {}, + ); await handleIrcInbound({ message: createMessage(), @@ -129,7 +131,7 @@ describe("irc inbound behavior", () => { expect.stringContaining("Your IRC id: alice!ident@example.com"), undefined, ); - const replyMessages = (sendReply.mock.calls as unknown[][]).map((call) => String(call[1])); + const replyMessages = sendReply.mock.calls.map((call) => call[1]); expect(replyMessages.some((message) => message.includes("CODE"))).toBe(true); }); diff --git a/extensions/line/src/bot-handlers.test.ts b/extensions/line/src/bot-handlers.test.ts index 3710b6649f1..9e243fc9a4b 100644 --- a/extensions/line/src/bot-handlers.test.ts +++ b/extensions/line/src/bot-handlers.test.ts @@ -84,7 +84,7 @@ vi.mock("openclaw/plugin-sdk/routing", () => ({ const { readAllowFromStoreMock, upsertPairingRequestMock } = vi.hoisted(() => ({ readAllowFromStoreMock: vi.fn(async () => [] as string[]), - upsertPairingRequestMock: vi.fn(async () => ({ code: "CODE", created: true })), + upsertPairingRequestMock: vi.fn(async (_args: unknown) => ({ code: "CODE", created: true })), })); vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({ diff --git a/extensions/line/src/setup-surface.test.ts b/extensions/line/src/setup-surface.test.ts index dc43a06ce07..578e5b0ac7d 100644 --- a/extensions/line/src/setup-surface.test.ts +++ b/extensions/line/src/setup-surface.test.ts @@ -388,11 +388,13 @@ describe("line runtime api", () => { }); function createRuntime() { - const monitorLineProvider = vi.fn(async () => ({ - account: { accountId: "default" }, - handleWebhook: async () => {}, - stop: () => {}, - })); + const monitorLineProvider = vi.fn( + async (_opts: { accountId?: string; channelAccessToken: string; channelSecret: string }) => ({ + account: { accountId: "default" }, + handleWebhook: async () => {}, + stop: () => {}, + }), + ); const runtime = { channel: { diff --git a/extensions/memory-core/src/dreaming-phases.test.ts b/extensions/memory-core/src/dreaming-phases.test.ts index 879e4b15d21..a1f14a19121 100644 --- a/extensions/memory-core/src/dreaming-phases.test.ts +++ b/extensions/memory-core/src/dreaming-phases.test.ts @@ -77,7 +77,10 @@ function requireCandidateKeyByPath( } function mockStringMessages(mock: { mock: { calls: unknown[][] } }): string[] { - return mock.mock.calls.map((call) => (typeof call[0] === "string" ? call[0] : "")); + return mock.mock.calls.map((call) => { + const message = call[0]; + return typeof message === "string" ? message : ""; + }); } function expectIncludesSubstring(values: readonly string[], expected: string): void { diff --git a/extensions/memory-core/src/dreaming.test.ts b/extensions/memory-core/src/dreaming.test.ts index 1db53dc8322..d048da77a8a 100644 --- a/extensions/memory-core/src/dreaming.test.ts +++ b/extensions/memory-core/src/dreaming.test.ts @@ -149,7 +149,10 @@ function createCronHarness( } function mockStringMessages(mock: { mock: { calls: unknown[][] } }): string[] { - return mock.mock.calls.map((call) => (typeof call[0] === "string" ? call[0] : "")); + return mock.mock.calls.map((call) => { + const message = call[0]; + return typeof message === "string" ? message : ""; + }); } function expectLogContains(mock: { mock: { calls: unknown[][] } }, expected: string): void { diff --git a/extensions/slack/src/monitor/media.test.ts b/extensions/slack/src/monitor/media.test.ts index 37a7c5e96ce..fd09488c6e4 100644 --- a/extensions/slack/src/monitor/media.test.ts +++ b/extensions/slack/src/monitor/media.test.ts @@ -141,7 +141,9 @@ function expectSaveMediaBufferCall(mock: unknown, contentType: string, maxBytes: } function expectVerboseLogContains(expected: string): void { - const messages = vi.mocked(logVerbose).mock.calls.map((call) => call[0]); + const messages = vi.mocked(logVerbose).mock.calls.map((call) => + typeof call[0] === "string" ? call[0] : "", + ); expect(messages.some((message) => message.includes(expected))).toBe(true); } diff --git a/extensions/synology-chat/src/channel.test.ts b/extensions/synology-chat/src/channel.test.ts index b25078147b3..fb7b7a05f1b 100644 --- a/extensions/synology-chat/src/channel.test.ts +++ b/extensions/synology-chat/src/channel.test.ts @@ -31,7 +31,10 @@ function expectIncludesSubstring(values: readonly string[], expected: string): v } function mockStringMessages(mock: { mock: { calls: unknown[][] } }): string[] { - return mock.mock.calls.map((call) => (typeof call[0] === "string" ? call[0] : "")); + return mock.mock.calls.map((call) => { + const message = call[0]; + return typeof message === "string" ? message : ""; + }); } const clientModule = await import("./client.js"); diff --git a/extensions/telegram/src/webhook.test.ts b/extensions/telegram/src/webhook.test.ts index 359967db33f..2f1abda089d 100644 --- a/extensions/telegram/src/webhook.test.ts +++ b/extensions/telegram/src/webhook.test.ts @@ -136,9 +136,10 @@ function requireMockCall(mock: unknown, index: number, label: string): unknown[] } function mockMessages(mock: unknown): string[] { - return (mock as MockCallReader).mock.calls.map((call) => - typeof call[0] === "string" ? call[0] : "", - ); + return (mock as MockCallReader).mock.calls.map((call) => { + const message = call[0]; + return typeof message === "string" ? message : ""; + }); } function expectMockMessageContains(mock: unknown, expected: string): void { diff --git a/security/opengrep/compile-rules.mjs b/security/opengrep/compile-rules.mjs index 3b062052e8d..b78104e9bf1 100644 --- a/security/opengrep/compile-rules.mjs +++ b/security/opengrep/compile-rules.mjs @@ -116,7 +116,7 @@ function toPortablePath(filePath, repoRoot = REPO_ROOT) { function rewriteRule(rule, params) { const originalId = String(rule.id ?? "rule"); - const metadata = { ...(rule.metadata ?? {}) }; + const metadata = { ...rule.metadata }; const sourceId = sourceIdFromMetadata(metadata); if (!sourceId) { throw new Error( @@ -183,7 +183,7 @@ async function listYamlFiles(dir) { } } await walk(dir); - return out.toSorted(); + return out.toSorted((a, b) => a.localeCompare(b)); } async function compile(opts) { diff --git a/src/cli/daemon-cli/restart-health.test.ts b/src/cli/daemon-cli/restart-health.test.ts index 1f3011cc783..58f943f8038 100644 --- a/src/cli/daemon-cli/restart-health.test.ts +++ b/src/cli/daemon-cli/restart-health.test.ts @@ -54,10 +54,10 @@ function makeGatewayService( } as unknown as GatewayService; } -function firstCallArg(mock: { mock: { calls: unknown[][] } }, _type?: (value: T) => T): T { +function firstCallArg(mock: { mock: { calls: unknown[][] } }): unknown { const call = mock.mock.calls[0]; expect(call).toBeDefined(); - return call?.[0] as T; + return call?.[0]; } async function inspectGatewayRestartWithSnapshot(params: { @@ -241,7 +241,7 @@ describe("inspectGatewayRestart", () => { }); expect(snapshot.healthy).toBe(true); - expect(firstCallArg<{ url?: string }>(probeGateway).url).toBe("ws://127.0.0.1:18789"); + expect((firstCallArg(probeGateway) as { url?: string }).url).toBe("ws://127.0.0.1:18789"); }); it("treats a busy port as healthy when runtime status lags but the probe succeeds", async () => { @@ -433,17 +433,17 @@ describe("inspectGatewayRestart", () => { expect(snapshot.healthy).toBe(true); expect(snapshot.gatewayVersion).toBe("2026.4.24"); expect(snapshot.expectedVersion).toBe("2026.4.24"); - const authResolveInput = firstCallArg<{ + const authResolveInput = firstCallArg(resolveGatewayProbeAuthSafeWithSecretInputs) as { cfg?: { gateway?: { auth?: { mode?: string; token?: string } } }; mode?: string; - }>(resolveGatewayProbeAuthSafeWithSecretInputs); + }; expect(authResolveInput.cfg?.gateway?.auth?.mode).toBe("token"); expect(authResolveInput.cfg?.gateway?.auth?.token).toBe("probe-token"); expect(authResolveInput.mode).toBe("local"); - const probeInput = firstCallArg<{ + const probeInput = firstCallArg(probeGateway) as { auth?: { token?: string; password?: string }; env?: NodeJS.ProcessEnv; - }>(probeGateway); + }; expect(probeInput.auth?.token).toBe("probe-token"); expect(probeInput.auth?.password).toBeUndefined(); expect(probeInput.env).toBe(serviceEnv); @@ -526,7 +526,7 @@ describe("inspectGatewayRestart", () => { }, ]); expect(snapshot.versionMismatch).toBeUndefined(); - expect(firstCallArg<{ includeDetails?: boolean }>(probeGateway).includeDetails).toBe(true); + expect((firstCallArg(probeGateway) as { includeDetails?: boolean }).includeDetails).toBe(true); const { renderRestartDiagnostics } = await import("./restart-health.js"); expect(renderRestartDiagnostics(snapshot).join("\n")).toContain( diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 9396b65a060..43a233fb673 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -638,10 +638,10 @@ describe("applyAuthChoice", () => { function expectPromptMessage(mock: { mock: { calls: unknown[][] } }, expected: string) { expect(promptMessages(mock)).toContain(expected); } - function firstCallArg(mock: { mock: { calls: unknown[][] } }, _type?: (value: T) => T): T { + function firstCallArg(mock: { mock: { calls: unknown[][] } }): unknown { const call = mock.mock.calls[0]; expect(call).toBeDefined(); - return call?.[0] as T; + return call?.[0]; } let defaultProviderPlugins: ProviderPlugin[] = []; @@ -1160,9 +1160,10 @@ describe("applyAuthChoice", () => { setDefaultModel: false, }); - const providerResolveInput = firstCallArg<{ env?: NodeJS.ProcessEnv; mode?: string }>( - resolvePluginProviders, - ); + const providerResolveInput = firstCallArg(resolvePluginProviders) as { + env?: NodeJS.ProcessEnv; + mode?: string; + }; expect(providerResolveInput.env).toBe(env); expect(providerResolveInput.mode).toBe("setup"); expectPromptMessageContaining(confirm, "OPENAI_API_KEY"); diff --git a/src/flows/channel-setup.test.ts b/src/flows/channel-setup.test.ts index b5057412416..28e2e98e52b 100644 --- a/src/flows/channel-setup.test.ts +++ b/src/flows/channel-setup.test.ts @@ -108,10 +108,12 @@ const getChannelSetupPlugin = vi.hoisted(() => vi.fn((_channel?: unknown) => und const listChannelSetupPlugins = vi.hoisted(() => vi.fn((): unknown[] => [])); const listActiveChannelSetupPlugins = vi.hoisted(() => vi.fn((): unknown[] => [])); const loadChannelSetupPluginRegistrySnapshotForChannel = vi.hoisted(() => - vi.fn((_params) => makePluginRegistry()), + vi.fn((_params: Parameters[0]) => + makePluginRegistry(), + ), ); const ensureChannelSetupPluginInstalled = vi.hoisted(() => - vi.fn(async ({ cfg, entry }) => ({ + vi.fn(async ({ cfg, entry }: Parameters[0]) => ({ cfg, installed: true, pluginId: entry?.pluginId, @@ -119,16 +121,20 @@ const ensureChannelSetupPluginInstalled = vi.hoisted(() => })), ); const resolveChannelSetupEntries = vi.hoisted(() => - vi.fn((_params) => ({ - entries: [], - installedCatalogEntries: [], - installableCatalogEntries: [], - installedCatalogById: new Map(), - installableCatalogById: new Map(), - })), + vi.fn( + ( + _params: Parameters[0], + ): ReturnType => ({ + entries: [], + installedCatalogEntries: [], + installableCatalogEntries: [], + installedCatalogById: new Map(), + installableCatalogById: new Map(), + }), + ), ); const collectChannelStatus = vi.hoisted(() => - vi.fn(async (_params) => ({ + vi.fn(async (_params: Parameters[0]) => ({ installedPlugins: [], catalogEntries: [], installedCatalogEntries: [], diff --git a/src/infra/outbound/delivery-queue.recovery.test.ts b/src/infra/outbound/delivery-queue.recovery.test.ts index 45f59d7af81..9fd23f47f72 100644 --- a/src/infra/outbound/delivery-queue.recovery.test.ts +++ b/src/infra/outbound/delivery-queue.recovery.test.ts @@ -22,14 +22,10 @@ vi.mock("./channel-resolution.js", () => ({ resolveOutboundChannelMessageAdapter: resolveOutboundChannelMessageAdapterMock, })); -function mockCallArg( - mock: { mock: { calls: unknown[][] } }, - index = 0, - _type?: (value: T) => T, -): T { +function mockCallArg(mock: { mock: { calls: unknown[][] } }, index = 0): unknown { const call = mock.mock.calls[index]; expect(call).toBeDefined(); - return call[0] as T; + return call[0]; } function expectMockMessageContaining(mock: { mock: { calls: unknown[][] } }, expected: string) { @@ -204,9 +200,11 @@ describe("delivery-queue recovery", () => { cfg: baseCfg, allowBootstrap: true, }); - const deliverInput = mockCallArg<{ channel?: string; to?: string; skipQueue?: boolean }>( - deliver, - ); + const deliverInput = mockCallArg(deliver) as { + channel?: string; + to?: string; + skipQueue?: boolean; + }; expect(deliverInput.channel).toBe("demo-channel-a"); expect(deliverInput.to).toBe("+1"); expect(deliverInput.skipQueue).toBe(true); @@ -280,7 +278,7 @@ describe("delivery-queue recovery", () => { skippedMaxRetries: 0, deferredBackoff: 0, }); - const reconcileInput = mockCallArg<{ + const reconcileInput = mockCallArg(reconcileUnknownSend) as { cfg?: unknown; queueId?: string; channel?: string; @@ -291,7 +289,7 @@ describe("delivery-queue recovery", () => { threadId?: string; silent?: boolean; retryCount?: number; - }>(reconcileUnknownSend); + }; expect(reconcileInput.cfg).toBe(baseCfg); expect(reconcileInput.queueId).toBe(id); expect(reconcileInput.channel).toBe("demo-channel-a"); @@ -303,7 +301,7 @@ describe("delivery-queue recovery", () => { expect(reconcileInput.silent).toBe(true); expect(reconcileInput.retryCount).toBe(0); - const afterCommitInput = mockCallArg<{ + const afterCommitInput = mockCallArg(afterCommit) as { kind?: string; to?: string; accountId?: string; @@ -311,7 +309,7 @@ describe("delivery-queue recovery", () => { threadId?: string; silent?: boolean; result?: { messageId?: string }; - }>(afterCommit); + }; expect(afterCommitInput.kind).toBe("text"); expect(afterCommitInput.to).toBe("+1"); expect(afterCommitInput.accountId).toBe("acct-1"); @@ -400,9 +398,11 @@ describe("delivery-queue recovery", () => { const { result } = await runRecovery({ deliver }); expect(deliver).toHaveBeenCalledTimes(1); - const deliverInput = mockCallArg<{ channel?: string; to?: string; skipQueue?: boolean }>( - deliver, - ); + const deliverInput = mockCallArg(deliver) as { + channel?: string; + to?: string; + skipQueue?: boolean; + }; expect(deliverInput.channel).toBe("demo-channel-a"); expect(deliverInput.to).toBe("+1"); expect(deliverInput.skipQueue).toBe(true); @@ -527,11 +527,11 @@ describe("delivery-queue recovery", () => { const deliver = vi.fn().mockResolvedValue([]); await runRecovery({ deliver }); - const deliverInput = mockCallArg<{ + const deliverInput = mockCallArg(deliver) as { deliveryQueueId?: string; deliveryQueueStateDir?: string; skipQueue?: boolean; - }>(deliver); + }; expect(deliverInput.deliveryQueueId).toBe(id); expect(deliverInput.deliveryQueueStateDir).toBe(tmpDir()); expect(deliverInput.skipQueue).toBe(true); @@ -628,7 +628,7 @@ describe("delivery-queue recovery", () => { const deliver = vi.fn().mockResolvedValue([]); await runRecovery({ deliver }); - const deliverInput = mockCallArg<{ + const deliverInput = mockCallArg(deliver) as { bestEffort?: boolean; gifPlayback?: boolean; silent?: boolean; @@ -638,7 +638,7 @@ describe("delivery-queue recovery", () => { gatewayClientScopes?: string[]; mirror?: unknown; session?: unknown; - }>(deliver); + }; expect(deliverInput.bestEffort).toBe(true); expect(deliverInput.gifPlayback).toBe(true); expect(deliverInput.silent).toBe(true); @@ -747,9 +747,11 @@ describe("delivery-queue recovery", () => { deferredBackoff: 1, }); expect(deliver).toHaveBeenCalledTimes(1); - const deliverInput = mockCallArg<{ channel?: string; to?: string; skipQueue?: boolean }>( - deliver, - ); + const deliverInput = mockCallArg(deliver) as { + channel?: string; + to?: string; + skipQueue?: boolean; + }; expect(deliverInput.channel).toBe("demo-channel-b"); expect(deliverInput.to).toBe("2"); expect(deliverInput.skipQueue).toBe(true); diff --git a/test/cli-json-stdout.e2e.test.ts b/test/cli-json-stdout.e2e.test.ts index 912113d289c..4bdfd154694 100644 --- a/test/cli-json-stdout.e2e.test.ts +++ b/test/cli-json-stdout.e2e.test.ts @@ -37,7 +37,11 @@ describe("cli json stdout contract", () => { if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error(`Expected JSON object stdout, got: ${stdout}`); } - expect(Object.keys(parsed).sort()).toEqual(["availability", "channel", "update"]); + expect(Object.keys(parsed).toSorted((a, b) => a.localeCompare(b))).toEqual([ + "availability", + "channel", + "update", + ]); expect(stdout).not.toContain("Doctor warnings"); expect(stdout).not.toContain("Doctor changes"); expect(stdout).not.toContain("Config invalid"); diff --git a/test/package-manager-config.test.ts b/test/package-manager-config.test.ts index d632e1e8eae..d57227d44fb 100644 --- a/test/package-manager-config.test.ts +++ b/test/package-manager-config.test.ts @@ -13,13 +13,13 @@ type RootPackageJson = { type WorkspaceConfig = PnpmBuildConfig; -function readJson(filePath: string): T { - return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; +function readJson(filePath: string): unknown { + return JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown; } describe("package manager build policy", () => { it("keeps optional native Discord opus builds disabled by default", () => { - const packageJson = readJson("package.json"); + const packageJson = readJson("package.json") as RootPackageJson; const workspace = parse(fs.readFileSync("pnpm-workspace.yaml", "utf8")) as WorkspaceConfig; for (const config of [packageJson.pnpm, workspace]) { diff --git a/test/plugin-clawhub-release.test.ts b/test/plugin-clawhub-release.test.ts index 5cfc73a450b..00b07bca92b 100644 --- a/test/plugin-clawhub-release.test.ts +++ b/test/plugin-clawhub-release.test.ts @@ -379,7 +379,7 @@ describe("collectClawHubOpenClawOwnerErrors", () => { ], registryBaseUrl: "https://clawhub.ai", fetchImpl: async (url) => { - const pathname = new URL(String(url)).pathname; + const pathname = new URL(url instanceof Request ? url.url : url).pathname; if (pathname.includes("%40openclaw%2Fmissing-plugin")) { return new Response("not found", { status: 404 }); } diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index 31bc79c8d20..3edfd0cf2ea 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -816,8 +816,8 @@ describe("scripts/lib/docker-e2e-plan", () => { expect(plan.lanes).toHaveLength(BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS); expect(plan.lanes.at(0)).toBeDefined(); expect(plan.lanes.at(23)).toBeDefined(); - expect(summarizeLane(plan.lanes[0]!)).toEqual(bundledPluginSweepLane(0)); - expect(summarizeLane(plan.lanes[23]!)).toEqual(bundledPluginSweepLane(23)); + expect(summarizeLane(plan.lanes[0])).toEqual(bundledPluginSweepLane(0)); + expect(summarizeLane(plan.lanes[23])).toEqual(bundledPluginSweepLane(23)); expect(plan.needs).toEqual({ bareImage: false, e2eImage: true, diff --git a/test/scripts/run-additional-boundary-checks.test.ts b/test/scripts/run-additional-boundary-checks.test.ts index 1a5e9f099a7..18c7b79e277 100644 --- a/test/scripts/run-additional-boundary-checks.test.ts +++ b/test/scripts/run-additional-boundary-checks.test.ts @@ -50,8 +50,8 @@ describe("run-additional-boundary-checks", () => { const shardedLabels = [1, 2, 3, 4].flatMap((index) => selectChecksForShard(BOUNDARY_CHECKS, `${index}/4`).map((check) => check.label), ); - expect(shardedLabels.toSorted()).toEqual( - BOUNDARY_CHECKS.map((check) => check.label).toSorted(), + expect(shardedLabels.toSorted((a, b) => a.localeCompare(b))).toEqual( + BOUNDARY_CHECKS.map((check) => check.label).toSorted((a, b) => a.localeCompare(b)), ); expect(new Set(shardedLabels).size).toBe(BOUNDARY_CHECKS.length); expect(() => parseShardSpec("5/4")).toThrow("Invalid shard spec"); diff --git a/test/scripts/test-live-shard.test.ts b/test/scripts/test-live-shard.test.ts index 2e79165ded8..598f23461d5 100644 --- a/test/scripts/test-live-shard.test.ts +++ b/test/scripts/test-live-shard.test.ts @@ -24,7 +24,7 @@ describe("scripts/test-live-shard", () => { .toSorted(); expect(allFiles.length).toBeGreaterThan(0); - expect([...new Set(selectedFiles)].toSorted()).toEqual(allFiles); + expect([...new Set(selectedFiles)].toSorted((a, b) => a.localeCompare(b))).toEqual(allFiles); expect(duplicateFiles).toEqual(["extensions/music-generation-providers.live.test.ts"]); expect(musicProviderFanout).toEqual([ "native-live-extensions-media-music-google", @@ -45,7 +45,7 @@ describe("scripts/test-live-shard", () => { [ ...selectLiveShardFiles("native-live-extensions-o-z-other", allFiles), ...selectLiveShardFiles("native-live-extensions-xai", allFiles), - ].toSorted(), + ].toSorted((a, b) => a.localeCompare(b)), ); const mediaAlias = selectLiveShardFiles("native-live-extensions-media", allFiles); @@ -54,7 +54,7 @@ describe("scripts/test-live-shard", () => { ...selectLiveShardFiles("native-live-extensions-media-audio", allFiles), ...selectLiveShardFiles("native-live-extensions-media-music", allFiles), ...selectLiveShardFiles("native-live-extensions-media-video", allFiles), - ].toSorted(), + ].toSorted((a, b) => a.localeCompare(b)), ); }); diff --git a/ui/src/i18n/.i18n/raw-copy-baseline.json b/ui/src/i18n/.i18n/raw-copy-baseline.json index 850ef976dfb..05f0ed2f982 100644 --- a/ui/src/i18n/.i18n/raw-copy-baseline.json +++ b/ui/src/i18n/.i18n/raw-copy-baseline.json @@ -232,6 +232,13 @@ "path": "ui/src/ui/chat/grouped-render.ts", "text": "Voice note" }, + { + "count": 1, + "kind": "object-property", + "name": "label", + "path": "ui/src/ui/chat/grouped-render.ts", + "text": "Tool output" + }, { "count": 1, "kind": "object-property",