From a260ff496289df574980398292f142bf3865b8f2 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 5 Nov 2026 19:29:54 -0600 Subject: [PATCH] fix(chat): use one continuation for secret binding approval Co-Authored-By: Paperclip --- .../issue-thread-interaction-routes.test.ts | 29 +++++++++++++++++++ server/src/routes/issues.ts | 8 ----- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/server/src/__tests__/issue-thread-interaction-routes.test.ts b/server/src/__tests__/issue-thread-interaction-routes.test.ts index 68b543544d2..ed7af802d03 100644 --- a/server/src/__tests__/issue-thread-interaction-routes.test.ts +++ b/server/src/__tests__/issue-thread-interaction-routes.test.ts @@ -50,6 +50,9 @@ const mockInteractionService = vi.hoisted(() => ({ recordSecretProposalExecutionResult: vi.fn(), })); +const mockSecretProposals = vi.hoisted(() => ({ getById: vi.fn(), approve: vi.fn() })); +vi.mock("../services/secret-proposals.js", () => ({ createSecretProposalsService: () => mockSecretProposals })); + const mockHeartbeatService = vi.hoisted(() => ({ wakeup: vi.fn(async () => undefined), cancelRun: vi.fn(async () => null), @@ -1705,6 +1708,32 @@ describe("issue thread interaction routes", () => { ); }); + it("uses only the bound continuation for the production secret approval fallback", async () => { + const proposalId = "44444444-4444-4444-8444-444444444444"; + mockSecretProposals.getById.mockResolvedValue({ id: proposalId, kind: "binding", originIssueId: ISSUE_ID, + interactionId: "interaction-secret-fallback", configPath: "env.DEPLOYMENT_API_KEY" }); + mockSecretProposals.approve.mockResolvedValue({ status: "approved" }); + mockInteractionService.acceptInteraction.mockResolvedValueOnce({ interaction: { + id: "interaction-secret-fallback", companyId: "company-1", issueId: ISSUE_ID, + kind: "request_confirmation", status: "accepted", continuationPolicy: "wake_assignee", + sourceRunId: RUN_1, payload: { version: 1, prompt: "Create binding?", secretProposal: { + version: 1, proposalId, configPath: "env.DEPLOYMENT_API_KEY" } }, + result: { version: 1, outcome: "accepted" }, + }, createdIssues: [] }); + const app = await createApp(); + const res = await request(app).post(`/api/issues/${ISSUE_ID}/interactions/interaction-secret-fallback/accept`).send({}); + expect(res.status).toBe(200); + expect(mockSecretProposals.approve).toHaveBeenCalledWith("company-1", proposalId, + expect.objectContaining({ resolvedByUserId: "local-board" })); + expect(res.body.result.secretProposal.status).toBe("executed"); + expect.soft(mockIssueService.addComment).not.toHaveBeenCalled(); + expect.soft(mockHeartbeatService.wakeup).toHaveBeenCalledTimes(1); + expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(ASSIGNEE_AGENT_ID, expect.objectContaining({ + contextSnapshot: expect.objectContaining({ issueId: ISSUE_ID, interactionId: "interaction-secret-fallback" }), + payload: expect.objectContaining({ secretProposal: expect.objectContaining({ proposalId, executionStatus: "executed" }) }), + })); + }); + it("records a failed secret-proposal execution and posts a thread comment", async () => { const proposalId = "55555555-5555-4555-8555-555555555555"; const approveSecretProposal = vi.fn().mockRejectedValue(new Error("binding failed")); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 4aa9bdfeea6..1cdc4c9e7f2 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -288,7 +288,6 @@ import { retainBacklogHumanAssignment } from "../services/human-directed-work.js import { queueIssueAssignmentWakeup } from "../services/issue-assignment-wakeup.js"; import { shouldWakeAssigneeForIssueComment } from "../services/issue-comment-wakeup.js"; import { createSecretProposalsService } from "../services/secret-proposals.js"; -import { notifySecretProposalResolution } from "../services/secret-proposal-notifications.js"; import { renderOnboardingGreeting, ONBOARDING_GREETING_AUTHORIZATION_REASON, @@ -17517,13 +17516,6 @@ export function issueRoutes( proposal: lockedProposal, }), }); - await notifySecretProposalResolution({ - proposal, - status: "approved", - userId: resolvedByUserId, - issues: svc, - heartbeat, - }); } continuationInteraction = await interactionSvc.recordSecretProposalExecutionResult(