Address interaction review follow-ups

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-21 20:18:56 -05:00
1 parent 33525a209e
commit aa5c37e8e7
7 files changed
+115 -10

No files matched your search

@@ -360,6 +360,56 @@ describe("issue thread interaction routes", () => {
);
});
it("answers questions and emits an accept-only continuation wake", async () => {
mockInteractionService.answerQuestions.mockResolvedValueOnce({
id: "interaction-2",
companyId: "company-1",
issueId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
kind: "ask_user_questions",
status: "answered",
continuationPolicy: "wake_assignee_on_accept",
idempotencyKey: null,
sourceCommentId: "comment-2",
sourceRunId: "run-2",
payload: {
version: 1,
questions: [{
id: "scope",
prompt: "Scope?",
selectionMode: "single",
options: [{ id: "phase-1", label: "Phase 1" }],
}],
},
result: {
version: 1,
answers: [{ questionId: "scope", optionIds: ["phase-1"] }],
},
createdAt: "2026-04-20T12:00:00.000Z",
updatedAt: "2026-04-20T12:06:00.000Z",
resolvedAt: "2026-04-20T12:06:00.000Z",
});
const app = await createApp();
const res = await request(app)
.post("/api/issues/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/interactions/interaction-2/respond")
.send({
answers: [{ questionId: "scope", optionIds: ["phase-1"] }],
});
expect(res.status).toBe(200);
expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(
ASSIGNEE_AGENT_ID,
expect.objectContaining({
reason: "issue_commented",
payload: expect.objectContaining({
interactionId: "interaction-2",
interactionKind: "ask_user_questions",
interactionStatus: "answered",
}),
}),
);
});
it("accepts request confirmations and wakes the current assignee when configured for accept-only wakeups", async () => {
mockInteractionService.acceptInteraction.mockResolvedValueOnce({
interaction: {
+6 -1
View File
@@ -204,13 +204,18 @@ function queueResolvedInteractionContinuationWakeup(input: {
actor: { actorType: "user" | "agent"; actorId: string };
source: string;
}) {
const isAcceptResolution = input.interaction.status === "accepted"
|| (
input.interaction.kind === "ask_user_questions"
&& input.interaction.status === "answered"
);
if (
input.interaction.continuationPolicy !== "wake_assignee"
&& input.interaction.continuationPolicy !== "wake_assignee_on_accept"
) return;
if (
input.interaction.continuationPolicy === "wake_assignee_on_accept"
&& input.interaction.status !== "accepted"
&& !isAcceptResolution
) return;
if (input.interaction.status === "expired") return;
if (!input.issue.assigneeAgentId || isClosedIssueStatus(input.issue.status)) return;
@@ -731,7 +731,7 @@ export function issueThreadInteractionService(db: Db) {
const current = await getPendingInteractionForResolution({ issue, interactionId });
switch (current.kind) {
case "suggest_tasks":
return issueThreadInteractionService(db).acceptSuggestedTasks(issue, interactionId, data, actor);
return issueThreadInteractionService(db).acceptSuggestedTasks(issue, interactionId, data, actor, current);
case "request_confirmation": {
const accepted = await acceptRequestConfirmation({
issue,
@@ -754,12 +754,14 @@ export function issueThreadInteractionService(db: Db) {
interactionId: string,
input: AcceptIssueThreadInteraction,
actor: InteractionActor,
currentRow?: IssueThreadInteractionRow,
) => {
const current = await db
.select()
.from(issueThreadInteractions)
.where(eq(issueThreadInteractions.id, interactionId))
.then((rows) => rows[0] ?? null);
const current = currentRow
?? await db
.select()
.from(issueThreadInteractions)
.where(eq(issueThreadInteractions.id, interactionId))
.then((rows) => rows[0] ?? null);
if (!current) throw notFound("Interaction not found");
if (current.companyId !== issue.companyId || current.issueId !== issue.id) {