From 372caafae22f8fbcca94e6729c5c6fccffb26992 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 3 Jun 2026 20:22:23 +0000 Subject: [PATCH] PAP-10279 QA evidence: no-overrides catalog install creates claude_local agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end embedded-Postgres integration test for the PAP-10277 fix (commit 9b1c8446d): teamsCatalogService.installCatalogTeam now produces claude_local-adapter agents for every bundled catalog agent the caller did not explicitly override, and explicit overrides still win. Covers QA AC1, AC2, AC5, and AC7 from PAP-10279: - core-exec-team install with no caller overrides → 3 claude_local agents, no process - product-design install with no caller overrides → claude_local, no process - product-engineering install with no caller overrides → claude_local, no process - explicit override for cto wins (opencode_local) while the rest default to claude_local Co-Authored-By: Paperclip --- ...teams-catalog-install-no-overrides.test.ts | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 server/src/__tests__/teams-catalog-install-no-overrides.test.ts diff --git a/server/src/__tests__/teams-catalog-install-no-overrides.test.ts b/server/src/__tests__/teams-catalog-install-no-overrides.test.ts new file mode 100644 index 00000000000..152e468bd9a --- /dev/null +++ b/server/src/__tests__/teams-catalog-install-no-overrides.test.ts @@ -0,0 +1,138 @@ +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 no-overrides install tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, + ); +} + +describeEmbeddedPostgres("teams catalog install with no caller adapter overrides", () => { + 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-no-overrides-")); + process.env.PAPERCLIP_HOME = tempHome; + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-teams-catalog-no-overrides-"); + 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 seedEmptyCompany() { + const companyId = randomUUID(); + await db.insert(companies).values({ + id: companyId, + name: "Clean install company", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + return companyId; + } + + async function listAdapterTypesByName(companyId: string) { + const rows = await db + .select({ + name: agents.name, + role: agents.role, + adapterType: agents.adapterType, + }) + .from(agents) + .where(eq(agents.companyId, companyId)); + return new Map(rows.map((row) => [row.name, row])); + } + + it("installs core-exec-team end-to-end with no caller overrides and creates 3 claude_local agents", async () => { + const companyId = await seedEmptyCompany(); + const svc = teamsCatalogService(db); + + await svc.installCatalogTeam(companyId, "core-exec-team", { + collisionStrategy: "rename", + include: { projects: false, issues: false }, + }); + + const byName = await listAdapterTypesByName(companyId); + expect(byName.size).toBe(3); + + const adapterTypes = Array.from(byName.values()).map((row) => row.adapterType); + expect(adapterTypes).toEqual(["claude_local", "claude_local", "claude_local"]); + expect(adapterTypes).not.toContain("process"); + expect(adapterTypes).not.toContain("http"); + }); + + it("installs product-design end-to-end with no caller overrides and uses claude_local", async () => { + const companyId = await seedEmptyCompany(); + const svc = teamsCatalogService(db); + + await svc.installCatalogTeam(companyId, "product-design", { + collisionStrategy: "rename", + include: { projects: false, issues: false }, + }); + + const byName = await listAdapterTypesByName(companyId); + expect(byName.size).toBe(1); + const adapterTypes = Array.from(byName.values()).map((row) => row.adapterType); + expect(adapterTypes).toEqual(["claude_local"]); + expect(adapterTypes).not.toContain("process"); + }); + + it("installs product-engineering end-to-end with no caller overrides and uses claude_local for every agent", async () => { + const companyId = await seedEmptyCompany(); + const svc = teamsCatalogService(db); + + await svc.installCatalogTeam(companyId, "product-engineering", { + collisionStrategy: "rename", + include: { projects: false, issues: false }, + }); + + const byName = await listAdapterTypesByName(companyId); + expect(byName.size).toBe(3); + const adapterTypes = Array.from(byName.values()).map((row) => row.adapterType); + expect(adapterTypes).toEqual(["claude_local", "claude_local", "claude_local"]); + expect(adapterTypes).not.toContain("process"); + }); + + it("honors an explicit caller adapter override for a single slug while defaulting the rest to claude_local", async () => { + const companyId = await seedEmptyCompany(); + const svc = teamsCatalogService(db); + + await svc.installCatalogTeam(companyId, "core-exec-team", { + collisionStrategy: "rename", + include: { projects: false, issues: false }, + adapterOverrides: { + cto: { adapterType: "opencode_local", adapterConfig: { model: "anthropic/claude-opus-4" } }, + }, + }); + + const byName = await listAdapterTypesByName(companyId); + expect(byName.size).toBe(3); + const ctoRow = Array.from(byName.values()).find((row) => row.role === "engineering-manager" || row.name === "CTO"); + expect(ctoRow?.adapterType).toBe("opencode_local"); + const otherAdapters = Array.from(byName.values()) + .filter((row) => row !== ctoRow) + .map((row) => row.adapterType); + expect(otherAdapters).toEqual(["claude_local", "claude_local"]); + }); +});