Files
FrenoCorp/server/trpc/character-router.test.ts
FrenoCorp Agent 8dc4827597 FRE-592: Implement character database and relationship mapping
Add full character management system with enriched profiles (bio, traits,
arcs, motivation, conflict, secrets), relationship mapping between
characters with types and strength, character search/filter by role and
arc type, and character statistics (scene count, dialogue, screen time).

Includes database schema, tRPC router procedures, SolidJS components,
API hooks, and unit tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-24 02:24:31 -04:00

257 lines
7.3 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { appRouter } from './index';
describe('tRPC API Layer - Character System', () => {
let ctx: { userId: string };
let projectId: string;
beforeEach(async () => {
ctx = { userId: '123e4567-e89b-12d3-a456-426614174000' };
const project = await appRouter.project.createProject.mutate({
input: { name: 'Character System Test Project' },
ctx,
});
projectId = project.id;
});
describe('createCharacter', () => {
it('should create a character with all profile fields', async () => {
const character = await appRouter.project.createCharacter.mutate({
input: {
name: 'John Doe',
bio: 'A brave hero',
role: 'protagonist',
arc: 'Grows from coward to leader',
arcType: 'positive',
age: 30,
gender: 'male',
voice: 'Deep, commanding',
traits: 'Brave, loyal, stubborn',
motivation: 'Protect his family',
conflict: 'Internal fear of failure',
secret: 'Afraid of heights',
projectId,
},
ctx,
});
expect(character).toMatchObject({
name: 'John Doe',
bio: 'A brave hero',
role: 'protagonist',
arcType: 'positive',
age: 30,
projectId,
});
expect(character.slug).toBe('john-doe');
});
it('should default role to supporting when not provided', async () => {
const character = await appRouter.project.createCharacter.mutate({
input: {
name: 'Jane Smith',
projectId,
},
ctx,
});
expect(character.role).toBe('supporting');
});
});
describe('updateCharacter', () => {
it('should update character profile fields', async () => {
const created = await appRouter.project.createCharacter.mutate({
input: { name: 'Original', projectId },
ctx,
});
const updated = await appRouter.project.updateCharacter.mutate({
input: {
id: created.id,
name: 'Updated Name',
bio: 'New bio',
role: 'antagonist',
},
ctx,
});
expect(updated.name).toBe('Updated Name');
expect(updated.slug).toBe('updated-name');
expect(updated.bio).toBe('New bio');
expect(updated.role).toBe('antagonist');
});
});
describe('searchCharacters', () => {
it('should filter characters by query', async () => {
await appRouter.project.createCharacter.mutate({
input: { name: 'Alice', bio: 'The hero', projectId },
ctx,
});
await appRouter.project.createCharacter.mutate({
input: { name: 'Bob', bio: 'The villain', projectId },
ctx,
});
const results = await appRouter.project.searchCharacters.query({
input: { projectId, query: 'hero' },
ctx,
});
expect(results.length).toBe(1);
expect(results[0].name).toBe('Alice');
});
it('should filter characters by role', async () => {
await appRouter.project.createCharacter.mutate({
input: { name: 'Protag', role: 'protagonist', projectId },
ctx,
});
await appRouter.project.createCharacter.mutate({
input: { name: 'Antag', role: 'antagonist', projectId },
ctx,
});
const results = await appRouter.project.searchCharacters.query({
input: { projectId, role: 'protagonist' },
ctx,
});
expect(results.length).toBe(1);
expect(results[0].name).toBe('Protag');
});
});
describe('createRelationship', () => {
it('should create a relationship between two characters', async () => {
const charA = await appRouter.project.createCharacter.mutate({
input: { name: 'Character A', projectId },
ctx,
});
const charB = await appRouter.project.createCharacter.mutate({
input: { name: 'Character B', projectId },
ctx,
});
const rel = await appRouter.project.createRelationship.mutate({
input: {
characterIdA: charA.id,
characterIdB: charB.id,
relationshipType: 'friendship',
strength: 80,
isAntagonistic: false,
},
ctx,
});
expect(rel.characterIdA).toBe(charA.id);
expect(rel.characterIdB).toBe(charB.id);
expect(rel.relationshipType).toBe('friendship');
expect(rel.strength).toBe(80);
});
it('should prevent self-relationships', async () => {
const charA = await appRouter.project.createCharacter.mutate({
input: { name: 'Character A', projectId },
ctx,
});
await expect(
appRouter.project.createRelationship.mutate({
input: {
characterIdA: charA.id,
characterIdB: charA.id,
relationshipType: 'friendship',
},
ctx,
})
).rejects.toThrow('Cannot create a relationship with the same character');
});
it('should prevent duplicate relationships', async () => {
const charA = await appRouter.project.createCharacter.mutate({
input: { name: 'Character A', projectId },
ctx,
});
const charB = await appRouter.project.createCharacter.mutate({
input: { name: 'Character B', projectId },
ctx,
});
await appRouter.project.createRelationship.mutate({
input: {
characterIdA: charA.id,
characterIdB: charB.id,
relationshipType: 'friendship',
},
ctx,
});
await expect(
appRouter.project.createRelationship.mutate({
input: {
characterIdA: charA.id,
characterIdB: charB.id,
relationshipType: 'rivalry',
},
ctx,
})
).rejects.toThrow('Relationship already exists between these characters');
});
});
describe('deleteCharacter', () => {
it('should remove associated relationships when deleting a character', async () => {
const charA = await appRouter.project.createCharacter.mutate({
input: { name: 'Character A', projectId },
ctx,
});
const charB = await appRouter.project.createCharacter.mutate({
input: { name: 'Character B', projectId },
ctx,
});
await appRouter.project.createRelationship.mutate({
input: {
characterIdA: charA.id,
characterIdB: charB.id,
relationshipType: 'friendship',
},
ctx,
});
await appRouter.project.deleteCharacter.mutate({
input: { id: charA.id },
ctx,
});
const rels = await appRouter.project.getRelationshipsForCharacter.query({
input: { characterId: charB.id },
ctx,
});
expect(rels.length).toBe(0);
});
});
describe('getCharacterStats', () => {
it('should return stats for a character', async () => {
const charA = await appRouter.project.createCharacter.mutate({
input: { name: 'TestChar', projectId },
ctx,
});
const stats = await appRouter.project.getCharacterStats.query({
input: { characterId: charA.id },
ctx,
});
expect(stats.characterId).toBe(charA.id);
expect(stats.sceneCount).toBe(0);
expect(stats.totalDialogueLines).toBe(0);
expect(stats.relationshipCount).toBe(0);
});
});
});