From ee928b5832a56e9d5f48370bac74a5c81c272611 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Thu, 30 Jul 2026 14:44:11 +0800 Subject: [PATCH] fix(hub): restore project create payload and stabilize integration DB seed Explorer POST /projects was dropping projectId/folderId/workspaceDir after a narrowed response shape, breaking admin-explorer. Make seedTestOrganization idempotent under shared-DB isolation, force single-worker vitest, and align the OAuth no-membership redirect expectation with authRoutes. --- hub/src/admin/routes/explorerRoutes.ts | 7 ++- hub/test/integration/admin-auth.test.ts | 3 +- hub/test/integration/helpers.ts | 71 ++++++++++++++----------- hub/vitest.config.ts | 8 +-- 4 files changed, 52 insertions(+), 37 deletions(-) diff --git a/hub/src/admin/routes/explorerRoutes.ts b/hub/src/admin/routes/explorerRoutes.ts index 9b10e97..08b6520 100644 --- a/hub/src/admin/routes/explorerRoutes.ts +++ b/hub/src/admin/routes/explorerRoutes.ts @@ -155,7 +155,12 @@ export async function registerExplorerRoutes( workspaceRoot: config.projectWorkspaceRoot, ...(typeof body.folderId === "string" ? { folderId: body.folderId } : {}), }); - return reply.status(201).send({ id: result.projectId, name: body.name }); + return reply.status(201).send({ + projectId: result.projectId, + folderId: result.folderId, + workspaceDir: result.workspaceDir, + name: body.name, + }); } catch (err) { return handleRouteError(reply, err); } diff --git a/hub/test/integration/admin-auth.test.ts b/hub/test/integration/admin-auth.test.ts index c0ad846..e4e9883 100644 --- a/hub/test/integration/admin-auth.test.ts +++ b/hub/test/integration/admin-auth.test.ts @@ -242,7 +242,8 @@ describe("admin auth + org API guards", () => { headers: { cookie: `${OAUTH_STATE_COOKIE_NAME}=${nonce}` }, }); expect(res.statusCode).toBe(302); - expect(res.headers.location).toBe("/admin"); + // New users without membership land on login error; session is still set. + expect(res.headers.location).toBe("/admin/login?error=no_organization"); expect(JSON.stringify(res.headers["set-cookie"])).toContain("cph_session="); const user = await prisma.user.findUnique({ where: { feishuOpenId: "ou_new" } }); diff --git a/hub/test/integration/helpers.ts b/hub/test/integration/helpers.ts index 29491b3..d6ba404 100644 --- a/hub/test/integration/helpers.ts +++ b/hub/test/integration/helpers.ts @@ -80,42 +80,51 @@ export async function seedTestOrganization( id: string = DEFAULT_ORG_ID, slug: string = "test-default", ): Promise { + // Serialise generate+inbox against concurrent callers in the same process. + // Integration tests share one DB and some files call seed without resetDb. await prisma.$transaction(async (tx) => { - await tx.organization.create({ - data: { - id, - slug, - name: "Test Default Organization", - projectSettings: { - create: { membersCanCreateProjects: true }, - }, - agentRoles: { - create: { - id: `agent_role_draft_${id}`, - roleId: "draft", - label: "草稿", - sortOrder: 10, - isDefault: true, + const existing = await tx.organization.findUnique({ + where: { id }, + select: { id: true }, + }); + if (existing === null) { + await tx.organization.create({ + data: { + id, + slug, + name: "Test Default Organization", + projectSettings: { + create: { membersCanCreateProjects: true }, + }, + agentRoles: { + create: { + id: `agent_role_draft_${id}`, + roleId: "draft", + label: "草稿", + sortOrder: 10, + isDefault: true, + }, }, }, - }, + }); + } + + const inbox = await tx.folder.findFirst({ + where: { organizationId: id, kind: "SYSTEM_INBOX", archivedAt: null }, + select: { id: true }, }); + if (inbox === null) { + await tx.folder.create({ + data: { + id: `folder_inbox_${id}`, + organizationId: id, + name: "Inbox", + kind: "SYSTEM_INBOX", + sortKey: "000000", + }, + }); + } }); - const inbox = await prisma.folder.findFirst({ - where: { organizationId: id, kind: "SYSTEM_INBOX", archivedAt: null }, - select: { id: true }, - }); - if (inbox === null) { - await prisma.folder.create({ - data: { - id: `folder_inbox_${id}`, - organizationId: id, - name: "Inbox", - kind: "SYSTEM_INBOX", - sortKey: "000000", - }, - }); - } } /** A logger that discards everything (tests don't need fastify's pino). */ diff --git a/hub/vitest.config.ts b/hub/vitest.config.ts index 0b64ad5..8efd4f5 100644 --- a/hub/vitest.config.ts +++ b/hub/vitest.config.ts @@ -3,11 +3,11 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { include: ["test/**/*.test.ts"], - // Integration tests share one DB; run files sequentially to avoid - // concurrent truncate/insert races. Unit tests are fast either way. - pool: "forks", - fileParallelism: false, + // Integration tests share one DB. Single worker + sequential files so + // TRUNCATE + seed cannot race across files. + pool: "threads", maxWorkers: 1, + fileParallelism: false, env: { NODE_ENV: "test" }, }, });