| |
| |
| |
| |
| |
|
|
| import { describe, it, expect } from 'vitest'; |
| import { fromGraph } from './fromGraph.js'; |
| import { NodeType, type ConcreteNode } from './types.js'; |
| import { NodeIdService } from './nodeIdService.js'; |
|
|
| describe('fromGraph', () => { |
| it('should reconstruct an empty history from empty nodes', () => { |
| expect(fromGraph([])).toEqual([]); |
| }); |
|
|
| it('should reconstruct a single turn from a single node', () => { |
| const nodes: ConcreteNode[] = [ |
| { |
| id: 'node_1', |
| turnId: 'turn_durable_1', |
| role: 'user', |
| type: NodeType.USER_PROMPT, |
| payload: { text: 'hello' }, |
| timestamp: 100, |
| }, |
| ]; |
|
|
| const history = fromGraph(nodes); |
| expect(history).toEqual([ |
| { |
| id: 'durable_1', |
| content: { |
| role: 'user', |
| parts: [{ text: 'hello' }], |
| }, |
| }, |
| ]); |
| }); |
|
|
| it('should coalesce adjacent nodes with the same turnId into a single turn', () => { |
| const nodes: ConcreteNode[] = [ |
| { |
| id: 'node_1', |
| turnId: 'turn_durable_1', |
| role: 'user', |
| type: NodeType.USER_PROMPT, |
| payload: { text: 'hello' }, |
| timestamp: 100, |
| }, |
| { |
| id: 'node_2', |
| turnId: 'turn_durable_1', |
| role: 'user', |
| type: NodeType.USER_PROMPT, |
| payload: { text: 'world' }, |
| timestamp: 101, |
| }, |
| ]; |
|
|
| const history = fromGraph(nodes); |
| expect(history).toEqual([ |
| { |
| id: 'durable_1', |
| content: { |
| role: 'user', |
| parts: [{ text: 'hello' }, { text: 'world' }], |
| }, |
| }, |
| ]); |
| }); |
|
|
| it('should split turns when the role changes', () => { |
| const nodes: ConcreteNode[] = [ |
| { |
| id: 'node_1', |
| turnId: 'turn_durable_1', |
| role: 'user', |
| type: NodeType.USER_PROMPT, |
| payload: { text: 'hello' }, |
| timestamp: 100, |
| }, |
| { |
| id: 'node_2', |
| turnId: 'turn_durable_2', |
| role: 'model', |
| type: NodeType.AGENT_THOUGHT, |
| payload: { text: 'hi' }, |
| timestamp: 101, |
| }, |
| ]; |
|
|
| const history = fromGraph(nodes); |
| expect(history).toEqual([ |
| { |
| id: 'durable_1', |
| content: { |
| role: 'user', |
| parts: [{ text: 'hello' }], |
| }, |
| }, |
| { |
| id: 'durable_2', |
| content: { |
| role: 'model', |
| parts: [{ text: 'hi' }], |
| }, |
| }, |
| ]); |
| }); |
|
|
| it('should split turns when the turnId changes, even if role is the same', () => { |
| const nodes: ConcreteNode[] = [ |
| { |
| id: 'node_1', |
| turnId: 'turn_durable_1', |
| role: 'user', |
| type: NodeType.USER_PROMPT, |
| payload: { text: 'hello' }, |
| timestamp: 100, |
| }, |
| { |
| id: 'node_2', |
| turnId: 'turn_durable_2', |
| role: 'user', |
| type: NodeType.USER_PROMPT, |
| payload: { text: 'world' }, |
| timestamp: 101, |
| }, |
| ]; |
|
|
| const history = fromGraph(nodes); |
| expect(history).toEqual([ |
| { |
| id: 'durable_1', |
| content: { |
| role: 'user', |
| parts: [{ text: 'hello' }], |
| }, |
| }, |
| { |
| id: 'durable_2', |
| content: { |
| role: 'user', |
| parts: [{ text: 'world' }], |
| }, |
| }, |
| ]); |
| }); |
|
|
| it('should correctly strip the turn_ prefix from turnId', () => { |
| const nodes: ConcreteNode[] = [ |
| { |
| id: 'node_1', |
| turnId: 'turn_my_stable_id_123', |
| role: 'user', |
| type: NodeType.USER_PROMPT, |
| payload: { text: 'hello' }, |
| timestamp: 100, |
| }, |
| ]; |
|
|
| const history = fromGraph(nodes); |
| expect(history[0].id).toBe('my_stable_id_123'); |
| }); |
|
|
| it('should handle orphan nodes gracefully', () => { |
| const nodes: ConcreteNode[] = [ |
| { |
| id: 'node_1', |
| role: 'user', |
| type: NodeType.USER_PROMPT, |
| payload: { text: 'orphan part' }, |
| timestamp: 100, |
| } as unknown as ConcreteNode, |
| ]; |
|
|
| const history = fromGraph(nodes); |
| expect(history[0].id).toBe('orphan'); |
| expect(history[0].content.parts).toEqual([{ text: 'orphan part' }]); |
| }); |
|
|
| it('should register identities with the NodeIdService if provided', () => { |
| const idService = new NodeIdService(); |
| const payload = { text: 'hello' }; |
| const nodes: ConcreteNode[] = [ |
| { |
| id: 'node_1', |
| turnId: 'turn_1', |
| role: 'user', |
| type: NodeType.USER_PROMPT, |
| payload, |
| timestamp: 100, |
| }, |
| ]; |
|
|
| fromGraph(nodes, idService); |
|
|
| |
| expect(idService.get(payload)).toBe('node_1'); |
| }); |
| }); |
|
|