From fa9afcc46673c1e280266f3c38d952c3ecdb0f2a Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 2 Nov 2026 11:00:14 -0600 Subject: [PATCH] fix: preserve recovery and optimistic task status transitions Co-Authored-By: Paperclip --- server/src/__tests__/issue-display-status.test.ts | 7 ++++++- .../src/__tests__/issue-summaries-routes.test.ts | 2 +- server/src/services/issue-display-status.ts | 2 +- server/src/services/status-cards.ts | 2 ++ ui/src/api/search.test.ts | 14 ++++++++++++++ ui/src/components/IssuesList.tsx | 4 ++-- ui/src/components/KanbanBoard.test.tsx | 10 ++++++++++ ui/src/components/LegacyIssuesList.tsx | 4 ++-- ui/src/lib/optimistic-issue-comments.test.ts | 6 ++++++ ui/src/lib/optimistic-issue-comments.ts | 2 ++ ui/src/lib/search-query-parser.test.ts | 2 +- ui/src/lib/search-query-parser.ts | 2 +- .../stories/tasks/monitoring-status.stories.tsx | 15 ++++++++++++++- 13 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 ui/src/api/search.test.ts diff --git a/server/src/__tests__/issue-display-status.test.ts b/server/src/__tests__/issue-display-status.test.ts index 17207b8fbb0..3ce490c416b 100644 --- a/server/src/__tests__/issue-display-status.test.ts +++ b/server/src/__tests__/issue-display-status.test.ts @@ -2,11 +2,12 @@ import { randomUUID } from "node:crypto"; import { eq, sql } from "drizzle-orm"; import { expect, it } from "vitest"; import request from "supertest"; -import { agents, heartbeatRuns, issueRecoveryActions, issues } from "@paperclipai/db"; +import { agents, heartbeatRuns, issueRecoveryActions, issues, statusCards } from "@paperclipai/db"; import { companySearchQuerySchema, createIssueSchema, PROVIDER_QUOTA_MONITOR_SERVICE_NAME } from "@paperclipai/shared"; import { issueDisplayStatusSql } from "../services/issue-display-status.js"; import { issueService } from "../services/issues.js"; import { companySearchService } from "../services/company-search.js"; +import { statusCardService } from "../services/status-cards.js"; import { issueRoutes } from "../routes/issues.js"; import { describeEmbeddedPostgres, routeApp, @@ -51,6 +52,8 @@ describeEmbeddedPostgres("Monitoring task presentation", () => { expect(result.results[0]?.issue).toMatchObject({ id: f.issueId, status: "in_progress", displayStatus: "monitoring" }); expect(result.filterOptionCounts.displayStatus?.monitoring).toBe(1); expect(result.filterOptionCounts.status.in_progress).toBe(1); + const [card] = await ctx.db.insert(statusCards).values({ companyId: f.companyId, interestPrompt: "Watch deployment", mentionedIssueIds: [f.issueId], refreshPolicy: { mode: "manual" } }).returning(); + expect(await statusCardService(ctx.db).listMentionedIssues(card!)).toEqual([expect.objectContaining({ id: f.issueId, status: "in_progress", displayStatus: "monitoring" })]); // Existing agent/API lifecycle filters keep their original meaning. expect(await svc.count(f.companyId, { status: "in_progress" })).toBe(1); expect(createIssueSchema.safeParse({ title: "Manual monitoring", status: "monitoring" }).success).toBe(false); @@ -103,6 +106,8 @@ describeEmbeddedPostgres("Monitoring task presentation", () => { const f = await fixture(); await ctx.db.insert(issueRecoveryActions).values({ companyId: f.companyId, sourceIssueId: f.issueId, kind: "source", ownerType: "board", cause: "process_lost", fingerprint: randomUUID(), nextAction: "Inspect stopped run" }); expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress"); + await ctx.db.execute(sql`UPDATE issue_recovery_actions SET status = 'escalated' WHERE source_issue_id = ${f.issueId}`); + expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress"); await ctx.db.execute(sql`UPDATE issue_recovery_actions SET status = 'resolved' WHERE source_issue_id = ${f.issueId}`); expect((await displayStatus(f.issueId)).displayStatus).toBe("monitoring"); await ctx.db.update(issues).set({ executionState: { currentParticipant: { type: "user", userId: "reviewer" } } }).where(eq(issues.id, f.issueId)); diff --git a/server/src/__tests__/issue-summaries-routes.test.ts b/server/src/__tests__/issue-summaries-routes.test.ts index c832d92af03..f82446cd20c 100644 --- a/server/src/__tests__/issue-summaries-routes.test.ts +++ b/server/src/__tests__/issue-summaries-routes.test.ts @@ -50,7 +50,7 @@ describeEmbeddedPostgres("bounded issue summaries", () => { expect(res.headers["cache-control"]).toBe("private, no-store"); expect(res.body.map((row: { id: string }) => row.id)).toEqual([f.rows[1]!.id, f.rows[0]!.id]); expect(Object.keys(res.body[0]).sort()).toEqual([ - "companyId", "externalConversationState", "id", "identifier", "status", "title", "updatedAt", + "companyId", "displayStatus", "externalConversationState", "id", "identifier", "status", "title", "updatedAt", ]); expect(res.body[0].externalConversationState).toBeNull(); expect(forbiddenRead).not.toHaveBeenCalled(); diff --git a/server/src/services/issue-display-status.ts b/server/src/services/issue-display-status.ts index e0f5fcc61f8..5c9bdefb54b 100644 --- a/server/src/services/issue-display-status.ts +++ b/server/src/services/issue-display-status.ts @@ -31,7 +31,7 @@ export function issueDisplayStatusSql(): SQL { SELECT 1 FROM issue_recovery_actions display_recovery WHERE display_recovery.company_id = ${companyId} AND display_recovery.source_issue_id = ${issueId} - AND display_recovery.status = 'active' + AND display_recovery.status IN ('active', 'escalated') ) THEN 'monitoring' ELSE ${issues.status} END`; } diff --git a/server/src/services/status-cards.ts b/server/src/services/status-cards.ts index d4fe9d7ce80..669e097c532 100644 --- a/server/src/services/status-cards.ts +++ b/server/src/services/status-cards.ts @@ -1,4 +1,5 @@ import { notifyStatusCardEvaluation } from "./status-card-notifications.js"; +import { issueDisplayStatusSql } from "./issue-display-status.js"; import { createHash } from "node:crypto"; import { and, desc, eq, gt, gte, inArray, isNotNull, isNull, lte, ne, or, sql, type SQL } from "drizzle-orm"; import { @@ -552,6 +553,7 @@ export function statusCardService( identifier: issues.identifier, title: issues.title, status: issues.status, + displayStatus: issueDisplayStatusSql(), priority: issues.priority, assigneeAgentId: issues.assigneeAgentId, assigneeUserId: issues.assigneeUserId, diff --git a/ui/src/api/search.test.ts b/ui/src/api/search.test.ts new file mode 100644 index 00000000000..9f2d39324e5 --- /dev/null +++ b/ui/src/api/search.test.ts @@ -0,0 +1,14 @@ +import { expect, it, vi } from "vitest"; +import { parseSearchQuery } from "../lib/search-query-parser"; +import { searchApi } from "./search"; +import { api } from "./client"; + +vi.mock("./client", () => ({ api: { get: vi.fn() } })); + +it("keeps scheduled monitor waits in is:open search requests", async () => { + const parsed = parseSearchQuery("is:open deployment", {}); + await searchApi.search("company-1", { q: parsed.query, ...parsed.filters }); + const url = new URL(vi.mocked(api.get).mock.calls[0]![0], "http://localhost"); + expect(url.searchParams.getAll("displayStatus")).toEqual(["backlog", "todo", "in_progress", "monitoring", "in_review", "blocked"]); + expect(url.searchParams.get("q")).toBe("deployment"); +}); diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 6695c350b2f..1a8816d0b15 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -1714,7 +1714,7 @@ function StreamlinedIssuesList({ const defaults: Record = { ...(baseCreateIssueDefaults ?? {}) }; if (projectId && defaults.projectId === undefined) defaults.projectId = projectId; if (groupKey) { - if (viewState.groupBy === "status") defaults.status = groupKey === "monitoring" ? "in_progress" : groupKey; + if (viewState.groupBy === "status") defaults.status = groupKey === "monitoring" ? "todo" : groupKey; else if (viewState.groupBy === "priority") defaults.priority = groupKey; else if (viewState.groupBy === "assignee" && groupKey !== "__unassigned") { if (groupKey.startsWith("__user:")) defaults.assigneeUserId = groupKey.slice("__user:".length); @@ -2149,7 +2149,7 @@ function StreamlinedIssuesList({ : [...viewState.collapsedGroups, group.key], }); }} - trailing={( + trailing={group.key === "monitoring" ? undefined : (