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.
This commit is contained in:
2026-07-19 20:08:44 +08:00
parent 7f09fb1f13
commit 15f9443d3d
4 changed files with 63 additions and 0 deletions
@@ -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, {
@@ -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(),