diff --git a/server/src/__tests__/chat-channels.integration.test.ts b/server/src/__tests__/chat-channels.integration.test.ts index 42c82be9669..c66432acfb1 100644 --- a/server/src/__tests__/chat-channels.integration.test.ts +++ b/server/src/__tests__/chat-channels.integration.test.ts @@ -4217,6 +4217,16 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(2); expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1); }); + it("retains reinstall intent after a provider failure instead of retrying rejected credentials", async () => { + const f = await managedFixture(); const scopes = f.state.grantedScopes; + f.state.grantedScopes = []; await f.provision(); + f.state.managedError = "internal_error"; await f.provision(); + expect((await f.service.get(f.endpoint.id)).setup.slackRegistration).toMatchObject({ status: "install", errorCode: "slack_provider_failure" }); + f.state.managedError = null; f.state.grantedScopes = scopes; await f.provision(); + expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured"); + expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(3); + expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1); + }); it("clears a manager reconnect error after renewing the bound grant", async () => { const f = await managedFixture(); f.state.managedError = "invalid_auth"; await f.provision(); expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBe("slack_manager_reauthorize"); diff --git a/server/src/services/connectors/slack/setup/managed.ts b/server/src/services/connectors/slack/setup/managed.ts index 44e4b67aeb4..6656d43967c 100644 --- a/server/src/services/connectors/slack/setup/managed.ts +++ b/server/src/services/connectors/slack/setup/managed.ts @@ -27,6 +27,8 @@ export function createManagedSlackSetup(store: SlackRegistrationStore, grants: R await completion.resumeLocked(endpointId, actor, lease); return; } + // Keep reinstall intent durable even if this dispatch fails or the process exits. + if (needsReinstall) await store.setFailure(row, "install", row.errorCode!, lease); const { grant } = await grants.token(input.grantId, row.companyId, actor); try { await lease.assertOwned(); @@ -41,7 +43,7 @@ export function createManagedSlackSetup(store: SlackRegistrationStore, grants: R } catch (error) { const code = object(object(error).details).code; const safe = typeof code === "string" ? code : "slack_install_failed"; - await store.setFailure(row, row.status, safe, lease); + await store.setFailure(row, needsReinstall ? "install" : row.status, safe, lease); await store.audit(row, actor, "installation_failed", safe); fallback = ["slack_approval_required", "slack_approval_pending", "slack_approval_denied"].includes(safe); return;