forked from bai/curriculum-project-hub
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:
@@ -250,6 +250,7 @@ async function initializeSilo(
|
|||||||
data: {
|
data: {
|
||||||
organizationId: input.organization.id,
|
organizationId: input.organization.id,
|
||||||
name: "Inbox",
|
name: "Inbox",
|
||||||
|
kind: "SYSTEM_INBOX",
|
||||||
sortKey: "000000",
|
sortKey: "000000",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -558,6 +558,26 @@ async function ensureInboxFolder(prisma: Prisma.TransactionClient, organizationI
|
|||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if (existing !== null) return existing;
|
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({
|
return prisma.folder.create({
|
||||||
data: { organizationId, name: "Inbox", kind: "SYSTEM_INBOX", sortKey: "000000" },
|
data: { organizationId, name: "Inbox", kind: "SYSTEM_INBOX", sortKey: "000000" },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
|
|||||||
@@ -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 () => {
|
it("blocks ordinary Feishu project creation when the org setting is off", async () => {
|
||||||
await seedUser("u-member", "ou_member", "MEMBER");
|
await seedUser("u-member", "ou_member", "MEMBER");
|
||||||
await setMembersCanCreateProjects(prisma, {
|
await setMembersCanCreateProjects(prisma, {
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ describe("Alpha Silo bootstrap", () => {
|
|||||||
{ roleId: "draft", label: "草稿", isDefault: true },
|
{ roleId: "draft", label: "草稿", isDefault: true },
|
||||||
{ roleId: "review", label: "审校", isDefault: false },
|
{ 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({
|
const persisted = JSON.stringify({
|
||||||
feishu: await prisma.feishuApplicationCredentialVersion.findMany(),
|
feishu: await prisma.feishuApplicationCredentialVersion.findMany(),
|
||||||
|
|||||||
Reference in New Issue
Block a user