Optimize issues workspace lookups

(cherry picked from commit 493ef18a2f68ee5372a2da585a2f6b709f5cf377)
This commit is contained in:
Dotta committed 2026-04-15 09:12:41 -05:00
1 parent 801a91e039
commit fd22d10db3
11 files changed
+248 -20

No files matched your search

+1
View File
@@ -224,6 +224,7 @@ export type {
ProjectGoalRef,
ProjectWorkspace,
ExecutionWorkspace,
ExecutionWorkspaceSummary,
ExecutionWorkspaceConfig,
ExecutionWorkspaceCloseAction,
ExecutionWorkspaceCloseActionKind,
+1
View File
@@ -63,6 +63,7 @@ export type { AssetImage } from "./asset.js";
export type { Project, ProjectCodebase, ProjectCodebaseOrigin, ProjectGoalRef, ProjectWorkspace } from "./project.js";
export type {
ExecutionWorkspace,
ExecutionWorkspaceSummary,
ExecutionWorkspaceConfig,
ExecutionWorkspaceCloseAction,
ExecutionWorkspaceCloseActionKind,
@@ -161,6 +161,13 @@ export interface IssueExecutionWorkspaceSettings {
workspaceRuntime?: Record<string, unknown> | null;
}
export interface ExecutionWorkspaceSummary {
id: string;
name: string;
mode: Exclude<ExecutionWorkspaceMode, "inherit" | "reuse_existing" | "agent_default"> | "adapter_managed" | "cloud_sandbox";
projectWorkspaceId: string | null;
}
export interface ExecutionWorkspace {
id: string;
companyId: string;
@@ -0,0 +1,90 @@
import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockExecutionWorkspaceService = vi.hoisted(() => ({
list: vi.fn(),
listSummaries: vi.fn(),
getById: vi.fn(),
getCloseReadiness: vi.fn(),
update: vi.fn(),
}));
const mockWorkspaceOperationService = vi.hoisted(() => ({
listForExecutionWorkspace: vi.fn(),
createRecorder: vi.fn(),
}));
function registerServiceMocks() {
vi.doMock("../services/index.js", () => ({
executionWorkspaceService: () => mockExecutionWorkspaceService,
logActivity: vi.fn(async () => undefined),
workspaceOperationService: () => mockWorkspaceOperationService,
}));
}
async function createApp() {
const [{ executionWorkspaceRoutes }, { errorHandler }] = await Promise.all([
vi.importActual<typeof import("../routes/execution-workspaces.js")>("../routes/execution-workspaces.js"),
vi.importActual<typeof import("../middleware/index.js")>("../middleware/index.js"),
]);
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
(req as any).actor = {
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
};
next();
});
app.use("/api", executionWorkspaceRoutes({} as any));
app.use(errorHandler);
return app;
}
describe("execution workspace routes", () => {
beforeEach(() => {
vi.resetModules();
vi.doUnmock("../services/index.js");
vi.doUnmock("../routes/execution-workspaces.js");
vi.doUnmock("../routes/authz.js");
vi.doUnmock("../middleware/index.js");
registerServiceMocks();
vi.resetAllMocks();
mockExecutionWorkspaceService.list.mockResolvedValue([]);
mockExecutionWorkspaceService.listSummaries.mockResolvedValue([
{
id: "workspace-1",
name: "Alpha",
mode: "isolated_workspace",
projectWorkspaceId: null,
},
]);
});
it("uses summary mode for lightweight workspace lookups", async () => {
const res = await request(await createApp())
.get("/api/companies/company-1/execution-workspaces?summary=true&reuseEligible=true");
expect(res.status).toBe(200);
expect(res.body).toEqual([
{
id: "workspace-1",
name: "Alpha",
mode: "isolated_workspace",
projectWorkspaceId: null,
},
]);
expect(mockExecutionWorkspaceService.listSummaries).toHaveBeenCalledWith("company-1", {
projectId: undefined,
projectWorkspaceId: undefined,
issueId: undefined,
status: undefined,
reuseEligible: true,
});
expect(mockExecutionWorkspaceService.list).not.toHaveBeenCalled();
});
});
+5 -2
View File
@@ -37,13 +37,16 @@ export function executionWorkspaceRoutes(db: Db) {
router.get("/companies/:companyId/execution-workspaces", async (req, res) => {
const companyId = req.params.companyId as string;
assertCompanyAccess(req, companyId);
const workspaces = await svc.list(companyId, {
const filters = {
projectId: req.query.projectId as string | undefined,
projectWorkspaceId: req.query.projectWorkspaceId as string | undefined,
issueId: req.query.issueId as string | undefined,
status: req.query.status as string | undefined,
reuseEligible: req.query.reuseEligible === "true",
});
};
const workspaces = req.query.summary === "true"
? await svc.listSummaries(companyId, filters)
: await svc.list(companyId, filters);
res.json(workspaces);
});
+59 -15
View File
@@ -7,6 +7,7 @@ import type { Db } from "@paperclipai/db";
import { executionWorkspaces, issues, projects, projectWorkspaces, workspaceRuntimeServices } from "@paperclipai/db";
import type {
ExecutionWorkspace,
ExecutionWorkspaceSummary,
ExecutionWorkspaceCloseAction,
ExecutionWorkspaceCloseGitReadiness,
ExecutionWorkspaceCloseReadiness,
@@ -336,6 +337,15 @@ function toExecutionWorkspace(
};
}
function toExecutionWorkspaceSummary(row: Pick<ExecutionWorkspaceRow, "id" | "name" | "mode" | "projectWorkspaceId">): ExecutionWorkspaceSummary {
return {
id: row.id,
name: row.name,
mode: row.mode as ExecutionWorkspaceSummary["mode"],
projectWorkspaceId: row.projectWorkspaceId ?? null,
};
}
function usesInheritedProjectRuntimeServices(row: ExecutionWorkspaceRow) {
if (row.mode !== "shared_workspace" || !row.projectWorkspaceId) return false;
return !readExecutionWorkspaceConfig((row.metadata as Record<string, unknown> | null) ?? null)?.workspaceRuntime;
@@ -372,6 +382,33 @@ async function loadEffectiveRuntimeServicesByExecutionWorkspace(
}
export function executionWorkspaceService(db: Db) {
function buildListConditions(
companyId: string,
filters?: {
projectId?: string;
projectWorkspaceId?: string;
issueId?: string;
status?: string;
reuseEligible?: boolean;
},
) {
const conditions = [eq(executionWorkspaces.companyId, companyId)];
if (filters?.projectId) conditions.push(eq(executionWorkspaces.projectId, filters.projectId));
if (filters?.projectWorkspaceId) {
conditions.push(eq(executionWorkspaces.projectWorkspaceId, filters.projectWorkspaceId));
}
if (filters?.issueId) conditions.push(eq(executionWorkspaces.sourceIssueId, filters.issueId));
if (filters?.status) {
const statuses = filters.status.split(",").map((value) => value.trim()).filter(Boolean);
if (statuses.length === 1) conditions.push(eq(executionWorkspaces.status, statuses[0]!));
else if (statuses.length > 1) conditions.push(inArray(executionWorkspaces.status, statuses));
}
if (filters?.reuseEligible) {
conditions.push(inArray(executionWorkspaces.status, ["active", "idle", "in_review"]));
}
return conditions;
}
return {
list: async (companyId: string, filters?: {
projectId?: string;
@@ -380,21 +417,7 @@ export function executionWorkspaceService(db: Db) {
status?: string;
reuseEligible?: boolean;
}) => {
const conditions = [eq(executionWorkspaces.companyId, companyId)];
if (filters?.projectId) conditions.push(eq(executionWorkspaces.projectId, filters.projectId));
if (filters?.projectWorkspaceId) {
conditions.push(eq(executionWorkspaces.projectWorkspaceId, filters.projectWorkspaceId));
}
if (filters?.issueId) conditions.push(eq(executionWorkspaces.sourceIssueId, filters.issueId));
if (filters?.status) {
const statuses = filters.status.split(",").map((value) => value.trim()).filter(Boolean);
if (statuses.length === 1) conditions.push(eq(executionWorkspaces.status, statuses[0]!));
else if (statuses.length > 1) conditions.push(inArray(executionWorkspaces.status, statuses));
}
if (filters?.reuseEligible) {
conditions.push(inArray(executionWorkspaces.status, ["active", "idle", "in_review"]));
}
const conditions = buildListConditions(companyId, filters);
const rows = await db
.select()
.from(executionWorkspaces)
@@ -409,6 +432,27 @@ export function executionWorkspaceService(db: Db) {
);
},
listSummaries: async (companyId: string, filters?: {
projectId?: string;
projectWorkspaceId?: string;
issueId?: string;
status?: string;
reuseEligible?: boolean;
}) => {
const conditions = buildListConditions(companyId, filters);
const rows = await db
.select({
id: executionWorkspaces.id,
name: executionWorkspaces.name,
mode: executionWorkspaces.mode,
projectWorkspaceId: executionWorkspaces.projectWorkspaceId,
})
.from(executionWorkspaces)
.where(and(...conditions))
.orderBy(desc(executionWorkspaces.lastUsedAt), desc(executionWorkspaces.createdAt));
return rows.map((row) => toExecutionWorkspaceSummary(row));
},
getById: async (id: string) => {
const row = await db
.select()
+29
View File
@@ -0,0 +1,29 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockApi = vi.hoisted(() => ({
get: vi.fn(),
}));
vi.mock("./client", () => ({
api: mockApi,
}));
import { executionWorkspacesApi } from "./execution-workspaces";
describe("executionWorkspacesApi.listSummaries", () => {
beforeEach(() => {
mockApi.get.mockReset();
mockApi.get.mockResolvedValue([]);
});
it("requests the lightweight summary payload", async () => {
await executionWorkspacesApi.listSummaries("company-1", {
projectId: "project-1",
reuseEligible: true,
});
expect(mockApi.get).toHaveBeenCalledWith(
"/companies/company-1/execution-workspaces?projectId=project-1&reuseEligible=true&summary=true",
);
});
});
+23
View File
@@ -1,5 +1,6 @@
import type {
ExecutionWorkspace,
ExecutionWorkspaceSummary,
ExecutionWorkspaceCloseReadiness,
WorkspaceOperation,
WorkspaceRuntimeControlTarget,
@@ -8,6 +9,28 @@ import { api } from "./client";
import { sanitizeWorkspaceRuntimeControlTarget } from "./workspace-runtime-control";
export const executionWorkspacesApi = {
listSummaries: (
companyId: string,
filters?: {
projectId?: string;
projectWorkspaceId?: string;
issueId?: string;
status?: string;
reuseEligible?: boolean;
},
) => {
const params = new URLSearchParams();
if (filters?.projectId) params.set("projectId", filters.projectId);
if (filters?.projectWorkspaceId) params.set("projectWorkspaceId", filters.projectWorkspaceId);
if (filters?.issueId) params.set("issueId", filters.issueId);
if (filters?.status) params.set("status", filters.status);
if (filters?.reuseEligible) params.set("reuseEligible", "true");
params.set("summary", "true");
const qs = params.toString();
return api.get<ExecutionWorkspaceSummary[]>(
`/companies/${companyId}/execution-workspaces${qs ? `?${qs}` : ""}`,
);
},
list: (
companyId: string,
filters?: {
+29 -1
View File
@@ -28,6 +28,7 @@ const mockAuthApi = vi.hoisted(() => ({
const mockExecutionWorkspacesApi = vi.hoisted(() => ({
list: vi.fn(),
listSummaries: vi.fn(),
}));
const mockInstanceSettingsApi = vi.hoisted(() => ({
@@ -183,11 +184,13 @@ describe("IssuesList", () => {
mockIssuesApi.listLabels.mockReset();
mockAuthApi.getSession.mockReset();
mockExecutionWorkspacesApi.list.mockReset();
mockExecutionWorkspacesApi.listSummaries.mockReset();
mockInstanceSettingsApi.getExperimental.mockReset();
mockIssuesApi.list.mockResolvedValue([]);
mockIssuesApi.listLabels.mockResolvedValue([]);
mockAuthApi.getSession.mockResolvedValue({ user: null, session: null });
mockExecutionWorkspacesApi.list.mockResolvedValue([]);
mockExecutionWorkspacesApi.listSummaries.mockResolvedValue([]);
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false });
localStorage.clear();
});
@@ -460,7 +463,7 @@ describe("IssuesList", () => {
it("filters the list to a single workspace when a workspace name is clicked", async () => {
localStorage.setItem("paperclip:test-issues:company-1:issue-columns", JSON.stringify(["id", "workspace"]));
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: true });
mockExecutionWorkspacesApi.list.mockResolvedValue([
mockExecutionWorkspacesApi.listSummaries.mockResolvedValue([
{
id: "workspace-alpha",
name: "Alpha",
@@ -661,4 +664,29 @@ describe("IssuesList", () => {
root.unmount();
});
});
it("uses workspace summaries instead of the full workspace list on the issues page", async () => {
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: true });
mockExecutionWorkspacesApi.listSummaries.mockResolvedValue([]);
const { root } = renderWithQueryClient(
<IssuesList
issues={[createIssue()]}
agents={[]}
projects={[]}
viewStateKey="paperclip:test-issues"
onUpdateIssue={() => undefined}
/>,
container,
);
await waitForAssertion(() => {
expect(mockExecutionWorkspacesApi.listSummaries).toHaveBeenCalledWith("company-1");
expect(mockExecutionWorkspacesApi.list).not.toHaveBeenCalled();
});
act(() => {
root.unmount();
});
});
});
+2 -2
View File
@@ -350,9 +350,9 @@ export function IssuesList({
});
const { data: executionWorkspaces = [] } = useQuery({
queryKey: selectedCompanyId
? queryKeys.executionWorkspaces.list(selectedCompanyId)
? queryKeys.executionWorkspaces.summaryList(selectedCompanyId)
: ["execution-workspaces", "__disabled__"],
queryFn: () => executionWorkspacesApi.list(selectedCompanyId!),
queryFn: () => executionWorkspacesApi.listSummaries(selectedCompanyId!),
enabled: !!selectedCompanyId && isolatedWorkspacesEnabled,
});
+2
View File
@@ -65,6 +65,8 @@ export const queryKeys = {
executionWorkspaces: {
list: (companyId: string, filters?: Record<string, string | boolean | undefined>) =>
["execution-workspaces", companyId, filters ?? {}] as const,
summaryList: (companyId: string, filters?: Record<string, string | boolean | undefined>) =>
["execution-workspaces", companyId, "summary", filters ?? {}] as const,
detail: (id: string) => ["execution-workspaces", "detail", id] as const,
closeReadiness: (id: string) => ["execution-workspaces", "close-readiness", id] as const,
workspaceOperations: (id: string) => ["execution-workspaces", "workspace-operations", id] as const,