Improve issue assignee update responsiveness

This commit is contained in:
dotta 2026-04-07 18:11:41 -05:00
parent bac5afa647
commit 038dd2bb82
5 changed files with 491 additions and 10 deletions

View file

@ -1,8 +1,12 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { Issue } from "@paperclipai/shared";
import {
applyOptimisticIssueFieldUpdate,
applyOptimisticIssueFieldUpdateToCollection,
applyOptimisticIssueCommentUpdate,
createOptimisticIssueComment,
isQueuedIssueComment,
matchesIssueRef,
mergeIssueComments,
upsertIssueComment,
} from "./optimistic-issue-comments";
@ -177,6 +181,267 @@ describe("optimistic issue comments", () => {
expect(next?.assigneeUserId).toBe("board-2");
});
it("applies optimistic field updates for issue property edits", () => {
const next = applyOptimisticIssueFieldUpdate(
{
id: "issue-1",
companyId: "company-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
goalId: null,
parentId: null,
title: "Fix property pane",
description: null,
status: "todo",
priority: "medium",
assigneeAgentId: "agent-1",
assigneeUserId: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
createdByAgentId: null,
createdByUserId: "board-1",
issueNumber: 1,
identifier: "PAP-1",
originKind: "manual",
originId: null,
originRunId: null,
requestDepth: 0,
billingCode: null,
assigneeAdapterOverrides: null,
executionWorkspaceId: "exec-1",
executionWorkspacePreference: "shared_workspace",
executionWorkspaceSettings: null,
startedAt: null,
completedAt: null,
cancelledAt: null,
hiddenAt: null,
labelIds: ["label-1", "label-2"],
labels: [
{
id: "label-1",
companyId: "company-1",
name: "One",
color: "#111111",
createdAt: new Date("2026-03-28T14:00:00.000Z"),
updatedAt: new Date("2026-03-28T14:00:00.000Z"),
},
{
id: "label-2",
companyId: "company-1",
name: "Two",
color: "#222222",
createdAt: new Date("2026-03-28T14:00:00.000Z"),
updatedAt: new Date("2026-03-28T14:00:00.000Z"),
},
],
blockedBy: [
{
id: "issue-2",
identifier: "PAP-2",
title: "First blocker",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
},
{
id: "issue-3",
identifier: "PAP-3",
title: "Second blocker",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
},
],
blocks: [],
project: {
id: "project-1",
companyId: "company-1",
urlKey: "project-one",
goalId: null,
goalIds: [],
goals: [],
name: "Project one",
description: null,
status: "in_progress",
leadAgentId: null,
targetDate: null,
color: null,
env: null,
pauseReason: null,
pausedAt: null,
executionWorkspacePolicy: null,
codebase: {
workspaceId: null,
repoUrl: null,
repoRef: null,
defaultRef: null,
repoName: null,
localFolder: null,
managedFolder: "/tmp/paperclip",
effectiveLocalFolder: "/tmp/paperclip",
origin: "local_folder",
},
workspaces: [],
primaryWorkspace: null,
archivedAt: null,
createdAt: new Date("2026-03-28T14:00:00.000Z"),
updatedAt: new Date("2026-03-28T14:00:00.000Z"),
},
currentExecutionWorkspace: {
id: "exec-1",
companyId: "company-1",
projectId: "project-1",
projectWorkspaceId: null,
sourceIssueId: "issue-1",
mode: "shared_workspace",
strategyType: "project_primary",
branchName: null,
status: "active",
name: "Execution workspace",
cwd: "/tmp/paperclip",
repoUrl: null,
baseRef: null,
providerType: "local_fs",
providerRef: null,
derivedFromExecutionWorkspaceId: null,
lastUsedAt: new Date("2026-03-28T14:00:00.000Z"),
cleanupEligibleAt: null,
cleanupReason: null,
config: null,
metadata: null,
createdAt: new Date("2026-03-28T14:00:00.000Z"),
updatedAt: new Date("2026-03-28T14:00:00.000Z"),
openedAt: new Date("2026-03-28T14:00:00.000Z"),
closedAt: null,
},
createdAt: new Date("2026-03-28T14:00:00.000Z"),
updatedAt: new Date("2026-03-28T14:00:00.000Z"),
},
{
status: "in_review",
assigneeAgentId: null,
assigneeUserId: "board-2",
labelIds: ["label-2"],
blockedByIssueIds: ["issue-3"],
projectId: "project-2",
executionWorkspaceId: "exec-2",
},
);
expect(next?.status).toBe("in_review");
expect(next?.assigneeAgentId).toBeNull();
expect(next?.assigneeUserId).toBe("board-2");
expect(next?.labelIds).toEqual(["label-2"]);
expect(next?.labels?.map((label) => label.id)).toEqual(["label-2"]);
expect(next?.blockedBy?.map((relation) => relation.id)).toEqual(["issue-3"]);
expect(next?.projectId).toBe("project-2");
expect(next?.project).toBeNull();
expect(next?.executionWorkspaceId).toBe("exec-2");
expect(next?.currentExecutionWorkspace).toBeNull();
});
it("matches issues by either uuid or identifier reference", () => {
expect(matchesIssueRef({ id: "issue-1", identifier: "PAP-1" } as const, ["issue-1"])).toBe(true);
expect(matchesIssueRef({ id: "issue-1", identifier: "PAP-1" } as const, ["PAP-1"])).toBe(true);
expect(matchesIssueRef({ id: "issue-1", identifier: "PAP-1" } as const, ["issue-2", "PAP-2"])).toBe(false);
});
it("applies optimistic field updates across cached issue collections", () => {
const issues: Issue[] = [
{
id: "issue-1",
companyId: "company-1",
projectId: null,
projectWorkspaceId: null,
goalId: null,
parentId: null,
title: "Fix property pane",
description: null,
status: "todo",
priority: "medium",
assigneeAgentId: "agent-1",
assigneeUserId: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
createdByAgentId: null,
createdByUserId: "board-1",
issueNumber: 1,
identifier: "PAP-1",
originKind: "manual",
originId: null,
originRunId: null,
requestDepth: 0,
billingCode: null,
assigneeAdapterOverrides: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
executionWorkspaceSettings: null,
startedAt: null,
completedAt: null,
cancelledAt: null,
hiddenAt: null,
labelIds: [],
labels: [],
blockedBy: [],
blocks: [],
createdAt: new Date("2026-03-28T14:00:00.000Z"),
updatedAt: new Date("2026-03-28T14:00:00.000Z"),
},
{
id: "issue-2",
companyId: "company-1",
projectId: null,
projectWorkspaceId: null,
goalId: null,
parentId: null,
title: "Leave me alone",
description: null,
status: "todo",
priority: "medium",
assigneeAgentId: "agent-2",
assigneeUserId: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
createdByAgentId: null,
createdByUserId: "board-1",
issueNumber: 2,
identifier: "PAP-2",
originKind: "manual",
originId: null,
originRunId: null,
requestDepth: 0,
billingCode: null,
assigneeAdapterOverrides: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
executionWorkspaceSettings: null,
startedAt: null,
completedAt: null,
cancelledAt: null,
hiddenAt: null,
labelIds: [],
labels: [],
blockedBy: [],
blocks: [],
createdAt: new Date("2026-03-28T14:00:00.000Z"),
updatedAt: new Date("2026-03-28T14:00:00.000Z"),
},
];
const next = applyOptimisticIssueFieldUpdateToCollection(issues, ["PAP-1"], { assigneeAgentId: "agent-9" });
expect(next?.[0]?.assigneeAgentId).toBe("agent-9");
expect(next?.[1]?.assigneeAgentId).toBe("agent-2");
});
it("treats comments without a run id as queued when they arrive during an active run", () => {
expect(
isQueuedIssueComment({

View file

@ -128,3 +128,85 @@ export function applyOptimisticIssueCommentUpdate(
return nextIssue;
}
export function applyOptimisticIssueFieldUpdate(
issue: Issue | undefined,
data: Record<string, unknown>,
) {
if (!issue) return issue;
const nextIssue: Issue = {
...issue,
updatedAt: new Date(),
};
const hasOwn = (key: string) => Object.prototype.hasOwnProperty.call(data, key);
const assign = <K extends keyof Issue>(key: K) => {
if (hasOwn(key)) {
nextIssue[key] = data[key] as Issue[K];
}
};
assign("status");
assign("priority");
assign("assigneeAgentId");
assign("assigneeUserId");
assign("projectId");
assign("projectWorkspaceId");
assign("executionWorkspaceId");
assign("executionWorkspacePreference");
assign("executionWorkspaceSettings");
assign("hiddenAt");
if (hasOwn("labelIds") && Array.isArray(data.labelIds)) {
const nextLabelIds = data.labelIds.filter((value): value is string => typeof value === "string");
nextIssue.labelIds = nextLabelIds;
if (issue.labels) {
nextIssue.labels = issue.labels.filter((label) => nextLabelIds.includes(label.id));
}
}
if (hasOwn("blockedByIssueIds") && Array.isArray(data.blockedByIssueIds) && issue.blockedBy) {
const nextBlockedByIds = new Set(
data.blockedByIssueIds.filter((value): value is string => typeof value === "string"),
);
nextIssue.blockedBy = issue.blockedBy.filter((relation) => nextBlockedByIds.has(relation.id));
}
if (hasOwn("projectId")) {
nextIssue.project = issue.project?.id === nextIssue.projectId ? issue.project : null;
}
if (hasOwn("executionWorkspaceId")) {
nextIssue.currentExecutionWorkspace =
issue.currentExecutionWorkspace?.id === nextIssue.executionWorkspaceId
? issue.currentExecutionWorkspace
: null;
}
return nextIssue;
}
export function matchesIssueRef(
issue: Pick<Issue, "id" | "identifier">,
refs: Iterable<string>,
) {
const refSet = refs instanceof Set ? refs : new Set(refs);
return refSet.has(issue.id) || (!!issue.identifier && refSet.has(issue.identifier));
}
export function applyOptimisticIssueFieldUpdateToCollection(
issues: Issue[] | undefined,
refs: Iterable<string>,
data: Record<string, unknown>,
) {
if (!issues) return issues;
let changed = false;
const nextIssues = issues.map((issue) => {
if (!matchesIssueRef(issue, refs)) return issue;
changed = true;
return applyOptimisticIssueFieldUpdate(issue, data) ?? issue;
});
return changed ? nextIssues : issues;
}