fix: bootstrap system user before system project (FK constraint)

The system project needs a valid ownerId that references an existing user.
Create a system@mcpctl.local user via upsert before creating the project.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-25 23:27:59 +00:00
parent d712d718db
commit d4aa677bfc
2 changed files with 39 additions and 6 deletions

View File

@@ -7,12 +7,12 @@
import type { PrismaClient } from '@prisma/client';
/** Well-known owner ID for system-managed resources. */
export const SYSTEM_OWNER_ID = 'system';
/** Well-known project name for system prompts. */
export const SYSTEM_PROJECT_NAME = 'mcpctl-system';
/** Well-known email for the system user. */
const SYSTEM_USER_EMAIL = 'system@mcpctl.local';
interface SystemPromptDef {
name: string;
priority: number;
@@ -62,6 +62,18 @@ This will load relevant project context, policies, and guidelines tailored to yo
* Uses upserts so this is safe to call on every startup.
*/
export async function bootstrapSystemProject(prisma: PrismaClient): Promise<void> {
// Ensure a system user exists (needed as project owner)
const systemUser = await prisma.user.upsert({
where: { email: SYSTEM_USER_EMAIL },
create: {
email: SYSTEM_USER_EMAIL,
name: 'System',
passwordHash: '!locked', // Cannot login — not a real password hash
role: 'USER',
},
update: {},
});
// Upsert the system project
const project = await prisma.project.upsert({
where: { name: SYSTEM_PROJECT_NAME },
@@ -71,7 +83,7 @@ export async function bootstrapSystemProject(prisma: PrismaClient): Promise<void
prompt: '',
proxyMode: 'direct',
gated: false,
ownerId: SYSTEM_OWNER_ID,
ownerId: systemUser.id,
},
update: {}, // Don't overwrite user edits to the project itself
});

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { bootstrapSystemProject, SYSTEM_PROJECT_NAME, SYSTEM_OWNER_ID, getSystemPromptNames } from '../src/bootstrap/system-project.js';
import { bootstrapSystemProject, SYSTEM_PROJECT_NAME, getSystemPromptNames } from '../src/bootstrap/system-project.js';
import type { PrismaClient } from '@prisma/client';
function mockPrisma(): PrismaClient {
@@ -7,6 +7,13 @@ function mockPrisma(): PrismaClient {
let promptIdCounter = 1;
return {
user: {
upsert: vi.fn(async () => ({
id: 'sys-user-id',
email: 'system@mcpctl.local',
name: 'System',
})),
},
project: {
upsert: vi.fn(async (args: { where: { name: string }; create: Record<string, unknown>; update: Record<string, unknown> }) => ({
id: 'sys-proj-id',
@@ -35,6 +42,20 @@ describe('bootstrapSystemProject', () => {
prisma = mockPrisma();
});
it('creates a system user via upsert', async () => {
await bootstrapSystemProject(prisma);
expect(prisma.user.upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { email: 'system@mcpctl.local' },
create: expect.objectContaining({
email: 'system@mcpctl.local',
name: 'System',
}),
}),
);
});
it('creates the mcpctl-system project via upsert', async () => {
await bootstrapSystemProject(prisma);
@@ -43,7 +64,7 @@ describe('bootstrapSystemProject', () => {
where: { name: SYSTEM_PROJECT_NAME },
create: expect.objectContaining({
name: SYSTEM_PROJECT_NAME,
ownerId: SYSTEM_OWNER_ID,
ownerId: 'sys-user-id',
gated: false,
}),
update: {},