|
@@ -0,0 +1,114 @@
|
|
|
|
|
+import { describe, it, expect } from 'vitest';
|
|
|
|
|
+import {
|
|
|
|
|
+ createEventNode,
|
|
|
|
|
+ reorderNarrative,
|
|
|
|
|
+ assignEventsToChapter,
|
|
|
|
|
+ validateEventGraph,
|
|
|
|
|
+} from '@/app/lib/event-graph-utils';
|
|
|
|
|
+import type { EventNode, EventEdge } from '@/app/lib/novel-types';
|
|
|
|
|
+
|
|
|
|
|
+function makeEvent(overrides: Partial<EventNode> = {}): EventNode {
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: 'evt-1',
|
|
|
|
|
+ title: 'Test Event',
|
|
|
|
|
+ description: 'A test event',
|
|
|
|
|
+ timeLabel: 'Day 1',
|
|
|
|
|
+ location: 'Test Location',
|
|
|
|
|
+ characters: ['主角'],
|
|
|
|
|
+ eventType: 'main',
|
|
|
|
|
+ chronologicalOrder: 1,
|
|
|
|
|
+ narrativeOrder: 1,
|
|
|
|
|
+ parentEventIds: [],
|
|
|
|
|
+ tags: [],
|
|
|
|
|
+ status: 'pending',
|
|
|
|
|
+ ...overrides,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+describe('createEventNode', () => {
|
|
|
|
|
+ it('generates a UUID and sets default values', () => {
|
|
|
|
|
+ const node = createEventNode({ title: 'New Event' });
|
|
|
|
|
+ expect(node.id).toMatch(/^[0-9a-f-]{36}$/);
|
|
|
|
|
+ expect(node.title).toBe('New Event');
|
|
|
|
|
+ expect(node.status).toBe('pending');
|
|
|
|
|
+ expect(node.eventType).toBe('main');
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+describe('reorderNarrative', () => {
|
|
|
|
|
+ it('updates narrativeOrder for each event by its new index', () => {
|
|
|
|
|
+ const events = [
|
|
|
|
|
+ makeEvent({ id: 'a', narrativeOrder: 0 }),
|
|
|
|
|
+ makeEvent({ id: 'b', narrativeOrder: 1 }),
|
|
|
|
|
+ makeEvent({ id: 'c', narrativeOrder: 2 }),
|
|
|
|
|
+ ];
|
|
|
|
|
+ const result = reorderNarrative(events, ['c', 'a', 'b']);
|
|
|
|
|
+ expect(result.find(e => e.id === 'c')!.narrativeOrder).toBe(0);
|
|
|
|
|
+ expect(result.find(e => e.id === 'a')!.narrativeOrder).toBe(1);
|
|
|
|
|
+ expect(result.find(e => e.id === 'b')!.narrativeOrder).toBe(2);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('preserves events not in the order list', () => {
|
|
|
|
|
+ const events = [makeEvent({ id: 'a' }), makeEvent({ id: 'b' })];
|
|
|
|
|
+ const result = reorderNarrative(events, ['b']);
|
|
|
|
|
+ expect(result.find(e => e.id === 'a')!.narrativeOrder).toBe(1);
|
|
|
|
|
+ expect(result.find(e => e.id === 'b')!.narrativeOrder).toBe(0);
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+describe('assignEventsToChapter', () => {
|
|
|
|
|
+ it('sets chapterIndex and status on assigned events', () => {
|
|
|
|
|
+ const events = [
|
|
|
|
|
+ makeEvent({ id: 'a' }),
|
|
|
|
|
+ makeEvent({ id: 'b' }),
|
|
|
|
|
+ makeEvent({ id: 'c' }),
|
|
|
|
|
+ ];
|
|
|
|
|
+ const result = assignEventsToChapter(events, ['a', 'c'], 3);
|
|
|
|
|
+ expect(result.find(e => e.id === 'a')!.chapterIndex).toBe(3);
|
|
|
|
|
+ expect(result.find(e => e.id === 'a')!.status).toBe('written');
|
|
|
|
|
+ expect(result.find(e => e.id === 'b')!.chapterIndex).toBeUndefined();
|
|
|
|
|
+ expect(result.find(e => e.id === 'b')!.status).toBe('pending');
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+describe('validateEventGraph', () => {
|
|
|
|
|
+ it('returns no errors for a valid graph', () => {
|
|
|
|
|
+ const events = [
|
|
|
|
|
+ makeEvent({ id: 'a', chronologicalOrder: 1 }),
|
|
|
|
|
+ makeEvent({ id: 'b', chronologicalOrder: 2, parentEventIds: ['a'] }),
|
|
|
|
|
+ ];
|
|
|
|
|
+ const edges: EventEdge[] = [
|
|
|
|
|
+ { sourceId: 'a', targetId: 'b', relationType: 'causal' },
|
|
|
|
|
+ ];
|
|
|
|
|
+ const result = validateEventGraph(events, edges);
|
|
|
|
|
+ expect(result.errors).toHaveLength(0);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('detects orphan nodes with no connections', () => {
|
|
|
|
|
+ const events = [makeEvent({ id: 'a' }), makeEvent({ id: 'b' })];
|
|
|
|
|
+ const edges: EventEdge[] = [];
|
|
|
|
|
+ const result = validateEventGraph(events, edges);
|
|
|
|
|
+ expect(result.warnings.some(w => w.includes('孤立'))).toBe(true);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('detects dangling parent references', () => {
|
|
|
|
|
+ const events = [
|
|
|
|
|
+ makeEvent({ id: 'a', parentEventIds: ['nonexistent'] }),
|
|
|
|
|
+ ];
|
|
|
|
|
+ const edges: EventEdge[] = [];
|
|
|
|
|
+ const result = validateEventGraph(events, edges);
|
|
|
|
|
+ expect(result.errors.length).toBeGreaterThan(0);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('warns about inverted causal order', () => {
|
|
|
|
|
+ const events = [
|
|
|
|
|
+ makeEvent({ id: 'a', title: 'Effect', chronologicalOrder: 2 }),
|
|
|
|
|
+ makeEvent({ id: 'b', title: 'Cause', chronologicalOrder: 1 }),
|
|
|
|
|
+ ];
|
|
|
|
|
+ const edges: EventEdge[] = [
|
|
|
|
|
+ { sourceId: 'a', targetId: 'b', relationType: 'causal' },
|
|
|
|
|
+ ];
|
|
|
|
|
+ const result = validateEventGraph(events, edges);
|
|
|
|
|
+ expect(result.warnings.some(w => w.includes('倒置'))).toBe(true);
|
|
|
|
|
+ });
|
|
|
|
|
+});
|