Files
Codewalkers/apps/server/db/repositories/drizzle/errand.test.ts
Lukas May 813979388b feat: wire conflictFiles through errand.get and add repository tests
- `errand.get` now returns `conflictFiles: string[]` (always an array,
  never null) with defensive JSON.parse error handling
- `errand.get` returns `projectPath: string | null` computed from
  workspaceRoot + getProjectCloneDir so cw errand resolve can locate
  the worktree without a second tRPC call
- Add `apps/server/db/repositories/drizzle/errand.test.ts` covering
  conflictFiles store/retrieve, null for non-conflict errands, and
  findAll including conflictFiles
- Update `errand.test.ts` mock to include getProjectCloneDir and fix
  conflictFiles expectation from null to []

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 16:28:56 +01:00

162 lines
5.4 KiB
TypeScript

/**
* DrizzleErrandRepository Tests
*
* Tests for the Errand repository adapter.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { DrizzleErrandRepository } from './errand.js';
import { DrizzleProjectRepository } from './project.js';
import { createTestDatabase } from './test-helpers.js';
import type { DrizzleDatabase } from '../../index.js';
import type { Project } from '../../schema.js';
describe('DrizzleErrandRepository', () => {
let db: DrizzleDatabase;
let repo: DrizzleErrandRepository;
let projectRepo: DrizzleProjectRepository;
const createProject = async (): Promise<Project> => {
const suffix = Math.random().toString(36).slice(2, 8);
return projectRepo.create({
name: `test-project-${suffix}`,
url: `https://github.com/test/repo-${suffix}`,
});
};
beforeEach(() => {
db = createTestDatabase();
repo = new DrizzleErrandRepository(db);
projectRepo = new DrizzleProjectRepository(db);
});
describe('create', () => {
it('creates an errand with generated id and timestamps', async () => {
const project = await createProject();
const errand = await repo.create({
description: 'fix typo',
branch: 'cw/errand/fix-typo-abc12345',
baseBranch: 'main',
agentId: null,
projectId: project.id,
status: 'active',
});
expect(errand.id).toBeDefined();
expect(errand.id.length).toBeGreaterThan(0);
expect(errand.description).toBe('fix typo');
expect(errand.branch).toBe('cw/errand/fix-typo-abc12345');
expect(errand.baseBranch).toBe('main');
expect(errand.agentId).toBeNull();
expect(errand.projectId).toBe(project.id);
expect(errand.status).toBe('active');
expect(errand.conflictFiles).toBeNull();
expect(errand.createdAt).toBeInstanceOf(Date);
expect(errand.updatedAt).toBeInstanceOf(Date);
});
});
describe('findById', () => {
it('returns null for non-existent errand', async () => {
const result = await repo.findById('does-not-exist');
expect(result).toBeNull();
});
it('returns errand with agentAlias null when no agent', async () => {
const project = await createProject();
const created = await repo.create({
description: 'test',
branch: 'cw/errand/test-xyz',
baseBranch: 'main',
agentId: null,
projectId: project.id,
status: 'active',
});
const found = await repo.findById(created.id);
expect(found).not.toBeNull();
expect(found!.agentAlias).toBeNull();
});
});
describe('findAll', () => {
it('returns empty array when no errands', async () => {
const results = await repo.findAll();
expect(results).toEqual([]);
});
it('filters by projectId', async () => {
const projectA = await createProject();
const projectB = await createProject();
await repo.create({ description: 'a', branch: 'cw/errand/a', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active' });
await repo.create({ description: 'b', branch: 'cw/errand/b', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'active' });
const results = await repo.findAll({ projectId: projectA.id });
expect(results).toHaveLength(1);
expect(results[0].description).toBe('a');
});
});
describe('update', () => {
it('updates errand status', async () => {
const project = await createProject();
const created = await repo.create({
description: 'upd test',
branch: 'cw/errand/upd',
baseBranch: 'main',
agentId: null,
projectId: project.id,
status: 'active',
});
const updated = await repo.update(created.id, { status: 'pending_review' });
expect(updated!.status).toBe('pending_review');
});
});
describe('conflictFiles column', () => {
it('stores and retrieves conflictFiles via update + findById', async () => {
const project = await createProject();
const created = await repo.create({
description: 'x',
branch: 'cw/errand/x',
baseBranch: 'main',
agentId: null,
projectId: project.id,
status: 'active',
});
await repo.update(created.id, { status: 'conflict', conflictFiles: '["src/a.ts","src/b.ts"]' });
const found = await repo.findById(created.id);
expect(found!.conflictFiles).toBe('["src/a.ts","src/b.ts"]');
expect(found!.status).toBe('conflict');
});
it('returns null conflictFiles for non-conflict errands', async () => {
const project = await createProject();
const created = await repo.create({
description: 'y',
branch: 'cw/errand/y',
baseBranch: 'main',
agentId: null,
projectId: project.id,
status: 'active',
});
const found = await repo.findById(created.id);
expect(found!.conflictFiles).toBeNull();
});
it('findAll includes conflictFiles in results', async () => {
const project = await createProject();
const created = await repo.create({
description: 'z',
branch: 'cw/errand/z',
baseBranch: 'main',
agentId: null,
projectId: project.id,
status: 'active',
});
await repo.update(created.id, { conflictFiles: '["x.ts"]' });
const all = await repo.findAll({ projectId: project.id });
expect(all[0].conflictFiles).toBe('["x.ts"]');
});
});
});