From fb725383b962dccf3464b07c1dd47d5fadd8abc1 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 9 Oct 2026 04:09:41 -0500 Subject: [PATCH] fix(ui): preserve exact retry feedback across agent aliases --- .../issue-update-comment-wakeup-routes.test.ts | 15 +++++++++------ tests/e2e/chat-adapters-ui-messaging.spec.ts | 18 ++++++++++++++++++ ui/src/pages/AgentDetail.tsx | 16 +++++++++++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts b/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts index b4e9d4a169..4b90c0db0a 100644 --- a/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts +++ b/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts @@ -201,12 +201,11 @@ function registerModuleMocks() { })); } +let errorHandler: typeof import("../middleware/index.js").errorHandler; +let issueRoutes: typeof import("../routes/issues.js").issueRoutes; + async function createApp(transaction: (callback: (tx: Record) => Promise) => Promise = async (callback) => callback({})) { - const [{ errorHandler }, { issueRoutes }] = await Promise.all([ - vi.importActual("../middleware/index.js"), - vi.importActual("../routes/issues.js"), - ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -249,7 +248,7 @@ function makeIssue(overrides: Record = {}) { } describe("issue update comment wakeups", () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules(); vi.doUnmock("../routes/issues.js"); vi.doUnmock("../routes/authz.js"); @@ -267,7 +266,11 @@ describe("issue update comment wakeups", () => { mockIssueService.getWakeableParentAfterChildCompletion.mockResolvedValue(null); mockIssueService.getCurrentScheduledRetry.mockResolvedValue(null); mockIssueService.listReviewAttention.mockResolvedValue(new Map()); - }); + [{ errorHandler }, { issueRoutes }] = await Promise.all([ + vi.importActual("../middleware/index.js"), + vi.importActual("../routes/issues.js"), + ]); + }, 30_000); it.each(["post", "patch"] as const)("rejects %s board messages under an inherited pause before any mutation", async (method) => { const existing = makeIssue(); diff --git a/tests/e2e/chat-adapters-ui-messaging.spec.ts b/tests/e2e/chat-adapters-ui-messaging.spec.ts index de11726d12..a542eff646 100644 --- a/tests/e2e/chat-adapters-ui-messaging.spec.ts +++ b/tests/e2e/chat-adapters-ui-messaging.spec.ts @@ -1355,12 +1355,26 @@ test.describe("Exact failed chat run retry", () => { body: Record; }> = []; const destinations: string[] = []; + let releaseCanonicalRead = () => {}; + const canonicalRead = new Promise((resolve) => { + releaseCanonicalRead = resolve; + }); + page.once("close", releaseCanonicalRead); page.on("framenavigated", (frame) => { if (frame === page.mainFrame()) destinations.push(frame.url()); }); await page.route("**/api/**", async (route) => { const url = new URL(route.request().url()); const pathname = url.pathname; + if (surface === "agent run" && outcome === "denied" + && pathname === "/api/agents/maya" && route.request().method() === "GET") { + // Canonical URL refresh must preserve the selected run and its + // retry feedback while the alias lookup is still pending. + const response = await route.fetch(); + await canonicalRead; + if (!page.isClosed()) await route.fulfill({ response }); + return; + } if (pathname === "/api/instance/settings/experimental") { await fulfill(route, { enableChatConnectors: true, @@ -1455,6 +1469,10 @@ test.describe("Exact failed chat run retry", () => { }); if (outcome === "denied") { await expect(page.getByText(denial, { exact: true })).toBeVisible(); + if (surface === "agent run") { + await page.screenshot({ path: testInfo.outputPath("exact-retry-denied-during-canonical-refresh.png") }); + } + releaseCanonicalRead(); if (surface !== "agent run") { await expect( page.getByText("Run retry failed", { exact: true }), diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index ffc57a489a..e2f69b200e 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -964,6 +964,20 @@ export function AgentDetail() { useEffect(() => { if (!agent) return; + if (routeAgentRef !== canonicalAgentRef) { + // Reuse the authorized response across the alias redirect. An empty + // alias query would unmount the selected run and discard its mutations. + queryClient.setQueryData( + [...queryKeys.agents.detail(canonicalAgentRef), lookupCompanyId ?? null], + agent, + ); + if (lookupCompanyId !== agent.companyId) { + queryClient.setQueryData( + [...queryKeys.agents.detail(canonicalAgentRef), agent.companyId], + agent, + ); + } + } if (urlRunId) { if (routeAgentRef !== canonicalAgentRef) { navigate(`/agents/${canonicalAgentRef}/runs/${urlRunId}`, { replace: true }); @@ -976,7 +990,7 @@ export function AgentDetail() { navigate(agentDetailHref(canonicalAgentRef, canonicalTab), { replace: true }); return; } - }, [agent, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, activeView, legacyAuditSection, navigate]); + }, [agent, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, activeView, legacyAuditSection, navigate, lookupCompanyId, queryClient]); useEffect(() => { if (!agent?.companyId || agent.companyId === selectedCompanyId) return;