From f7b55ebfd3ed76eca9df3cb59c99a1d464170b4b Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 3 Jun 2026 23:25:02 +0000 Subject: [PATCH] PAP-10287 clean up teams catalog extraction Co-Authored-By: Paperclip --- cli/src/__tests__/company.test.ts | 8 + .../src/__tests__/company-portability.test.ts | 85 ++++++++++ .../teams-catalog-install-reparent.test.ts | 159 ------------------ 3 files changed, 93 insertions(+), 159 deletions(-) delete mode 100644 server/src/__tests__/teams-catalog-install-reparent.test.ts diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index 144b3147dcb..2736dd40b00 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -184,6 +184,8 @@ describe("renderCompanyImportPreview", () => { icon: null, capabilities: null, reportsToSlug: null, + reportsToExistingAgentId: null, + reportsToExistingAgentSlug: null, adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, @@ -401,6 +403,8 @@ describe("import selection catalog", () => { icon: null, capabilities: null, reportsToSlug: null, + reportsToExistingAgentId: null, + reportsToExistingAgentSlug: null, adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, @@ -558,6 +562,8 @@ describe("default adapter overrides", () => { icon: null, capabilities: null, reportsToSlug: null, + reportsToExistingAgentId: null, + reportsToExistingAgentSlug: null, adapterType: "process", adapterConfig: {}, runtimeConfig: {}, @@ -575,6 +581,8 @@ describe("default adapter overrides", () => { icon: null, capabilities: null, reportsToSlug: null, + reportsToExistingAgentId: null, + reportsToExistingAgentSlug: null, adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, diff --git a/server/src/__tests__/company-portability.test.ts b/server/src/__tests__/company-portability.test.ts index e3648b30988..ef05717309e 100644 --- a/server/src/__tests__/company-portability.test.ts +++ b/server/src/__tests__/company-portability.test.ts @@ -1501,6 +1501,91 @@ describe("company portability", () => { ); }); + it("reparents imported roots to pre-existing target managers before resolving imported hierarchy", async () => { + const portability = companyPortabilityService({} as any); + agentSvc.list.mockResolvedValue([ + { + id: "existing-ceo", + name: "CEO", + status: "idle", + role: "ceo", + adapterType: "claude_local", + adapterConfig: {}, + runtimeConfig: {}, + budgetMonthlyCents: 0, + permissions: {}, + metadata: null, + }, + ]); + agentSvc.create.mockImplementation(async (_companyId: string, input: Record) => ({ + id: `${String(input.name).toLowerCase()}-created`, + name: input.name, + status: input.status, + adapterType: input.adapterType, + adapterConfig: input.adapterConfig, + runtimeConfig: input.runtimeConfig, + })); + + await portability.importBundle({ + source: { + type: "inline", + rootPath: "paperclip-demo", + files: { + "COMPANY.md": [ + "---", + 'schema: "agentcompanies/v1"', + 'name: "Imported Paperclip"', + "includes:", + " - agents/cto/AGENTS.md", + " - agents/qa/AGENTS.md", + "---", + "", + ].join("\n"), + "agents/cto/AGENTS.md": [ + "---", + 'name: "CTO"', + 'slug: "cto"', + 'kind: "agent"', + "---", + "", + "Lead engineering.", + "", + ].join("\n"), + "agents/qa/AGENTS.md": [ + "---", + 'name: "QA"', + 'slug: "qa"', + 'kind: "agent"', + 'reportsTo: "cto"', + "---", + "", + "Verify engineering work.", + "", + ].join("\n"), + ".paperclip.yaml": [ + 'schema: "paperclip/v1"', + "agents:", + " cto:", + ' reportsToExistingAgentId: "existing-ceo"', + ' reportsToExistingAgentSlug: "ceo"', + " adapter:", + ' type: "claude_local"', + " qa:", + " adapter:", + ' type: "claude_local"', + "", + ].join("\n"), + }, + }, + include: { company: false, agents: true, projects: false, issues: false, skills: false }, + target: { mode: "existing_company", companyId: "company-1" }, + collisionStrategy: "rename", + }, "user-1"); + + expect(agentSvc.update).toHaveBeenCalledWith("cto-created", { reportsTo: "existing-ceo" }); + expect(agentSvc.update).toHaveBeenCalledWith("qa-created", { reportsTo: "cto-created" }); + }); + it("exports project env as portable inputs without concrete values", async () => { const portability = companyPortabilityService({} as any); diff --git a/server/src/__tests__/teams-catalog-install-reparent.test.ts b/server/src/__tests__/teams-catalog-install-reparent.test.ts deleted file mode 100644 index 17437552235..00000000000 --- a/server/src/__tests__/teams-catalog-install-reparent.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { promises as fs } from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { eq } from "drizzle-orm"; -import { agents, companies, createDb } from "@paperclipai/db"; -import { - getEmbeddedPostgresTestSupport, - startEmbeddedPostgresTestDatabase, -} from "./helpers/embedded-postgres.js"; -import { teamsCatalogService } from "../services/teams-catalog.js"; - -const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); -const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe.sequential : describe.skip; - -if (!embeddedPostgresSupport.supported) { - console.warn( - `Skipping embedded Postgres teams catalog install tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, - ); -} - -describeEmbeddedPostgres("teams catalog target-manager reparent", () => { - let db!: ReturnType; - let tempDb: Awaited> | null = null; - let tempHome: string | null = null; - let oldPaperclipHome: string | undefined; - - beforeAll(async () => { - oldPaperclipHome = process.env.PAPERCLIP_HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-teams-catalog-reparent-")); - process.env.PAPERCLIP_HOME = tempHome; - tempDb = await startEmbeddedPostgresTestDatabase("paperclip-teams-catalog-reparent-"); - db = createDb(tempDb.connectionString); - }, 20_000); - - afterAll(async () => { - if (oldPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME; - else process.env.PAPERCLIP_HOME = oldPaperclipHome; - if (tempHome) await fs.rm(tempHome, { recursive: true, force: true }); - await tempDb?.cleanup(); - }); - - async function seedCompanyWithExistingExecTeam() { - const companyId = randomUUID(); - const existingCeoId = randomUUID(); - const existingCtoId = randomUUID(); - const existingQaId = randomUUID(); - - await db.insert(companies).values({ - id: companyId, - name: "Existing company", - issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, - requireBoardApprovalForNewAgents: false, - }); - await db.insert(agents).values([ - { - id: existingCeoId, - companyId, - name: "CEO", - role: "ceo", - title: "Chief Executive Officer", - status: "idle", - adapterType: "claude_local", - adapterConfig: {}, - runtimeConfig: {}, - permissions: {}, - }, - { - id: existingCtoId, - companyId, - name: "CTO", - role: "engineering-manager", - title: "Chief Technology Officer", - status: "idle", - reportsTo: existingCeoId, - adapterType: "claude_local", - adapterConfig: {}, - runtimeConfig: {}, - permissions: {}, - }, - { - id: existingQaId, - companyId, - name: "QA", - role: "qa", - title: "QA Engineer", - status: "idle", - reportsTo: existingCtoId, - adapterType: "claude_local", - adapterConfig: {}, - runtimeConfig: {}, - permissions: {}, - }, - ]); - - return { companyId, existingCeoId }; - } - - async function listCompanyAgents(companyId: string) { - const rows = await db - .select({ - id: agents.id, - name: agents.name, - reportsTo: agents.reportsTo, - }) - .from(agents) - .where(eq(agents.companyId, companyId)); - return new Map(rows.map((agent) => [agent.name, agent])); - } - - const adapterOverrides = { - ceo: { adapterType: "claude_local" }, - cto: { adapterType: "claude_local" }, - qa: { adapterType: "claude_local" }, - }; - - it("reparents the renamed imported root to targetManagerSlug while preserving renamed imported hierarchy", async () => { - const { companyId, existingCeoId } = await seedCompanyWithExistingExecTeam(); - const svc = teamsCatalogService(db); - - await svc.installCatalogTeam(companyId, "core-exec-team", { - targetManagerSlug: "ceo", - collisionStrategy: "rename", - include: { projects: false, issues: false }, - adapterOverrides, - }); - - const byName = await listCompanyAgents(companyId); - const importedCeo = byName.get("CEO 2"); - const importedCto = byName.get("CTO 2"); - const importedQa = byName.get("QA 2"); - - expect(importedCeo?.reportsTo).toBe(existingCeoId); - expect(importedCto?.reportsTo).toBe(importedCeo?.id); - expect(importedQa?.reportsTo).toBe(importedCto?.id); - }); - - it("reparents the renamed imported root to targetManagerAgentId while preserving renamed imported hierarchy", async () => { - const { companyId, existingCeoId } = await seedCompanyWithExistingExecTeam(); - const svc = teamsCatalogService(db); - - await svc.installCatalogTeam(companyId, "core-exec-team", { - targetManagerAgentId: existingCeoId, - collisionStrategy: "rename", - include: { projects: false, issues: false }, - adapterOverrides, - }); - - const byName = await listCompanyAgents(companyId); - const importedCeo = byName.get("CEO 2"); - const importedCto = byName.get("CTO 2"); - const importedQa = byName.get("QA 2"); - - expect(importedCeo?.reportsTo).toBe(existingCeoId); - expect(importedCto?.reportsTo).toBe(importedCeo?.id); - expect(importedQa?.reportsTo).toBe(importedCto?.id); - }); -});