Update private task storage compatibility

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-30 09:53:27 -05:00
1 parent 19879cc29b
commit a34b90c8f1
2 files changed
-144

No files matched your search

@@ -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 $$
-117
View File
@@ -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();