import { describe, it, expect, vi } from 'vitest'; import { createMissionTasksRepo } from './mission-tasks.js'; /** * SHARED-CONTRACT §5.5 "mission_tasks.status write prohibition": this repo is * the sole path that authors mission_tasks.status from caller input (storage * tier migration is row transport and preserves stored values; the generic * storage adapters have no mission_tasks caller), and it must never forward a * caller-supplied status to the database on create or update. Callers keep * working (the field is accepted and ignored), so these tests assert on what * reaches the Drizzle chain, not on rejection. */ function makeInsertDb(returned: unknown[]) { const values = vi.fn((_v: unknown) => ({ returning: vi.fn().mockResolvedValue(returned) })); return { db: { insert: vi.fn(() => ({ values })) }, values }; } function makeUpdateDb(returned: unknown[]) { const set = vi.fn((_v: unknown) => ({ where: vi.fn(() => ({ returning: vi.fn().mockResolvedValue(returned) })), })); return { db: { update: vi.fn(() => ({ set })) }, set }; } describe('createMissionTasksRepo — status write prohibition', () => { it('create strips a caller-supplied status before insert', async () => { const { db, values } = makeInsertDb([{ id: 'mt1', status: 'not-started' }]); const repo = createMissionTasksRepo(db as never); const result = await repo.create({ missionId: 'm1', userId: 'u1', status: 'done', description: 'd', } as never); expect(values).toHaveBeenCalledTimes(1); const inserted = values.mock.calls[0]![0] as Record; expect('status' in inserted).toBe(false); expect(inserted.missionId).toBe('m1'); expect(inserted.description).toBe('d'); expect(result.id).toBe('mt1'); }); it('create without status still inserts (DB default applies)', async () => { const { db, values } = makeInsertDb([{ id: 'mt2' }]); const repo = createMissionTasksRepo(db as never); await repo.create({ missionId: 'm1', userId: 'u1' } as never); const inserted = values.mock.calls[0]![0] as Record; expect('status' in inserted).toBe(false); }); it('update strips a caller-supplied status but keeps the other fields', async () => { const { db, set } = makeUpdateDb([{ id: 'mt1', notes: 'n' }]); const repo = createMissionTasksRepo(db as never); const result = await repo.update('mt1', { status: 'done', notes: 'n' } as never); expect(set).toHaveBeenCalledTimes(1); const updated = set.mock.calls[0]![0] as Record; expect('status' in updated).toBe(false); expect(updated.notes).toBe('n'); expect(updated.updatedAt).toBeInstanceOf(Date); expect(result?.id).toBe('mt1'); }); it('update with only status degenerates to a timestamp-only update', async () => { const { db, set } = makeUpdateDb([{ id: 'mt1' }]); const repo = createMissionTasksRepo(db as never); await repo.update('mt1', { status: 'blocked' } as never); const updated = set.mock.calls[0]![0] as Record; expect(Object.keys(updated)).toEqual(['updatedAt']); }); });