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.
This commit is contained in:
2026-07-30 14:44:11 +08:00
parent 0f4377f16c
commit ee928b5832
4 changed files with 52 additions and 37 deletions
+6 -1
View File
@@ -155,7 +155,12 @@ export async function registerExplorerRoutes(
workspaceRoot: config.projectWorkspaceRoot, workspaceRoot: config.projectWorkspaceRoot,
...(typeof body.folderId === "string" ? { folderId: body.folderId } : {}), ...(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) { } catch (err) {
return handleRouteError(reply, err); return handleRouteError(reply, err);
} }
+2 -1
View File
@@ -242,7 +242,8 @@ describe("admin auth + org API guards", () => {
headers: { cookie: `${OAUTH_STATE_COOKIE_NAME}=${nonce}` }, headers: { cookie: `${OAUTH_STATE_COOKIE_NAME}=${nonce}` },
}); });
expect(res.statusCode).toBe(302); 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="); expect(JSON.stringify(res.headers["set-cookie"])).toContain("cph_session=");
const user = await prisma.user.findUnique({ where: { feishuOpenId: "ou_new" } }); const user = await prisma.user.findUnique({ where: { feishuOpenId: "ou_new" } });
+40 -31
View File
@@ -80,42 +80,51 @@ export async function seedTestOrganization(
id: string = DEFAULT_ORG_ID, id: string = DEFAULT_ORG_ID,
slug: string = "test-default", slug: string = "test-default",
): Promise<void> { ): Promise<void> {
// 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 prisma.$transaction(async (tx) => {
await tx.organization.create({ const existing = await tx.organization.findUnique({
data: { where: { id },
id, select: { id: true },
slug, });
name: "Test Default Organization", if (existing === null) {
projectSettings: { await tx.organization.create({
create: { membersCanCreateProjects: true }, data: {
}, id,
agentRoles: { slug,
create: { name: "Test Default Organization",
id: `agent_role_draft_${id}`, projectSettings: {
roleId: "draft", create: { membersCanCreateProjects: true },
label: "草稿", },
sortOrder: 10, agentRoles: {
isDefault: true, 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). */ /** A logger that discards everything (tests don't need fastify's pino). */
+4 -4
View File
@@ -3,11 +3,11 @@ import { defineConfig } from "vitest/config";
export default defineConfig({ export default defineConfig({
test: { test: {
include: ["test/**/*.test.ts"], include: ["test/**/*.test.ts"],
// Integration tests share one DB; run files sequentially to avoid // Integration tests share one DB. Single worker + sequential files so
// concurrent truncate/insert races. Unit tests are fast either way. // TRUNCATE + seed cannot race across files.
pool: "forks", pool: "threads",
fileParallelism: false,
maxWorkers: 1, maxWorkers: 1,
fileParallelism: false,
env: { NODE_ENV: "test" }, env: { NODE_ENV: "test" },
}, },
}); });