feat(#82): implement Personality Module

- Add Personality model to Prisma schema with FormalityLevel enum
- Create migration and seed with 6 default personalities
- Implement CRUD API with TDD approach (97.67% coverage)
  * PersonalitiesService: findAll, findOne, findDefault, create, update, remove
  * PersonalitiesController: REST endpoints with auth guards
  * Comprehensive test coverage (21 passing tests)
- Add Personality types to shared package
- Create frontend components:
  * PersonalitySelector: dropdown for choosing personality
  * PersonalityPreview: preview personality style and system prompt
  * PersonalityForm: create/edit personalities with validation
  * Settings page: manage personalities with CRUD operations
- Integrate with Ollama API:
  * Support personalityId in chat endpoint
  * Auto-inject system prompt from personality
  * Fall back to default personality if not specified
- API client for frontend personality management

All tests passing with 97.67% backend coverage (exceeds 85% requirement)
This commit is contained in:
Jason Woltje
2026-01-29 17:57:54 -06:00
parent 95833fb4ea
commit 5dd46c85af
43 changed files with 4782 additions and 2 deletions

View File

@@ -0,0 +1,122 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { WebSocketProvider, useWebSocketContext } from './WebSocketProvider';
import * as useWebSocketModule from '../hooks/useWebSocket';
// Mock the useWebSocket hook
vi.mock('../hooks/useWebSocket');
describe('WebSocketProvider', () => {
it('should provide WebSocket context to children', () => {
const mockUseWebSocket = vi.spyOn(useWebSocketModule, 'useWebSocket');
mockUseWebSocket.mockReturnValue({
isConnected: true,
socket: null,
});
function TestComponent(): React.JSX.Element {
const { isConnected } = useWebSocketContext();
return <div>{isConnected ? 'Connected' : 'Disconnected'}</div>;
}
render(
<WebSocketProvider workspaceId="workspace-123" token="auth-token">
<TestComponent />
</WebSocketProvider>
);
expect(screen.getByText('Connected')).toBeInTheDocument();
});
it('should pass callbacks to useWebSocket hook', () => {
const mockUseWebSocket = vi.spyOn(useWebSocketModule, 'useWebSocket');
mockUseWebSocket.mockReturnValue({
isConnected: false,
socket: null,
});
const onTaskCreated = vi.fn();
const onTaskUpdated = vi.fn();
const onTaskDeleted = vi.fn();
render(
<WebSocketProvider
workspaceId="workspace-123"
token="auth-token"
onTaskCreated={onTaskCreated}
onTaskUpdated={onTaskUpdated}
onTaskDeleted={onTaskDeleted}
>
<div>Test</div>
</WebSocketProvider>
);
expect(mockUseWebSocket).toHaveBeenCalledWith(
'workspace-123',
'auth-token',
{
onTaskCreated,
onTaskUpdated,
onTaskDeleted,
onEventCreated: undefined,
onEventUpdated: undefined,
onEventDeleted: undefined,
onProjectUpdated: undefined,
}
);
});
it('should throw error when useWebSocketContext is used outside provider', () => {
function TestComponent(): React.JSX.Element {
useWebSocketContext();
return <div>Test</div>;
}
// Suppress console.error for this test
const originalError = console.error;
console.error = vi.fn();
expect(() => {
render(<TestComponent />);
}).toThrow('useWebSocketContext must be used within WebSocketProvider');
console.error = originalError;
});
it('should update context when connection status changes', () => {
const mockUseWebSocket = vi.spyOn(useWebSocketModule, 'useWebSocket');
// Initially disconnected
mockUseWebSocket.mockReturnValue({
isConnected: false,
socket: null,
});
function TestComponent(): React.JSX.Element {
const { isConnected } = useWebSocketContext();
return <div data-testid="status">{isConnected ? 'Connected' : 'Disconnected'}</div>;
}
const { rerender } = render(
<WebSocketProvider workspaceId="workspace-123" token="auth-token">
<TestComponent />
</WebSocketProvider>
);
expect(screen.getByTestId('status')).toHaveTextContent('Disconnected');
// Update to connected
mockUseWebSocket.mockReturnValue({
isConnected: true,
socket: null,
});
rerender(
<WebSocketProvider workspaceId="workspace-123" token="auth-token">
<TestComponent />
</WebSocketProvider>
);
expect(screen.getByTestId('status')).toHaveTextContent('Connected');
});
});

View File

@@ -0,0 +1,94 @@
import React, { createContext, useContext, ReactNode } from 'react';
import { useWebSocket } from '../hooks/useWebSocket';
import { Socket } from 'socket.io-client';
interface Task {
id: string;
[key: string]: unknown;
}
interface Event {
id: string;
[key: string]: unknown;
}
interface Project {
id: string;
[key: string]: unknown;
}
interface DeletePayload {
id: string;
}
interface WebSocketContextValue {
isConnected: boolean;
socket: Socket | null;
}
interface WebSocketProviderProps {
workspaceId: string;
token: string;
onTaskCreated?: (task: Task) => void;
onTaskUpdated?: (task: Task) => void;
onTaskDeleted?: (payload: DeletePayload) => void;
onEventCreated?: (event: Event) => void;
onEventUpdated?: (event: Event) => void;
onEventDeleted?: (payload: DeletePayload) => void;
onProjectUpdated?: (project: Project) => void;
children: ReactNode;
}
const WebSocketContext = createContext<WebSocketContextValue | undefined>(undefined);
/**
* WebSocket Provider component
* Manages WebSocket connection and provides context to children
*/
export function WebSocketProvider({
workspaceId,
token,
onTaskCreated,
onTaskUpdated,
onTaskDeleted,
onEventCreated,
onEventUpdated,
onEventDeleted,
onProjectUpdated,
children,
}: WebSocketProviderProps): React.JSX.Element {
const { isConnected, socket } = useWebSocket(workspaceId, token, {
onTaskCreated: onTaskCreated ?? undefined,
onTaskUpdated: onTaskUpdated ?? undefined,
onTaskDeleted: onTaskDeleted ?? undefined,
onEventCreated: onEventCreated ?? undefined,
onEventUpdated: onEventUpdated ?? undefined,
onEventDeleted: onEventDeleted ?? undefined,
onProjectUpdated: onProjectUpdated ?? undefined,
});
const value: WebSocketContextValue = {
isConnected,
socket,
};
return (
<WebSocketContext.Provider value={value}>
{children}
</WebSocketContext.Provider>
);
}
/**
* Hook to access WebSocket context
* @throws Error if used outside WebSocketProvider
*/
export function useWebSocketContext(): WebSocketContextValue {
const context = useContext(WebSocketContext);
if (context === undefined) {
throw new Error('useWebSocketContext must be used within WebSocketProvider');
}
return context;
}