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:
@@ -7,12 +7,12 @@
|
|||||||
|
|
||||||
import type { PrismaClient } from '@prisma/client';
|
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. */
|
/** Well-known project name for system prompts. */
|
||||||
export const SYSTEM_PROJECT_NAME = 'mcpctl-system';
|
export const SYSTEM_PROJECT_NAME = 'mcpctl-system';
|
||||||
|
|
||||||
|
/** Well-known email for the system user. */
|
||||||
|
const SYSTEM_USER_EMAIL = 'system@mcpctl.local';
|
||||||
|
|
||||||
interface SystemPromptDef {
|
interface SystemPromptDef {
|
||||||
name: string;
|
name: string;
|
||||||
priority: number;
|
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.
|
* Uses upserts so this is safe to call on every startup.
|
||||||
*/
|
*/
|
||||||
export async function bootstrapSystemProject(prisma: PrismaClient): Promise<void> {
|
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
|
// Upsert the system project
|
||||||
const project = await prisma.project.upsert({
|
const project = await prisma.project.upsert({
|
||||||
where: { name: SYSTEM_PROJECT_NAME },
|
where: { name: SYSTEM_PROJECT_NAME },
|
||||||
@@ -71,7 +83,7 @@ export async function bootstrapSystemProject(prisma: PrismaClient): Promise<void
|
|||||||
prompt: '',
|
prompt: '',
|
||||||
proxyMode: 'direct',
|
proxyMode: 'direct',
|
||||||
gated: false,
|
gated: false,
|
||||||
ownerId: SYSTEM_OWNER_ID,
|
ownerId: systemUser.id,
|
||||||
},
|
},
|
||||||
update: {}, // Don't overwrite user edits to the project itself
|
update: {}, // Don't overwrite user edits to the project itself
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
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';
|
import type { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
function mockPrisma(): PrismaClient {
|
function mockPrisma(): PrismaClient {
|
||||||
@@ -7,6 +7,13 @@ function mockPrisma(): PrismaClient {
|
|||||||
let promptIdCounter = 1;
|
let promptIdCounter = 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
user: {
|
||||||
|
upsert: vi.fn(async () => ({
|
||||||
|
id: 'sys-user-id',
|
||||||
|
email: 'system@mcpctl.local',
|
||||||
|
name: 'System',
|
||||||
|
})),
|
||||||
|
},
|
||||||
project: {
|
project: {
|
||||||
upsert: vi.fn(async (args: { where: { name: string }; create: Record<string, unknown>; update: Record<string, unknown> }) => ({
|
upsert: vi.fn(async (args: { where: { name: string }; create: Record<string, unknown>; update: Record<string, unknown> }) => ({
|
||||||
id: 'sys-proj-id',
|
id: 'sys-proj-id',
|
||||||
@@ -35,6 +42,20 @@ describe('bootstrapSystemProject', () => {
|
|||||||
prisma = mockPrisma();
|
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 () => {
|
it('creates the mcpctl-system project via upsert', async () => {
|
||||||
await bootstrapSystemProject(prisma);
|
await bootstrapSystemProject(prisma);
|
||||||
|
|
||||||
@@ -43,7 +64,7 @@ describe('bootstrapSystemProject', () => {
|
|||||||
where: { name: SYSTEM_PROJECT_NAME },
|
where: { name: SYSTEM_PROJECT_NAME },
|
||||||
create: expect.objectContaining({
|
create: expect.objectContaining({
|
||||||
name: SYSTEM_PROJECT_NAME,
|
name: SYSTEM_PROJECT_NAME,
|
||||||
ownerId: SYSTEM_OWNER_ID,
|
ownerId: 'sys-user-id',
|
||||||
gated: false,
|
gated: false,
|
||||||
}),
|
}),
|
||||||
update: {},
|
update: {},
|
||||||
|
|||||||
Reference in New Issue
Block a user