Files
Codewalkers/apps/server/db/repositories/drizzle/initiative.test.ts
Lukas May 5137a60e70 feat: add quality_review task status and qualityReview initiative flag
Adds two new fields to the database and propagates them through the
repository layer:

- Task status enum gains 'quality_review' (between in_progress and
  completed), enabling a QA gate before tasks are marked complete.
- initiatives.quality_review (INTEGER DEFAULT 0) lets an initiative be
  flagged for quality-review workflow without a data migration (existing
  rows default to false).

Includes:
- Schema changes in schema.ts
- Migration 0037 (ALTER TABLE initiatives ADD quality_review)
- Snapshot chain repaired: deleted stale 0036 snapshot, fixed 0035
  prevId to create a linear chain (0032 → 0034 → 0035), then generated
  clean 0037 snapshot
- Repository adapter already uses SELECT * / spread-update pattern so
  no adapter code changes were needed
- Initiative and task repository tests extended with qualityReview /
  quality_review_status describe blocks (7 new tests)
- docs/database.md updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 21:47:34 +01:00

179 lines
5.7 KiB
TypeScript

/**
* DrizzleInitiativeRepository Tests
*
* Tests for the Initiative repository adapter.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { DrizzleInitiativeRepository } from './initiative.js';
import { createTestDatabase } from './test-helpers.js';
import type { DrizzleDatabase } from '../../index.js';
describe('DrizzleInitiativeRepository', () => {
let db: DrizzleDatabase;
let repo: DrizzleInitiativeRepository;
beforeEach(() => {
db = createTestDatabase();
repo = new DrizzleInitiativeRepository(db);
});
describe('create', () => {
it('should create an initiative with generated id and timestamps', async () => {
const initiative = await repo.create({
name: 'Test Initiative',
});
expect(initiative.id).toBeDefined();
expect(initiative.id.length).toBeGreaterThan(0);
expect(initiative.name).toBe('Test Initiative');
expect(initiative.status).toBe('active');
expect(initiative.createdAt).toBeInstanceOf(Date);
expect(initiative.updatedAt).toBeInstanceOf(Date);
});
it('should use provided status', async () => {
const initiative = await repo.create({
name: 'Completed Initiative',
status: 'completed',
});
expect(initiative.status).toBe('completed');
});
});
describe('findById', () => {
it('should return null for non-existent initiative', async () => {
const result = await repo.findById('non-existent-id');
expect(result).toBeNull();
});
it('should find an existing initiative', async () => {
const created = await repo.create({
name: 'Find Me',
});
const found = await repo.findById(created.id);
expect(found).not.toBeNull();
expect(found!.id).toBe(created.id);
expect(found!.name).toBe('Find Me');
});
});
describe('findAll', () => {
it('should return empty array initially', async () => {
const all = await repo.findAll();
expect(all).toEqual([]);
});
it('should return all initiatives', async () => {
await repo.create({ name: 'Initiative 1' });
await repo.create({ name: 'Initiative 2' });
await repo.create({ name: 'Initiative 3' });
const all = await repo.findAll();
expect(all.length).toBe(3);
});
});
describe('update', () => {
it('should update fields and updatedAt', async () => {
const created = await repo.create({
name: 'Original Name',
status: 'active',
});
// Small delay to ensure updatedAt differs
await new Promise((resolve) => setTimeout(resolve, 10));
const updated = await repo.update(created.id, {
name: 'Updated Name',
status: 'completed',
});
expect(updated.name).toBe('Updated Name');
expect(updated.status).toBe('completed');
expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(created.updatedAt.getTime());
});
it('should throw for non-existent initiative', async () => {
await expect(
repo.update('non-existent-id', { name: 'New Name' })
).rejects.toThrow('Initiative not found');
});
});
describe('delete', () => {
it('should delete an existing initiative', async () => {
const created = await repo.create({ name: 'To Delete' });
await repo.delete(created.id);
const found = await repo.findById(created.id);
expect(found).toBeNull();
});
it('should throw for non-existent initiative', async () => {
await expect(repo.delete('non-existent-id')).rejects.toThrow(
'Initiative not found'
);
});
});
describe('findByStatus', () => {
it('should return empty array for no matches', async () => {
await repo.create({ name: 'Active 1', status: 'active' });
const completed = await repo.findByStatus('completed');
expect(completed).toEqual([]);
});
it('should filter by status', async () => {
await repo.create({ name: 'Active 1', status: 'active' });
await repo.create({ name: 'Active 2', status: 'active' });
await repo.create({ name: 'Completed', status: 'completed' });
await repo.create({ name: 'Archived', status: 'archived' });
const active = await repo.findByStatus('active');
expect(active).toHaveLength(2);
expect(active.every((i) => i.status === 'active')).toBe(true);
const completed = await repo.findByStatus('completed');
expect(completed).toHaveLength(1);
expect(completed[0].name).toBe('Completed');
const archived = await repo.findByStatus('archived');
expect(archived).toHaveLength(1);
expect(archived[0].name).toBe('Archived');
});
});
describe('qualityReview', () => {
it('defaults to false on create', async () => {
const initiative = await repo.create({ name: 'QR Test' });
expect(initiative.qualityReview).toBe(false);
});
it('can be set to true via update', async () => {
const created = await repo.create({ name: 'QR Toggle' });
const updated = await repo.update(created.id, { qualityReview: true });
expect(updated.qualityReview).toBe(true);
});
it('round-trips through findById', async () => {
const created = await repo.create({ name: 'QR Round-trip' });
await repo.update(created.id, { qualityReview: true });
const found = await repo.findById(created.id);
expect(found!.qualityReview).toBe(true);
});
it('round-trips false value', async () => {
const created = await repo.create({ name: 'QR False' });
await repo.update(created.id, { qualityReview: true });
await repo.update(created.id, { qualityReview: false });
const found = await repo.findById(created.id);
expect(found!.qualityReview).toBe(false);
});
});
});