From a34b90c8f127fb5d1d78b51ceaa1e6fb5cf02a87 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 30 Sep 2026 09:53:27 -0500 Subject: [PATCH] Update private task storage compatibility Co-Authored-By: Paperclip --- .../migrations/0291_private_task_access.sql | 27 ---- server/src/__tests__/issues-service.test.ts | 117 ------------------ 2 files changed, 144 deletions(-) diff --git a/packages/db/src/migrations/0291_private_task_access.sql b/packages/db/src/migrations/0291_private_task_access.sql index a5c4f2af7a6..d9fa235477c 100644 --- a/packages/db/src/migrations/0291_private_task_access.sql +++ b/packages/db/src/migrations/0291_private_task_access.sql @@ -204,33 +204,6 @@ BEGIN LOOP cursor_id := next_id; END LOOP; END $$; ---> statement-breakpoint --- Bind historical inline draft images to their first owned task. Unbound drafts --- remain uploader-only. Attachment deletion never makes the draft company-open. -DO $$ -DECLARE cursor_id uuid := '00000000-0000-0000-0000-000000000000'; next_id uuid; -BEGIN - LOOP - SELECT max(id::text)::uuid INTO next_id FROM ( - SELECT id FROM assets WHERE id > cursor_id AND object_key LIKE company_id::text || '/assets/issues/drafts/%' - ORDER BY id LIMIT 1000 - ) batch; - EXIT WHEN next_id IS NULL; - INSERT INTO issue_attachments (company_id, issue_id, asset_id) - SELECT a.company_id, first_issue.id, a.id FROM assets a - CROSS JOIN LATERAL ( - SELECT i.id FROM issues i WHERE i.company_id = a.company_id - AND (i.created_by_user_id = a.created_by_user_id OR i.responsible_user_id = a.created_by_user_id - OR i.created_by_agent_id = a.created_by_agent_id) - AND strpos(coalesce(i.description, ''), '/api/assets/' || a.id::text || '/content') > 0 - ORDER BY i.created_at, i.id LIMIT 1 - ) first_issue - WHERE a.id > cursor_id AND a.id <= next_id AND a.object_key LIKE a.company_id::text || '/assets/issues/drafts/%' - ON CONFLICT DO NOTHING; - cursor_id := next_id; - END LOOP; -END $$; - --> statement-breakpoint -- Preserve operation history while retaining every original task/run boundary. CREATE OR REPLACE FUNCTION paperclip_keep_operation_privacy_sources() RETURNS trigger AS $$ diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index a71cc5b3b2d..0c152f5cb1d 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -18,7 +18,6 @@ import { issueComments, issueInboxArchives, issueDocuments, - issueAccessGrants, issuePlanDecompositions, issueReadStates, issueRelations, @@ -26,7 +25,6 @@ import { issueWorkProducts, issues, projectWorkspaces, - projectAccessMembers, projects, workspaceOperations, toolApplications, @@ -3282,121 +3280,6 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => { expect(child.responsibleUserId).toBe(responsibleUserId); }); - it("creates children under a private issue in the same private subtree", async () => { - const companyId = randomUUID(); - const parentIssueId = randomUUID(); - await db.insert(companies).values({ - id: companyId, - name: "Private subtree company", - issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, - }); - await db.insert(issues).values({ - id: parentIssueId, - companyId, - title: "Private root", - visibility: "private", - privacyRootIssueId: parentIssueId, - }); - - const child = await svc.create(companyId, { - parentId: parentIssueId, - title: "Inherited private child", - visibility: "open", - }); - - expect(child.visibility).toBe("private"); - expect(child.privacyRootIssueId).toBe(parentIssueId); - }); - - it("creates one personal private project and parks private tasks there", async () => { - const companyId = randomUUID(); - const responsibleUserId = randomUUID(); - await db.insert(companies).values({ - id: companyId, - name: "Personal private tasks company", - issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, - }); - - const [first, second] = await Promise.all([ - svc.create(companyId, { - title: "First personal private task", - visibility: "private", - createdByUserId: responsibleUserId, - }), - svc.create(companyId, { - title: "Second personal private task", - visibility: "private", - createdByUserId: responsibleUserId, - }), - ]); - - const personalProjects = await db.select().from(projects) - .where(eq(projects.personalOwnerUserId, responsibleUserId)); - expect(personalProjects).toHaveLength(1); - expect(personalProjects[0]).toMatchObject({ - companyId, - name: "My private tasks", - visibility: "private", - }); - expect(first.projectId).toBe(personalProjects[0]!.id); - expect(second.projectId).toBe(personalProjects[0]!.id); - expect(first.privacyRootIssueId).toBe(first.id); - expect(second.privacyRootIssueId).toBe(second.id); - await expect(db.select().from(projectAccessMembers) - .where(eq(projectAccessMembers.projectId, personalProjects[0]!.id))) - .resolves.toEqual([ - expect.objectContaining({ subjectType: "user", subjectId: responsibleUserId }), - ]); - }); - - it("keeps a responsible human and grants an agent access to its explicitly private task", async () => { - const companyId = randomUUID(); - const responsibleUserId = randomUUID(); - const creatorAgentId = randomUUID(); - await db.insert(companies).values({ - id: companyId, - name: "Agent private task company", - issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, - }); - await db.insert(agents).values({ - id: creatorAgentId, - companyId, - name: "Private task creator", - role: "engineer", - adapterType: "process", - adapterConfig: {}, - runtimeConfig: {}, - permissions: {}, - }); - - const issue = await svc.create(companyId, { - title: "Agent email scratch task", - visibility: "private", - createdByAgentId: creatorAgentId, - actorResponsibleUserId: responsibleUserId, - }); - - expect(issue.responsibleUserId).toBe(responsibleUserId); - const personalProject = await db.select().from(projects) - .where(eq(projects.id, issue.projectId!)) - .then((rows) => rows[0]!); - expect(personalProject.personalOwnerUserId).toBe(responsibleUserId); - await expect(db.select().from(projectAccessMembers) - .where(eq(projectAccessMembers.projectId, personalProject.id))) - .resolves.toEqual([ - expect.objectContaining({ subjectType: "user", subjectId: responsibleUserId }), - ]); - await expect(db.select().from(issueAccessGrants) - .where(eq(issueAccessGrants.issueId, issue.id))) - .resolves.toEqual([ - expect.objectContaining({ - subjectType: "agent", - subjectId: creatorAgentId, - source: "explicit", - }), - ]); - }); - it("only honors explicit responsibleUserId for trusted issue create callers", async () => { const companyId = randomUUID(); const creatorUserId = randomUUID();