From 15f9443d3d69b23b035f7c0b4df81a12fd82ffe9 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Sun, 19 Jul 2026 20:08:44 +0800 Subject: [PATCH] fix(hub): mark bootstrap Inbox as SYSTEM_INBOX Alpha silo bootstrap created the root Inbox without kind=SYSTEM_INBOX, so Feishu card project creation tried to insert a second Inbox and hit the sibling-name unique index. Tag the bootstrap folder correctly and promote any legacy root Inbox on ensure. --- hub/src/deployment/bootstrap-silo.ts | 1 + hub/src/projectOnboarding.ts | 20 ++++++++++ .../integration/project-onboarding.test.ts | 37 +++++++++++++++++++ hub/test/integration/silo-bootstrap.test.ts | 5 +++ 4 files changed, 63 insertions(+) diff --git a/hub/src/deployment/bootstrap-silo.ts b/hub/src/deployment/bootstrap-silo.ts index e4bb51b..133656f 100644 --- a/hub/src/deployment/bootstrap-silo.ts +++ b/hub/src/deployment/bootstrap-silo.ts @@ -250,6 +250,7 @@ async function initializeSilo( data: { organizationId: input.organization.id, name: "Inbox", + kind: "SYSTEM_INBOX", sortKey: "000000", }, }); diff --git a/hub/src/projectOnboarding.ts b/hub/src/projectOnboarding.ts index f9d31f6..0aa8f5b 100644 --- a/hub/src/projectOnboarding.ts +++ b/hub/src/projectOnboarding.ts @@ -558,6 +558,26 @@ async function ensureInboxFolder(prisma: Prisma.TransactionClient, organizationI select: { id: true }, }); if (existing !== null) return existing; + + // Bootstrap once created a root "Inbox" without kind=SYSTEM_INBOX. Sibling-name + // uniqueness then makes a second create fail. Recover by promoting that row. + const legacyRootInbox = await prisma.folder.findFirst({ + where: { + organizationId, + parentId: null, + name: "Inbox", + archivedAt: null, + }, + select: { id: true }, + }); + if (legacyRootInbox !== null) { + return prisma.folder.update({ + where: { id: legacyRootInbox.id }, + data: { kind: "SYSTEM_INBOX" }, + select: { id: true }, + }); + } + return prisma.folder.create({ data: { organizationId, name: "Inbox", kind: "SYSTEM_INBOX", sortKey: "000000" }, select: { id: true }, diff --git a/hub/test/integration/project-onboarding.test.ts b/hub/test/integration/project-onboarding.test.ts index 0866b66..c43ab96 100644 --- a/hub/test/integration/project-onboarding.test.ts +++ b/hub/test/integration/project-onboarding.test.ts @@ -101,6 +101,43 @@ describe("ADR-0021 project onboarding", () => { ])); }); + it("promotes a legacy root Inbox when Feishu chat creates a project", async () => { + await seedUser("u-member", "ou_member", "MEMBER"); + // Production drift after bootstrap omitted kind=SYSTEM_INBOX. The protect + // trigger refuses demotion, so the test plants the legacy shape directly. + await prisma.$executeRawUnsafe(`ALTER TABLE "Folder" DISABLE TRIGGER cph_protect_system_inbox`); + try { + await prisma.folder.updateMany({ + where: { organizationId: DEFAULT_ORG_ID, kind: "SYSTEM_INBOX", archivedAt: null }, + data: { kind: "REGULAR" }, + }); + } finally { + await prisma.$executeRawUnsafe(`ALTER TABLE "Folder" ENABLE TRIGGER cph_protect_system_inbox`); + } + const legacy = await prisma.folder.findFirstOrThrow({ + where: { organizationId: DEFAULT_ORG_ID, parentId: null, name: "Inbox", archivedAt: null }, + select: { id: true, kind: true }, + }); + expect(legacy.kind).toBe("REGULAR"); + + const result = await createProjectFromFeishuChat(prisma, { + organizationId: DEFAULT_ORG_ID, + actorFeishuOpenId: "ou_member", + chatId: "chat-legacy-inbox", + name: "Recovered Inbox Project", + workspaceRoot: await tempWorkspaceRoot(), + }); + + expect(result.folderId).toBe(legacy.id); + await expect(prisma.folder.findUniqueOrThrow({ + where: { id: legacy.id }, + select: { kind: true }, + })).resolves.toEqual({ kind: "SYSTEM_INBOX" }); + expect(await prisma.folder.count({ + where: { organizationId: DEFAULT_ORG_ID, name: "Inbox", archivedAt: null }, + })).toBe(1); + }); + it("blocks ordinary Feishu project creation when the org setting is off", async () => { await seedUser("u-member", "ou_member", "MEMBER"); await setMembersCanCreateProjects(prisma, { diff --git a/hub/test/integration/silo-bootstrap.test.ts b/hub/test/integration/silo-bootstrap.test.ts index a5e357e..8cd8276 100644 --- a/hub/test/integration/silo-bootstrap.test.ts +++ b/hub/test/integration/silo-bootstrap.test.ts @@ -62,6 +62,11 @@ describe("Alpha Silo bootstrap", () => { { roleId: "draft", label: "草稿", isDefault: true }, { roleId: "review", label: "审校", isDefault: false }, ]); + await expect(prisma.folder.findMany({ + where: { organizationId: "org_alpha", archivedAt: null }, + select: { name: true, kind: true, parentId: true }, + })).resolves.toEqual([{ name: "Inbox", kind: "SYSTEM_INBOX", parentId: null }]); + const persisted = JSON.stringify({ feishu: await prisma.feishuApplicationCredentialVersion.findMany(),