diff --git a/extensions/codex/src/commands.test.ts b/extensions/codex/src/commands.test.ts index 0b58eb8b873..aa3ec1e44f5 100644 --- a/extensions/codex/src/commands.test.ts +++ b/extensions/codex/src/commands.test.ts @@ -83,6 +83,28 @@ function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } +function requireResultText(result: PluginCommandResult): string { + if (typeof result.text !== "string") { + throw new Error("expected command result text"); + } + return result.text; +} + +function expectResultTextContains(result: PluginCommandResult, expected: string): void { + expect(requireResultText(result)).toContain(expected); +} + +function requireRecord(value: unknown, message: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(message); + } + return value as Record; +} + +function requireRequestParams(call: unknown[] | undefined): Record { + return requireRecord(call?.[2], "expected request params object"); +} + function expectedDiagnosticsTargetBlock(params: { index?: number; channel?: string; @@ -190,17 +212,18 @@ describe("codex command", () => { it("shows model ids from Codex app-server", async () => { const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } }; + const listCodexAppServerModels = vi.fn(async (_options?: { config?: unknown }) => ({ + models: [ + { + id: "gpt-5.4", + model: "gpt-5.4", + inputModalities: ["text"], + supportedReasoningEfforts: ["medium"], + }, + ], + })); const deps = createDeps({ - listCodexAppServerModels: vi.fn(async () => ({ - models: [ - { - id: "gpt-5.4", - model: "gpt-5.4", - inputModalities: ["text"], - supportedReasoningEfforts: ["medium"], - }, - ], - })), + listCodexAppServerModels, }); await expect( @@ -209,7 +232,8 @@ describe("codex command", () => { text: "Codex models:\n- gpt-5.4", }); expect(deps.requestOptions).toHaveBeenCalledWith(undefined, 100, config); - expect(deps.listCodexAppServerModels).toHaveBeenCalledWith(expect.objectContaining({ config })); + const modelsRequest = listCodexAppServerModels.mock.calls[0]?.[0]; + expect(modelsRequest?.config).toBe(config); }); it("shows when Codex app-server model output is truncated", async () => { @@ -428,12 +452,10 @@ describe("codex command", () => { .mockResolvedValueOnce(limits), }); - await expect(handleCodexCommand(createContext("status"), { deps })).resolves.toMatchObject({ - text: expect.stringContaining("Rate limits: Codex: primary 42%"), - }); - await expect(handleCodexCommand(createContext("account"), { deps })).resolves.toMatchObject({ - text: expect.stringContaining("Rate limits: Codex: primary 42%"), - }); + const statusResult = await handleCodexCommand(createContext("status"), { deps }); + expectResultTextContains(statusResult, "Rate limits: Codex: primary 42%"); + const accountResult = await handleCodexCommand(createContext("account"), { deps }); + expectResultTextContains(accountResult, "Rate limits: Codex: primary 42%"); }); it("rejects extra operands for read-only Codex commands", async () => { @@ -515,9 +537,13 @@ describe("codex command", () => { expect(result.text).toContain("Account: codex@example.com"); expect(result.text).toContain("Rate limits: Codex: primary 50%, resets in"); - expect(readRecentCodexRateLimits()).toMatchObject({ - rateLimits: { limitId: "codex" }, - }); + const cachedLimits = requireRecord( + readRecentCodexRateLimits(), + "expected cached Codex rate limits", + ); + expect(requireRecord(cachedLimits.rateLimits, "expected rate limits object").limitId).toBe( + "codex", + ); expect(safeCodexControlRequest).toHaveBeenCalledWith(undefined, CODEX_CONTROL_METHODS.account, { refreshToken: false, }); @@ -721,30 +747,26 @@ describe("codex command", () => { "Computer Use is installed, but the computer-use plugin is disabled. Run /codex computer-use install or enable computerUse.autoInstall to re-enable it.", })); - await expect( - handleCodexCommand(createContext("computer-use status"), { - deps: createDeps({ readCodexComputerUseStatus }), - }), - ).resolves.toEqual({ - text: expect.stringContaining("Plugin: computer-use (installed, disabled)"), + const result = await handleCodexCommand(createContext("computer-use status"), { + deps: createDeps({ readCodexComputerUseStatus }), }); + + expectResultTextContains(result, "Plugin: computer-use (installed, disabled)"); }); it("installs Codex Computer Use from command overrides", async () => { const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus()); - await expect( - handleCodexCommand( - createContext( - "computer-use install --source github:example/desktop-tools --marketplace desktop-tools", - ), - { - deps: createDeps({ installCodexComputerUse }), - }, + const result = await handleCodexCommand( + createContext( + "computer-use install --source github:example/desktop-tools --marketplace desktop-tools", ), - ).resolves.toEqual({ - text: expect.stringContaining("Computer Use: ready"), - }); + { + deps: createDeps({ installCodexComputerUse }), + }, + ); + + expectResultTextContains(result, "Computer Use: ready"); expect(installCodexComputerUse).toHaveBeenCalledWith({ pluginConfig: undefined, forceEnable: true, @@ -758,13 +780,11 @@ describe("codex command", () => { it("shows help when Computer Use option values are missing", async () => { const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus()); - await expect( - handleCodexCommand(createContext("computer-use install --source"), { - deps: createDeps({ installCodexComputerUse }), - }), - ).resolves.toEqual({ - text: expect.stringContaining("Usage: /codex computer-use"), + const result = await handleCodexCommand(createContext("computer-use install --source"), { + deps: createDeps({ installCodexComputerUse }), }); + + expectResultTextContains(result, "Usage: /codex computer-use"); expect(installCodexComputerUse).not.toHaveBeenCalled(); }); @@ -772,13 +792,11 @@ describe("codex command", () => { const readCodexComputerUseStatus = vi.fn(async () => computerUseReadyStatus()); const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus()); - await expect( - handleCodexCommand(createContext("computer-use status install"), { - deps: createDeps({ readCodexComputerUseStatus, installCodexComputerUse }), - }), - ).resolves.toEqual({ - text: expect.stringContaining("Usage: /codex computer-use"), + const result = await handleCodexCommand(createContext("computer-use status install"), { + deps: createDeps({ readCodexComputerUseStatus, installCodexComputerUse }), }); + + expectResultTextContains(result, "Usage: /codex computer-use"); expect(readCodexComputerUseStatus).not.toHaveBeenCalled(); expect(installCodexComputerUse).not.toHaveBeenCalled(); }); @@ -833,7 +851,7 @@ describe("codex command", () => { "This request expires in 5 minutes.", ].join("\n"), ); - expect(request.interactive).toMatchObject({ + expect(request.interactive).toEqual({ blocks: [ { type: "buttons", @@ -843,7 +861,11 @@ describe("codex command", () => { value: `/codex diagnostics confirm ${token}`, style: "danger", }, - { label: "Cancel", value: `/codex diagnostics cancel ${token}` }, + { + label: "Cancel", + value: `/codex diagnostics cancel ${token}`, + style: "secondary", + }, ], }, ], @@ -926,11 +948,11 @@ describe("codex command", () => { }); expect(safeCodexControlRequest).not.toHaveBeenCalled(); - await expect( - handleCodexCommand(createContext(`diagnostics confirm ${token}`, sessionFile), { deps }), - ).resolves.toMatchObject({ - text: expect.stringContaining("Codex diagnostics sent to OpenAI servers:"), - }); + const confirmResult = await handleCodexCommand( + createContext(`diagnostics confirm ${token}`, sessionFile), + { deps }, + ); + expectResultTextContains(confirmResult, "Codex diagnostics sent to OpenAI servers:"); expect(safeCodexControlRequest).toHaveBeenCalledTimes(1); }); @@ -1112,18 +1134,16 @@ describe("codex command", () => { ].join("\n"), }); expect(safeCodexControlRequest).toHaveBeenCalledTimes(2); - expect(safeCodexControlRequest).toHaveBeenNthCalledWith( - 1, - undefined, - CODEX_CONTROL_METHODS.feedback, - expect.objectContaining({ threadId: "thread-111", includeLogs: true }), - ); - expect(safeCodexControlRequest).toHaveBeenNthCalledWith( - 2, - undefined, - CODEX_CONTROL_METHODS.feedback, - expect.objectContaining({ threadId: "thread-222", includeLogs: true }), - ); + expect(safeCodexControlRequest.mock.calls[0]?.[0]).toBeUndefined(); + expect(safeCodexControlRequest.mock.calls[0]?.[1]).toBe(CODEX_CONTROL_METHODS.feedback); + const firstFeedbackParams = requireRequestParams(safeCodexControlRequest.mock.calls[0]); + expect(firstFeedbackParams.threadId).toBe("thread-111"); + expect(firstFeedbackParams.includeLogs).toBe(true); + expect(safeCodexControlRequest.mock.calls[1]?.[0]).toBeUndefined(); + expect(safeCodexControlRequest.mock.calls[1]?.[1]).toBe(CODEX_CONTROL_METHODS.feedback); + const secondFeedbackParams = requireRequestParams(safeCodexControlRequest.mock.calls[1]); + expect(secondFeedbackParams.threadId).toBe("thread-222"); + expect(secondFeedbackParams.includeLogs).toBe(true); }); it("requires an owner for Codex diagnostics feedback uploads", async () => { @@ -1251,9 +1271,8 @@ describe("codex command", () => { }); releaseFirstConfirmBindingRead(); - await expect(firstConfirm).resolves.toMatchObject({ - text: expect.stringContaining("Codex diagnostics sent to OpenAI servers:"), - }); + const firstConfirmResult = await firstConfirm; + expectResultTextContains(firstConfirmResult, "Codex diagnostics sent to OpenAI servers:"); expect(safeCodexControlRequest).toHaveBeenCalledTimes(1); }); @@ -1304,10 +1323,12 @@ describe("codex command", () => { `${sessionFile}.codex-app-server.json`, JSON.stringify({ schemaVersion: 1, threadId: "thread-private", cwd: "/repo" }), ); - const safeCodexControlRequest = vi.fn(async () => ({ - ok: true as const, - value: { threadId: "thread-private" }, - })); + const safeCodexControlRequest = vi.fn( + async (_pluginConfig: unknown, _method: string, _requestParams: unknown) => ({ + ok: true as const, + value: { threadId: "thread-private" }, + }), + ); const deps = createDeps({ safeCodexControlRequest }); const request = await handleCodexCommand( @@ -1342,15 +1363,12 @@ describe("codex command", () => { "Included Codex logs and spawned Codex subthreads when available.", ].join("\n"), }); - expect(safeCodexControlRequest).toHaveBeenCalledWith( - undefined, - CODEX_CONTROL_METHODS.feedback, - expect.objectContaining({ - classification: "bug", - threadId: "thread-private", - includeLogs: true, - }), - ); + expect(safeCodexControlRequest.mock.calls[0]?.[0]).toBeUndefined(); + expect(safeCodexControlRequest.mock.calls[0]?.[1]).toBe(CODEX_CONTROL_METHODS.feedback); + const feedbackParams = requireRequestParams(safeCodexControlRequest.mock.calls[0]); + expect(feedbackParams.classification).toBe("bug"); + expect(feedbackParams.threadId).toBe("thread-private"); + expect(feedbackParams.includeLogs).toBe(true); }); it("keeps diagnostics confirmation eviction scoped to account identity", async () => { @@ -1405,10 +1423,12 @@ describe("codex command", () => { `${sessionFile}.codex-app-server.json`, JSON.stringify({ schemaVersion: 1, threadId: "thread-789", cwd: "/repo" }), ); - const safeCodexControlRequest = vi.fn(async () => ({ - ok: true as const, - value: { threadId: "thread-789" }, - })); + const safeCodexControlRequest = vi.fn( + async (_pluginConfig: unknown, _method: string, _requestParams: unknown) => ({ + ok: true as const, + value: { threadId: "thread-789" }, + }), + ); const note = "x".repeat(2050); const deps = createDeps({ safeCodexControlRequest }); @@ -1418,13 +1438,10 @@ describe("codex command", () => { const token = readDiagnosticsConfirmationToken(request); await handleCodexCommand(createContext(`diagnostics confirm ${token}`, sessionFile), { deps }); - expect(safeCodexControlRequest).toHaveBeenCalledWith( - undefined, - CODEX_CONTROL_METHODS.feedback, - expect.objectContaining({ - reason: "x".repeat(2048), - }), - ); + expect(safeCodexControlRequest.mock.calls[0]?.[0]).toBeUndefined(); + expect(safeCodexControlRequest.mock.calls[0]?.[1]).toBe(CODEX_CONTROL_METHODS.feedback); + const feedbackParams = requireRequestParams(safeCodexControlRequest.mock.calls[0]); + expect(feedbackParams.reason).toBe("x".repeat(2048)); }); it("escapes diagnostics notes before showing approval text", async () => { @@ -1544,17 +1561,14 @@ describe("codex command", () => { { deps }, ); const firstToken = readDiagnosticsConfirmationToken(firstRequest); - await expect( - handleCodexCommand( - createContext(`diagnostics confirm ${firstToken}`, sessionFile, { - accountId: "account-1", - channelId: "channel-1", - }), - { deps }, - ), - ).resolves.toMatchObject({ - text: expect.stringContaining("Codex diagnostics sent to OpenAI servers:"), - }); + const firstConfirmResult = await handleCodexCommand( + createContext(`diagnostics confirm ${firstToken}`, sessionFile, { + accountId: "account-1", + channelId: "channel-1", + }), + { deps }, + ); + expectResultTextContains(firstConfirmResult, "Codex diagnostics sent to OpenAI servers:"); await fs.writeFile( `${sessionFile}.codex-app-server.json`, @@ -1568,17 +1582,14 @@ describe("codex command", () => { { deps }, ); const secondToken = readDiagnosticsConfirmationToken(secondRequest); - await expect( - handleCodexCommand( - createContext(`diagnostics confirm ${secondToken}`, sessionFile, { - accountId: "account-2", - channelId: "channel-2", - }), - { deps }, - ), - ).resolves.toMatchObject({ - text: expect.stringContaining("Codex diagnostics sent to OpenAI servers:"), - }); + const secondConfirmResult = await handleCodexCommand( + createContext(`diagnostics confirm ${secondToken}`, sessionFile, { + accountId: "account-2", + channelId: "channel-2", + }), + { deps }, + ); + expectResultTextContains(secondConfirmResult, "Codex diagnostics sent to OpenAI servers:"); expect(safeCodexControlRequest).toHaveBeenCalledTimes(2); }); @@ -1605,14 +1616,11 @@ describe("codex command", () => { { deps }, ); const firstToken = readDiagnosticsConfirmationToken(firstRequest); - await expect( - handleCodexCommand( - createContext(`diagnostics confirm ${firstToken}`, sessionFile, firstScope), - { deps }, - ), - ).resolves.toMatchObject({ - text: expect.stringContaining("Codex diagnostics sent to OpenAI servers:"), - }); + const firstConfirmResult = await handleCodexCommand( + createContext(`diagnostics confirm ${firstToken}`, sessionFile, firstScope), + { deps }, + ); + expectResultTextContains(firstConfirmResult, "Codex diagnostics sent to OpenAI servers:"); await fs.writeFile( `${sessionFile}.codex-app-server.json`, @@ -1627,14 +1635,11 @@ describe("codex command", () => { { deps }, ); const secondToken = readDiagnosticsConfirmationToken(secondRequest); - await expect( - handleCodexCommand( - createContext(`diagnostics confirm ${secondToken}`, sessionFile, secondScope), - { deps }, - ), - ).resolves.toMatchObject({ - text: expect.stringContaining("Codex diagnostics sent to OpenAI servers:"), - }); + const secondConfirmResult = await handleCodexCommand( + createContext(`diagnostics confirm ${secondToken}`, sessionFile, secondScope), + { deps }, + ); + expectResultTextContains(secondConfirmResult, "Codex diagnostics sent to OpenAI servers:"); expect(safeCodexControlRequest).toHaveBeenCalledTimes(2); }); @@ -1661,14 +1666,11 @@ describe("codex command", () => { { deps }, ); const firstToken = readDiagnosticsConfirmationToken(firstRequest); - await expect( - handleCodexCommand( - createContext(`diagnostics confirm ${firstToken}`, sessionFile, firstScope), - { deps }, - ), - ).resolves.toMatchObject({ - text: expect.stringContaining("Codex diagnostics sent to OpenAI servers:"), - }); + const firstConfirmResult = await handleCodexCommand( + createContext(`diagnostics confirm ${firstToken}`, sessionFile, firstScope), + { deps }, + ); + expectResultTextContains(firstConfirmResult, "Codex diagnostics sent to OpenAI servers:"); await fs.writeFile( `${sessionFile}.codex-app-server.json`, @@ -1683,14 +1685,11 @@ describe("codex command", () => { { deps }, ); const secondToken = readDiagnosticsConfirmationToken(secondRequest); - await expect( - handleCodexCommand( - createContext(`diagnostics confirm ${secondToken}`, sessionFile, secondScope), - { deps }, - ), - ).resolves.toMatchObject({ - text: expect.stringContaining("Codex diagnostics sent to OpenAI servers:"), - }); + const secondConfirmResult = await handleCodexCommand( + createContext(`diagnostics confirm ${secondToken}`, sessionFile, secondScope), + { deps }, + ); + expectResultTextContains(secondConfirmResult, "Codex diagnostics sent to OpenAI servers:"); expect(safeCodexControlRequest).toHaveBeenCalledTimes(2); }); @@ -1937,7 +1936,7 @@ describe("codex command", () => { sessionFile, workspaceDir: "/repo", })); - const requestConversationBinding = vi.fn(async () => ({ + const requestConversationBinding = vi.fn(async (_request?: { summary?: string }) => ({ status: "bound" as const, binding: { bindingId: "binding-1", @@ -1999,7 +1998,7 @@ describe("codex command", () => { sessionFile, workspaceDir: "/repo with space", })); - const requestConversationBinding = vi.fn(async () => ({ + const requestConversationBinding = vi.fn(async (_request?: { summary?: string }) => ({ status: "bound" as const, binding: { bindingId: "binding-1", @@ -2048,7 +2047,7 @@ describe("codex command", () => { sessionFile, workspaceDir: unsafeWorkspace, })); - const requestConversationBinding = vi.fn(async () => ({ + const requestConversationBinding = vi.fn(async (_request?: { summary?: string }) => ({ status: "bound" as const, binding: { bindingId: "binding-1", @@ -2077,11 +2076,9 @@ describe("codex command", () => { expect(result.text).toContain("/repo \uff3btrusted\uff3d\uff08https://evil\uff09"); expect(result.text).not.toContain("<@U123>"); expect(result.text).not.toContain("[trusted](https://evil)"); - expect(requestConversationBinding).toHaveBeenCalledWith( - expect.objectContaining({ - summary: - "Codex app-server thread thread-123 <\uff20U123> in /repo \uff3btrusted\uff3d\uff08https://evil\uff09", - }), + const bindingRequest = requestConversationBinding.mock.calls[0]?.[0]; + expect(bindingRequest?.summary).toBe( + "Codex app-server thread thread-123 <\uff20U123> in /repo \uff3btrusted\uff3d\uff08https://evil\uff09", ); });