Files
FrenoCorp/server/trpc/revisions-router.test.ts
Michael Freno ccbf3039d9 FRE-594: Implement revision tracking and version history system
Add complete revision tracking system for scripts with:
- Database schema for revisions and revision_changes tables
- Diff engine with color-coded change types (addition/deletion/modification)
- tRPC router with 14 endpoints (create/list/compare/rollback/branch/merge)
- SolidJS components: RevisionTimeline, DiffViewer, RevisionReview
- Unit tests for diff engine and router

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-24 05:54:06 -04:00

235 lines
7.2 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { appRouter } from './index';
describe('revisionsRouter', () => {
const ctx = { userId: '123e4567-e89b-12d3-a456-426614174000' };
describe('createRevision', () => {
it('should create a revision with version 1', async () => {
const result = await appRouter.revisions.createRevision.mutate({
input: {
scriptId: 1,
title: 'Initial draft',
content: 'FADE IN:\n\nINT. ROOM - DAY',
authorId: 1,
},
ctx,
});
expect(result.versionNumber).toBe(1);
expect(result.branchName).toBe('main');
expect(result.status).toBe('draft');
});
it('should increment version number for same script', async () => {
await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'v1', content: 'content1', authorId: 1 },
ctx,
});
const result = await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'v2', content: 'content2', authorId: 1 },
ctx,
});
expect(result.versionNumber).toBe(2);
});
it('should support custom branch', async () => {
const result = await appRouter.revisions.createRevision.mutate({
input: {
scriptId: 1,
title: 'Branch revision',
content: 'branch content',
branchName: 'feature-act2',
authorId: 1,
},
ctx,
});
expect(result.branchName).toBe('feature-act2');
});
});
describe('listRevisions', () => {
it('should return empty array for unknown script', async () => {
const result = await appRouter.revisions.listRevisions.query({
input: { scriptId: 999 },
ctx,
});
expect(result).toEqual([]);
});
it('should filter by branch', async () => {
await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'main v1', content: 'main', branchName: 'main', authorId: 1 },
ctx,
});
await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'feature v1', content: 'feature', branchName: 'feature', authorId: 1 },
ctx,
});
const mainRevisions = await appRouter.revisions.listRevisions.query({
input: { scriptId: 1, branchName: 'main' },
ctx,
});
expect(mainRevisions).toHaveLength(1);
expect(mainRevisions[0]!.branchName).toBe('main');
});
});
describe('acceptRevision', () => {
it('should accept a revision', async () => {
const created = await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'To accept', content: 'content', authorId: 1 },
ctx,
});
const result = await appRouter.revisions.acceptRevision.mutate({
input: { revisionId: created.id, reviewedById: 2 },
ctx,
});
expect(result.status).toBe('accepted');
expect(result.reviewedById).toBe(2);
expect(result.reviewedAt).toBeDefined();
});
});
describe('rejectRevision', () => {
it('should reject a revision with reason', async () => {
const created = await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'To reject', content: 'content', authorId: 1 },
ctx,
});
const result = await appRouter.revisions.rejectRevision.mutate({
input: { revisionId: created.id, reviewedById: 2, reason: 'Needs more work on dialogue' },
ctx,
});
expect(result.status).toBe('rejected');
expect(result.summary).toContain('Needs more work on dialogue');
});
});
describe('rollbackToRevision', () => {
it('should create a new revision with old content', async () => {
const original = await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'Original', content: 'original content', authorId: 1 },
ctx,
});
await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'Changed', content: 'changed content', authorId: 1 },
ctx,
});
const rollback = await appRouter.revisions.rollbackToRevision.mutate({
input: { scriptId: 1, revisionId: original.id, authorId: 1 },
ctx,
});
expect(rollback.content).toBe('original content');
expect(rollback.versionNumber).toBe(3);
expect(rollback.title).toContain('Rollback');
});
});
describe('compareRevisions', () => {
it('should compare two revisions', async () => {
const rev1 = await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'v1', content: 'line1\nline2\nline3', authorId: 1 },
ctx,
});
const rev2 = await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'v2', content: 'line1\nchanged\nline3', authorId: 1 },
ctx,
});
const result = await appRouter.revisions.compareRevisions.query({
input: { baseRevisionId: rev1.id, targetRevisionId: rev2.id },
ctx,
});
expect(result.diff.modifications).toBe(1);
expect(result.diff.additions).toBe(0);
expect(result.diff.deletions).toBe(0);
});
});
describe('getTimeline', () => {
it('should return timeline entries in chronological order', async () => {
await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'First', content: 'first', authorId: 1 },
ctx,
});
await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'Second', content: 'second', authorId: 1 },
ctx,
});
const timeline = await appRouter.revisions.getTimeline.query({
input: { scriptId: 1 },
ctx,
});
expect(timeline).toHaveLength(2);
expect(timeline[0]!.revision.title).toBe('First');
expect(timeline[1]!.revision.title).toBe('Second');
});
});
describe('getBranches', () => {
it('should return branch information', async () => {
await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'main v1', content: 'main', authorId: 1 },
ctx,
});
await appRouter.revisions.createBranch.mutate({
input: { scriptId: 1, branchName: 'feature', authorId: 1 },
ctx,
});
const branches = await appRouter.revisions.getBranches.query({
input: { scriptId: 1 },
ctx,
});
expect(branches).toHaveLength(2);
const branchNames = branches.map((b: any) => b.branchName);
expect(branchNames).toContain('main');
expect(branchNames).toContain('feature');
});
});
describe('deleteRevision', () => {
it('should delete a revision', async () => {
const created = await appRouter.revisions.createRevision.mutate({
input: { scriptId: 1, title: 'To delete', content: 'content', authorId: 1 },
ctx,
});
const result = await appRouter.revisions.deleteRevision.mutate({
input: { id: created.id },
ctx,
});
expect(result.success).toBe(true);
await expect(
appRouter.revisions.getRevision.query({
input: { id: created.id },
ctx,
})
).rejects.toThrow();
});
});
});