Merge branch 'cw/small-change-flow-task-3yaqNLqG3kk5ku6rH0exF' into cw-merge-1772810964768
This commit is contained in:
161
apps/server/db/repositories/drizzle/errand.test.ts
Normal file
161
apps/server/db/repositories/drizzle/errand.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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"]');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,7 @@ vi.mock('../../git/manager.js', () => ({
|
||||
|
||||
vi.mock('../../git/project-clones.js', () => ({
|
||||
ensureProjectClone: mockEnsureProjectClone,
|
||||
getProjectCloneDir: vi.fn().mockReturnValue('repos/test-project-abc123'),
|
||||
}));
|
||||
|
||||
vi.mock('../../agent/file-io.js', async (importOriginal) => {
|
||||
@@ -393,7 +394,7 @@ describe('errand procedures', () => {
|
||||
|
||||
expect(result.id).toBe(errand.id);
|
||||
expect(result).toHaveProperty('agentAlias');
|
||||
expect(result.conflictFiles).toBeNull();
|
||||
expect(result.conflictFiles).toEqual([]);
|
||||
});
|
||||
|
||||
it('parses conflictFiles JSON when present', async () => {
|
||||
|
||||
@@ -18,8 +18,9 @@ import {
|
||||
} from './_helpers.js';
|
||||
import { writeErrandManifest } from '../../agent/file-io.js';
|
||||
import { buildErrandPrompt } from '../../agent/prompts/index.js';
|
||||
import { join } from 'node:path';
|
||||
import { SimpleGitWorktreeManager } from '../../git/manager.js';
|
||||
import { ensureProjectClone } from '../../git/project-clones.js';
|
||||
import { ensureProjectClone, getProjectCloneDir } from '../../git/project-clones.js';
|
||||
import type { TRPCContext } from '../context.js';
|
||||
|
||||
// ErrandStatus values for input validation
|
||||
@@ -200,10 +201,27 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
if (!errand) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand not found' });
|
||||
}
|
||||
return {
|
||||
...errand,
|
||||
conflictFiles: errand.conflictFiles ? (JSON.parse(errand.conflictFiles) as string[]) : null,
|
||||
};
|
||||
|
||||
// Parse conflictFiles; return [] on null or malformed JSON
|
||||
let conflictFiles: string[] = [];
|
||||
if (errand.conflictFiles) {
|
||||
try {
|
||||
conflictFiles = JSON.parse(errand.conflictFiles) as string[];
|
||||
} catch {
|
||||
conflictFiles = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Compute project clone path for cw errand resolve
|
||||
let projectPath: string | null = null;
|
||||
if (errand.projectId && ctx.workspaceRoot) {
|
||||
const project = await requireProjectRepository(ctx).findById(errand.projectId);
|
||||
if (project) {
|
||||
projectPath = join(ctx.workspaceRoot, getProjectCloneDir(project.name, project.id));
|
||||
}
|
||||
}
|
||||
|
||||
return { ...errand, conflictFiles, projectPath };
|
||||
}),
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user