- `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>
162 lines
5.4 KiB
TypeScript
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"]');
|
|
});
|
|
});
|
|
});
|