diff --git a/server/src/__tests__/issue-thread-interactions-service.test.ts b/server/src/__tests__/issue-thread-interactions-service.test.ts index f381768b..0f0692d5 100644 --- a/server/src/__tests__/issue-thread-interactions-service.test.ts +++ b/server/src/__tests__/issue-thread-interactions-service.test.ts @@ -845,6 +845,12 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => { userId: "local-board", }); + expect(created).toMatchObject({ + payload: { + supersedeOnUserComment: true, + }, + }); + const expired = await interactionsSvc.expireRequestConfirmationsSupersededByComment({ id: issueId, companyId, @@ -903,6 +909,40 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => { expect(rows[0]?.status).toBe("pending"); }); + it("keeps legacy request confirmations pending when comment supersede was not stored", async () => { + const { companyId, issueId } = await seedConfirmationIssue("Legacy confirmation without comment supersede flag"); + + await db.insert(issueThreadInteractions).values({ + id: randomUUID(), + companyId, + issueId, + kind: "request_confirmation", + status: "pending", + continuationPolicy: { kind: "none" }, + payload: { + version: 1, + prompt: "Proceed with the current draft?", + }, + createdByUserId: "local-board", + }); + + const expired = await interactionsSvc.expireRequestConfirmationsSupersededByComment({ + id: issueId, + companyId, + }, { + id: randomUUID(), + createdAt: new Date(Date.now() + 1_000), + authorUserId: "local-board", + }, { + userId: "local-board", + }); + + expect(expired).toHaveLength(0); + const rows = await db.select().from(issueThreadInteractions); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("pending"); + }); + it("does not supersede request confirmations for agent, system, or older user comments", async () => { const { companyId, issueId } = await seedConfirmationIssue("Comment supersede exclusions"); diff --git a/server/src/services/issue-thread-interactions.ts b/server/src/services/issue-thread-interactions.ts index f27ec37e..4f84272c 100644 --- a/server/src/services/issue-thread-interactions.ts +++ b/server/src/services/issue-thread-interactions.ts @@ -159,7 +159,18 @@ function shouldReturnAcceptedConfirmationToCreatorAgent(args: { } function shouldSupersedeRequestConfirmationOnUserComment(interaction: RequestConfirmationInteraction) { - return interaction.payload.supersedeOnUserComment !== false; + return interaction.payload.supersedeOnUserComment === true; +} + +function normalizeCreateInteractionInput(input: CreateIssueThreadInteraction): CreateIssueThreadInteraction { + if (input.kind !== "request_confirmation") return input; + return { + ...input, + payload: { + ...input.payload, + supersedeOnUserComment: input.payload.supersedeOnUserComment ?? true, + }, + }; } function isCommentAtOrAfterInteraction(args: { @@ -673,7 +684,7 @@ export function issueThreadInteractionService(db: Db) { input: CreateIssueThreadInteraction, actor: InteractionActor, ) => { - const data = createIssueThreadInteractionSchema.parse(input); + const data = normalizeCreateInteractionInput(createIssueThreadInteractionSchema.parse(input)); if (data.idempotencyKey) { const existing = await getIdempotentInteraction({ diff --git a/ui/src/pages/Routines.test.tsx b/ui/src/pages/Routines.test.tsx index 22a38da4..993f0c67 100644 --- a/ui/src/pages/Routines.test.tsx +++ b/ui/src/pages/Routines.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import type { AnchorHTMLAttributes, ReactNode } from "react"; +import { act, type AnchorHTMLAttributes, type ReactNode } from "react"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { Issue, RoutineListItem } from "@paperclipai/shared"; @@ -237,10 +237,6 @@ vi.mock("../components/AgentIconPicker", () => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; -async function act(callback: () => void | Promise) { - await callback(); -} - function createRoutine(overrides: Partial): RoutineListItem { return { id: "routine-1",