Files
Codewalkers/apps/server/db/repositories/drizzle/message.test.ts
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt
standard monorepo conventions (apps/ for runnable apps, packages/
for reusable libraries). Update all config files, shared package
imports, test fixtures, and documentation to reflect new paths.

Key fixes:
- Update workspace config to ["apps/*", "packages/*"]
- Update tsconfig.json rootDir/include for apps/server/
- Add apps/web/** to vitest exclude list
- Update drizzle.config.ts schema path
- Fix ensure-schema.ts migration path detection (3 levels up in dev,
  2 levels up in dist)
- Fix tests/integration/cli-server.test.ts import paths
- Update packages/shared imports to apps/server/ paths
- Update all docs/ files with new paths
2026-03-03 11:22:53 +01:00

457 lines
14 KiB
TypeScript

/**
* DrizzleMessageRepository Tests
*
* Tests for the Message repository adapter.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { DrizzleMessageRepository } from './message.js';
import { DrizzleAgentRepository } from './agent.js';
import { DrizzleTaskRepository } from './task.js';
import { DrizzlePhaseRepository } from './phase.js';
import { DrizzleInitiativeRepository } from './initiative.js';
import { createTestDatabase } from './test-helpers.js';
import type { DrizzleDatabase } from '../../index.js';
describe('DrizzleMessageRepository', () => {
let db: DrizzleDatabase;
let messageRepo: DrizzleMessageRepository;
let agentRepo: DrizzleAgentRepository;
let testAgentId: string;
beforeEach(async () => {
db = createTestDatabase();
messageRepo = new DrizzleMessageRepository(db);
agentRepo = new DrizzleAgentRepository(db);
// Create required hierarchy for agent FK
const taskRepo = new DrizzleTaskRepository(db);
const phaseRepo = new DrizzlePhaseRepository(db);
const initiativeRepo = new DrizzleInitiativeRepository(db);
const initiative = await initiativeRepo.create({
name: 'Test Initiative',
});
const phase = await phaseRepo.create({
initiativeId: initiative.id,
name: 'Test Phase',
});
const task = await taskRepo.create({
phaseId: phase.id,
name: 'Test Task',
order: 1,
});
// Create a test agent
const agent = await agentRepo.create({
name: 'test-agent',
worktreeId: 'worktree-123',
taskId: task.id,
});
testAgentId = agent.id;
});
describe('create', () => {
it('should create agent→user message (question)', async () => {
const message = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
type: 'question',
content: 'Do you want to proceed with deployment?',
requiresResponse: true,
});
expect(message.id).toBeDefined();
expect(message.id.length).toBeGreaterThan(0);
expect(message.senderType).toBe('agent');
expect(message.senderId).toBe(testAgentId);
expect(message.recipientType).toBe('user');
expect(message.recipientId).toBeNull();
expect(message.type).toBe('question');
expect(message.content).toBe('Do you want to proceed with deployment?');
expect(message.requiresResponse).toBe(true);
expect(message.status).toBe('pending');
expect(message.parentMessageId).toBeNull();
expect(message.createdAt).toBeInstanceOf(Date);
expect(message.updatedAt).toBeInstanceOf(Date);
});
it('should create agent→user message (notification, requiresResponse=false)', async () => {
const message = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
type: 'info',
content: 'Build completed successfully.',
requiresResponse: false,
});
expect(message.type).toBe('info');
expect(message.requiresResponse).toBe(false);
expect(message.status).toBe('pending');
});
it('should create user→agent response', async () => {
// First create the question
const question = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
type: 'question',
content: 'Which database?',
requiresResponse: true,
});
// Then create user response
const response = await messageRepo.create({
senderType: 'user',
recipientType: 'agent',
recipientId: testAgentId,
type: 'response',
content: 'Use PostgreSQL',
parentMessageId: question.id,
});
expect(response.senderType).toBe('user');
expect(response.senderId).toBeNull();
expect(response.recipientType).toBe('agent');
expect(response.recipientId).toBe(testAgentId);
expect(response.parentMessageId).toBe(question.id);
});
it('should default type to info', async () => {
const message = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'Status update',
});
expect(message.type).toBe('info');
});
});
describe('findById', () => {
it('should return null for non-existent message', async () => {
const result = await messageRepo.findById('non-existent-id');
expect(result).toBeNull();
});
it('should find an existing message', async () => {
const created = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'Test message',
});
const found = await messageRepo.findById(created.id);
expect(found).not.toBeNull();
expect(found!.id).toBe(created.id);
expect(found!.content).toBe('Test message');
});
});
describe('findBySender', () => {
it('should find messages by agent sender', async () => {
await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'Message 1',
});
await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'Message 2',
});
const messages = await messageRepo.findBySender('agent', testAgentId);
expect(messages.length).toBe(2);
});
it('should find messages by user sender', async () => {
await messageRepo.create({
senderType: 'user',
recipientType: 'agent',
recipientId: testAgentId,
content: 'User message 1',
});
await messageRepo.create({
senderType: 'user',
recipientType: 'agent',
recipientId: testAgentId,
content: 'User message 2',
});
const messages = await messageRepo.findBySender('user');
expect(messages.length).toBe(2);
});
it('should return empty array when no messages from sender', async () => {
const messages = await messageRepo.findBySender('agent', 'nonexistent-id');
expect(messages).toEqual([]);
});
});
describe('findByRecipient', () => {
it('should find messages by user recipient', async () => {
await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'For user 1',
});
await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'For user 2',
});
const messages = await messageRepo.findByRecipient('user');
expect(messages.length).toBe(2);
});
it('should find messages by agent recipient', async () => {
await messageRepo.create({
senderType: 'user',
recipientType: 'agent',
recipientId: testAgentId,
content: 'For agent',
});
const messages = await messageRepo.findByRecipient('agent', testAgentId);
expect(messages.length).toBe(1);
});
it('should return empty array when no messages for recipient', async () => {
const messages = await messageRepo.findByRecipient('agent', 'nonexistent-id');
expect(messages).toEqual([]);
});
});
describe('findPendingForUser', () => {
it('should return only user-targeted pending messages', async () => {
// Create pending message for user
await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'Pending for user',
status: 'pending',
});
// Create read message for user (should not be returned)
const readMessage = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'Already read',
});
await messageRepo.update(readMessage.id, { status: 'read' });
// Create pending message for agent (should not be returned)
await messageRepo.create({
senderType: 'user',
recipientType: 'agent',
recipientId: testAgentId,
content: 'For agent not user',
});
const pending = await messageRepo.findPendingForUser();
expect(pending.length).toBe(1);
expect(pending[0].content).toBe('Pending for user');
});
it('should return empty array when no pending messages for user', async () => {
const pending = await messageRepo.findPendingForUser();
expect(pending).toEqual([]);
});
});
describe('findRequiringResponse', () => {
it('should return only messages needing response', async () => {
// Create message requiring response
await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
type: 'question',
content: 'Requires answer',
requiresResponse: true,
});
// Create message not requiring response
await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
type: 'info',
content: 'Just info',
requiresResponse: false,
});
// Create message that required response but was responded
const responded = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
type: 'question',
content: 'Already answered',
requiresResponse: true,
});
await messageRepo.update(responded.id, { status: 'responded' });
const requiring = await messageRepo.findRequiringResponse();
expect(requiring.length).toBe(1);
expect(requiring[0].content).toBe('Requires answer');
});
it('should return empty array when no messages require response', async () => {
const requiring = await messageRepo.findRequiringResponse();
expect(requiring).toEqual([]);
});
});
describe('findReplies (message threading)', () => {
it('should find all replies to a parent message', async () => {
// Create original question
const question = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
type: 'question',
content: 'Original question',
requiresResponse: true,
});
// Create two replies
await messageRepo.create({
senderType: 'user',
recipientType: 'agent',
recipientId: testAgentId,
type: 'response',
content: 'First reply',
parentMessageId: question.id,
});
await messageRepo.create({
senderType: 'user',
recipientType: 'agent',
recipientId: testAgentId,
type: 'response',
content: 'Second reply',
parentMessageId: question.id,
});
// Create unrelated message (should not be in replies)
await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'Unrelated message',
});
const replies = await messageRepo.findReplies(question.id);
expect(replies.length).toBe(2);
expect(replies.every((r) => r.parentMessageId === question.id)).toBe(true);
});
it('should return empty array when message has no replies', async () => {
const message = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'No replies',
});
const replies = await messageRepo.findReplies(message.id);
expect(replies).toEqual([]);
});
});
describe('update status flow', () => {
it('should update status: pending → read → responded', async () => {
const message = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
type: 'question',
content: 'Status flow test',
requiresResponse: true,
});
expect(message.status).toBe('pending');
// Wait to ensure different timestamps
await new Promise((resolve) => setTimeout(resolve, 10));
// Update to read
const readMessage = await messageRepo.update(message.id, { status: 'read' });
expect(readMessage.status).toBe('read');
expect(readMessage.updatedAt.getTime()).toBeGreaterThanOrEqual(message.updatedAt.getTime());
// Wait again
await new Promise((resolve) => setTimeout(resolve, 10));
// Update to responded
const respondedMessage = await messageRepo.update(readMessage.id, {
status: 'responded',
});
expect(respondedMessage.status).toBe('responded');
expect(respondedMessage.updatedAt.getTime()).toBeGreaterThanOrEqual(
readMessage.updatedAt.getTime()
);
});
});
describe('update', () => {
it('should throw for non-existent message', async () => {
await expect(
messageRepo.update('non-existent-id', { status: 'read' })
).rejects.toThrow('Message not found');
});
it('should update content and updatedAt', async () => {
const created = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'Original content',
});
await new Promise((resolve) => setTimeout(resolve, 10));
const updated = await messageRepo.update(created.id, {
content: 'Updated content',
});
expect(updated.content).toBe('Updated content');
expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(created.updatedAt.getTime());
});
});
describe('delete', () => {
it('should delete an existing message', async () => {
const created = await messageRepo.create({
senderType: 'agent',
senderId: testAgentId,
recipientType: 'user',
content: 'To delete',
});
await messageRepo.delete(created.id);
const found = await messageRepo.findById(created.id);
expect(found).toBeNull();
});
it('should throw for non-existent message', async () => {
await expect(messageRepo.delete('non-existent-id')).rejects.toThrow(
'Message not found'
);
});
});
});