303 lines
10 KiB
TypeScript
303 lines
10 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
import { PromptService } from '../../src/services/prompt.service.js';
|
||
|
|
import type { IPromptRepository } from '../../src/repositories/prompt.repository.js';
|
||
|
|
import type { IPromptRequestRepository } from '../../src/repositories/prompt-request.repository.js';
|
||
|
|
import type { IProjectRepository } from '../../src/repositories/project.repository.js';
|
||
|
|
import type { Prompt, PromptRequest, Project } from '@prisma/client';
|
||
|
|
|
||
|
|
function makePrompt(overrides: Partial<Prompt> = {}): Prompt {
|
||
|
|
return {
|
||
|
|
id: 'prompt-1',
|
||
|
|
name: 'test-prompt',
|
||
|
|
content: 'Hello world',
|
||
|
|
projectId: null,
|
||
|
|
version: 1,
|
||
|
|
createdAt: new Date(),
|
||
|
|
updatedAt: new Date(),
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makePromptRequest(overrides: Partial<PromptRequest> = {}): PromptRequest {
|
||
|
|
return {
|
||
|
|
id: 'req-1',
|
||
|
|
name: 'test-request',
|
||
|
|
content: 'Proposed content',
|
||
|
|
projectId: null,
|
||
|
|
createdBySession: 'session-abc',
|
||
|
|
createdByUserId: null,
|
||
|
|
createdAt: new Date(),
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeProject(overrides: Partial<Project> = {}): Project {
|
||
|
|
return {
|
||
|
|
id: 'proj-1',
|
||
|
|
name: 'test-project',
|
||
|
|
description: '',
|
||
|
|
prompt: '',
|
||
|
|
proxyMode: 'direct',
|
||
|
|
llmProvider: null,
|
||
|
|
llmModel: null,
|
||
|
|
ownerId: 'user-1',
|
||
|
|
createdAt: new Date(),
|
||
|
|
updatedAt: new Date(),
|
||
|
|
...overrides,
|
||
|
|
} as Project;
|
||
|
|
}
|
||
|
|
|
||
|
|
function mockPromptRepo(): IPromptRepository {
|
||
|
|
return {
|
||
|
|
findAll: vi.fn(async () => []),
|
||
|
|
findById: vi.fn(async () => null),
|
||
|
|
findByNameAndProject: vi.fn(async () => null),
|
||
|
|
create: vi.fn(async (data) => makePrompt(data)),
|
||
|
|
update: vi.fn(async (id, data) => makePrompt({ id, ...data })),
|
||
|
|
delete: vi.fn(async () => {}),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function mockPromptRequestRepo(): IPromptRequestRepository {
|
||
|
|
return {
|
||
|
|
findAll: vi.fn(async () => []),
|
||
|
|
findById: vi.fn(async () => null),
|
||
|
|
findByNameAndProject: vi.fn(async () => null),
|
||
|
|
findBySession: vi.fn(async () => []),
|
||
|
|
create: vi.fn(async (data) => makePromptRequest(data)),
|
||
|
|
delete: vi.fn(async () => {}),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function mockProjectRepo(): IProjectRepository {
|
||
|
|
return {
|
||
|
|
findAll: vi.fn(async () => []),
|
||
|
|
findById: vi.fn(async () => null),
|
||
|
|
findByName: vi.fn(async () => null),
|
||
|
|
create: vi.fn(async (data) => makeProject(data)),
|
||
|
|
update: vi.fn(async (id, data) => makeProject({ id, ...data })),
|
||
|
|
delete: vi.fn(async () => {}),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('PromptService', () => {
|
||
|
|
let promptRepo: IPromptRepository;
|
||
|
|
let promptRequestRepo: IPromptRequestRepository;
|
||
|
|
let projectRepo: IProjectRepository;
|
||
|
|
let service: PromptService;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
promptRepo = mockPromptRepo();
|
||
|
|
promptRequestRepo = mockPromptRequestRepo();
|
||
|
|
projectRepo = mockProjectRepo();
|
||
|
|
service = new PromptService(promptRepo, promptRequestRepo, projectRepo);
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── Prompt CRUD ──
|
||
|
|
|
||
|
|
describe('listPrompts', () => {
|
||
|
|
it('should return all prompts', async () => {
|
||
|
|
const prompts = [makePrompt(), makePrompt({ id: 'prompt-2', name: 'other' })];
|
||
|
|
vi.mocked(promptRepo.findAll).mockResolvedValue(prompts);
|
||
|
|
|
||
|
|
const result = await service.listPrompts();
|
||
|
|
expect(result).toEqual(prompts);
|
||
|
|
expect(promptRepo.findAll).toHaveBeenCalledWith(undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should filter by projectId', async () => {
|
||
|
|
await service.listPrompts('proj-1');
|
||
|
|
expect(promptRepo.findAll).toHaveBeenCalledWith('proj-1');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('getPrompt', () => {
|
||
|
|
it('should return a prompt by id', async () => {
|
||
|
|
const prompt = makePrompt();
|
||
|
|
vi.mocked(promptRepo.findById).mockResolvedValue(prompt);
|
||
|
|
|
||
|
|
const result = await service.getPrompt('prompt-1');
|
||
|
|
expect(result).toEqual(prompt);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should throw NotFoundError for missing prompt', async () => {
|
||
|
|
await expect(service.getPrompt('nope')).rejects.toThrow('Prompt not found: nope');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('createPrompt', () => {
|
||
|
|
it('should create a prompt', async () => {
|
||
|
|
const result = await service.createPrompt({ name: 'new-prompt', content: 'stuff' });
|
||
|
|
expect(promptRepo.create).toHaveBeenCalledWith({ name: 'new-prompt', content: 'stuff' });
|
||
|
|
expect(result.name).toBe('new-prompt');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should validate project exists when projectId given', async () => {
|
||
|
|
vi.mocked(projectRepo.findById).mockResolvedValue(makeProject());
|
||
|
|
await service.createPrompt({ name: 'scoped', content: 'x', projectId: 'proj-1' });
|
||
|
|
expect(projectRepo.findById).toHaveBeenCalledWith('proj-1');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should throw when project not found', async () => {
|
||
|
|
await expect(
|
||
|
|
service.createPrompt({ name: 'bad', content: 'x', projectId: 'nope' }),
|
||
|
|
).rejects.toThrow('Project not found: nope');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should reject invalid name format', async () => {
|
||
|
|
await expect(
|
||
|
|
service.createPrompt({ name: 'INVALID_NAME', content: 'x' }),
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('updatePrompt', () => {
|
||
|
|
it('should update prompt content', async () => {
|
||
|
|
vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt());
|
||
|
|
await service.updatePrompt('prompt-1', { content: 'updated' });
|
||
|
|
expect(promptRepo.update).toHaveBeenCalledWith('prompt-1', { content: 'updated' });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should throw for missing prompt', async () => {
|
||
|
|
await expect(service.updatePrompt('nope', { content: 'x' })).rejects.toThrow('Prompt not found');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('deletePrompt', () => {
|
||
|
|
it('should delete an existing prompt', async () => {
|
||
|
|
vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt());
|
||
|
|
await service.deletePrompt('prompt-1');
|
||
|
|
expect(promptRepo.delete).toHaveBeenCalledWith('prompt-1');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should throw for missing prompt', async () => {
|
||
|
|
await expect(service.deletePrompt('nope')).rejects.toThrow('Prompt not found');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── PromptRequest CRUD ──
|
||
|
|
|
||
|
|
describe('listPromptRequests', () => {
|
||
|
|
it('should return all prompt requests', async () => {
|
||
|
|
const reqs = [makePromptRequest()];
|
||
|
|
vi.mocked(promptRequestRepo.findAll).mockResolvedValue(reqs);
|
||
|
|
|
||
|
|
const result = await service.listPromptRequests();
|
||
|
|
expect(result).toEqual(reqs);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('getPromptRequest', () => {
|
||
|
|
it('should return a prompt request by id', async () => {
|
||
|
|
const req = makePromptRequest();
|
||
|
|
vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);
|
||
|
|
|
||
|
|
const result = await service.getPromptRequest('req-1');
|
||
|
|
expect(result).toEqual(req);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should throw for missing request', async () => {
|
||
|
|
await expect(service.getPromptRequest('nope')).rejects.toThrow('PromptRequest not found');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('deletePromptRequest', () => {
|
||
|
|
it('should delete an existing request', async () => {
|
||
|
|
vi.mocked(promptRequestRepo.findById).mockResolvedValue(makePromptRequest());
|
||
|
|
await service.deletePromptRequest('req-1');
|
||
|
|
expect(promptRequestRepo.delete).toHaveBeenCalledWith('req-1');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── Propose ──
|
||
|
|
|
||
|
|
describe('propose', () => {
|
||
|
|
it('should create a prompt request', async () => {
|
||
|
|
const result = await service.propose({
|
||
|
|
name: 'my-prompt',
|
||
|
|
content: 'proposal',
|
||
|
|
createdBySession: 'sess-1',
|
||
|
|
});
|
||
|
|
expect(promptRequestRepo.create).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({ name: 'my-prompt', content: 'proposal', createdBySession: 'sess-1' }),
|
||
|
|
);
|
||
|
|
expect(result.name).toBe('my-prompt');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should validate project exists when projectId given', async () => {
|
||
|
|
vi.mocked(projectRepo.findById).mockResolvedValue(makeProject());
|
||
|
|
await service.propose({
|
||
|
|
name: 'scoped',
|
||
|
|
content: 'x',
|
||
|
|
projectId: 'proj-1',
|
||
|
|
});
|
||
|
|
expect(projectRepo.findById).toHaveBeenCalledWith('proj-1');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── Approve ──
|
||
|
|
|
||
|
|
describe('approve', () => {
|
||
|
|
it('should delete request and create prompt (atomic)', async () => {
|
||
|
|
const req = makePromptRequest({ id: 'req-1', name: 'approved', content: 'good stuff', projectId: 'proj-1' });
|
||
|
|
vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);
|
||
|
|
|
||
|
|
const result = await service.approve('req-1');
|
||
|
|
|
||
|
|
expect(promptRepo.create).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({ name: 'approved', content: 'good stuff', projectId: 'proj-1' }),
|
||
|
|
);
|
||
|
|
expect(promptRequestRepo.delete).toHaveBeenCalledWith('req-1');
|
||
|
|
expect(result.name).toBe('approved');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should throw for missing request', async () => {
|
||
|
|
await expect(service.approve('nope')).rejects.toThrow('PromptRequest not found');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle global prompt (no projectId)', async () => {
|
||
|
|
const req = makePromptRequest({ id: 'req-2', name: 'global', content: 'stuff', projectId: null });
|
||
|
|
vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);
|
||
|
|
|
||
|
|
await service.approve('req-2');
|
||
|
|
|
||
|
|
// Should NOT include projectId in the create call
|
||
|
|
const createArg = vi.mocked(promptRepo.create).mock.calls[0]![0];
|
||
|
|
expect(createArg).not.toHaveProperty('projectId');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── Visibility ──
|
||
|
|
|
||
|
|
describe('getVisiblePrompts', () => {
|
||
|
|
it('should return approved prompts and session requests', async () => {
|
||
|
|
vi.mocked(promptRepo.findAll).mockResolvedValue([
|
||
|
|
makePrompt({ name: 'approved-1', content: 'A' }),
|
||
|
|
]);
|
||
|
|
vi.mocked(promptRequestRepo.findBySession).mockResolvedValue([
|
||
|
|
makePromptRequest({ name: 'pending-1', content: 'B' }),
|
||
|
|
]);
|
||
|
|
|
||
|
|
const result = await service.getVisiblePrompts('proj-1', 'sess-1');
|
||
|
|
|
||
|
|
expect(result).toHaveLength(2);
|
||
|
|
expect(result[0]).toEqual({ name: 'approved-1', content: 'A', type: 'prompt' });
|
||
|
|
expect(result[1]).toEqual({ name: 'pending-1', content: 'B', type: 'promptrequest' });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not include pending requests without sessionId', async () => {
|
||
|
|
vi.mocked(promptRepo.findAll).mockResolvedValue([makePrompt()]);
|
||
|
|
|
||
|
|
const result = await service.getVisiblePrompts('proj-1');
|
||
|
|
|
||
|
|
expect(result).toHaveLength(1);
|
||
|
|
expect(promptRequestRepo.findBySession).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return empty when no prompts or requests', async () => {
|
||
|
|
const result = await service.getVisiblePrompts();
|
||
|
|
expect(result).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|