diff --git a/src/agents/cli-runner.before-agent-reply-cron.test.ts b/src/agents/cli-runner.before-agent-reply-cron.test.ts index 7c571c3fbbb2..7b638e79e01f 100644 --- a/src/agents/cli-runner.before-agent-reply-cron.test.ts +++ b/src/agents/cli-runner.before-agent-reply-cron.test.ts @@ -34,6 +34,7 @@ type BeforeAgentReplyResult = const { hasHooksMock, runBeforeAgentReplyMock, + runBeforeAgentRunMock, executePreparedCliRunMock, prepareCliRunContextMock, closeClaudeLiveSessionForContextMock, @@ -48,6 +49,7 @@ const { runBeforeAgentReplyMock: vi.fn<(event: unknown, ctx: unknown) => Promise>( async () => undefined, ), + runBeforeAgentRunMock: vi.fn(async () => undefined), executePreparedCliRunMock: vi.fn< (_context: unknown, _cliSessionIdToUse?: string) => Promise >(async () => ({ text: "" })), @@ -65,6 +67,7 @@ vi.mock("../plugins/hook-runner-global.js", () => ({ getGlobalHookRunner: vi.fn(() => ({ hasHooks: hasHooksMock, runBeforeAgentReply: runBeforeAgentReplyMock, + runBeforeAgentRun: runBeforeAgentRunMock, })), })); @@ -155,6 +158,8 @@ beforeEach(() => { hasHooksMock.mockReturnValue(false); runBeforeAgentReplyMock.mockReset(); runBeforeAgentReplyMock.mockResolvedValue(undefined); + runBeforeAgentRunMock.mockReset(); + runBeforeAgentRunMock.mockResolvedValue(undefined); executePreparedCliRunMock.mockReset(); executePreparedCliRunMock.mockResolvedValue({ text: "" }); prepareCliRunContextMock.mockReset(); @@ -327,6 +332,36 @@ describe("runCliAgent before_agent_reply seam", () => { }); }); + it("does not settle auth health when before_agent_run blocks before backend execution", async () => { + const profileId = "codex-cli:selected"; + const store = { + version: 1, + profiles: { [profileId]: { type: "oauth", provider: "codex-cli" } }, + }; + const recorder = { + persistBlocked: vi.fn(async (message) => ({ message })), + } as unknown as NonNullable[0]["userTurnTranscriptRecorder"]>; + prepareCliRunContextMock.mockImplementationOnce(async (params) => ({ + ...(makeStubContext(params as typeof baseRunParams & { trigger?: string }) as object), + effectiveAuthProfileId: profileId, + authProfileStore: store, + agentDir: "/tmp/agent", + })); + hasHooksMock.mockImplementation((hookName) => hookName === "before_agent_run"); + runBeforeAgentRunMock.mockResolvedValueOnce({ + pluginId: "policy-plugin", + decision: { outcome: "block", message: "Blocked by policy." }, + }); + + await expect( + runCliAgent({ ...baseRunParams, userTurnTranscriptRecorder: recorder }), + ).resolves.toMatchObject({ meta: { livenessState: "blocked" } }); + + expect(executePreparedCliRunMock).not.toHaveBeenCalled(); + expect(markAuthProfileFailureMock).not.toHaveBeenCalled(); + expect(markAuthProfileSuccessMock).not.toHaveBeenCalled(); + }); + it.each([ new FailoverError("bad transcript", { reason: "format" }), new FailoverError("context full", { reason: "context_overflow" }), diff --git a/src/agents/cli-runner.ts b/src/agents/cli-runner.ts index 4cd732c7bb1f..a3623bb5d5f4 100644 --- a/src/agents/cli-runner.ts +++ b/src/agents/cli-runner.ts @@ -720,7 +720,7 @@ async function runCliAgentInternal( modelId: context.modelId, }, }); - } else if (result && result.meta.stopReason !== "error") { + } else if (result?.meta.executionTrace?.attempts?.at(-1)?.result === "success") { const provider = authProfileStore.profiles[profileId]?.provider ?? params.provider; await settleCliAuthProfile({ store: authProfileStore,