jest.mock('uuid', () => ({ v4: jest.fn(), v7: jest.fn(), })); import { v4 as uuidv4, v7 as uuidv7 } from 'uuid'; import type { LabelEntry, SessionTreeEntry } from '../src/modules/netaclaw/session-tree/types.js'; import { createSessionTreeEntryId, createSessionTreeSessionId, } from '../src/modules/netaclaw/session-tree/id.js'; import { ROOT_PARENT_KEY, buildEntryIndex, findCommonAncestorEntryId, getPathToLeaf, groupChildrenByParent, resolveLatestLabels, } from '../src/modules/netaclaw/session-tree/path.js'; describe('session tree path helpers', () => { const mockedUuidV4 = uuidv4 as unknown as jest.Mock; const mockedUuidV7 = uuidv7 as unknown as jest.Mock; const createMessageEntry = ( id: string, parentId: string | null, timestamp: string, ): SessionTreeEntry => ({ id, parentId, timestamp, type: 'message', message: { role: 'user', content: `${id}-content`, }, }); beforeEach(() => { mockedUuidV4.mockReset(); mockedUuidV7.mockReset(); }); it('createSessionTreeSessionId returns a non-empty string', () => { mockedUuidV7.mockReturnValue('session-v7-id'); expect(createSessionTreeSessionId()).toBe('session-v7-id'); }); it('createSessionTreeEntryId returns a non-empty string', () => { mockedUuidV4.mockReturnValue('12345678-1234-1234-1234-1234567890ab'); const entryId = createSessionTreeEntryId(); expect(entryId).toBe('entry_123456781234'); expect(entryId.length).toBeGreaterThan(0); }); it('createSessionTreeEntryId retries when the generated id already exists', () => { mockedUuidV4 .mockReturnValueOnce('12345678-1234-1234-1234-1234567890ab') .mockReturnValueOnce('abcdefab-cdef-abcd-efab-cdefabcdefab'); const existingIds = new Set(['entry_123456781234']); const entryId = createSessionTreeEntryId(existingIds); expect(entryId).toBe('entry_abcdefabcdef'); expect(existingIds.has(entryId)).toBe(false); expect(mockedUuidV4).toHaveBeenCalledTimes(2); }); it('exports the canonical root parent key', () => { expect(ROOT_PARENT_KEY).toBe('__root__'); }); it('getPathToLeaf returns a root-to-leaf path', () => { const entries = [ createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'), createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'), createMessageEntry('c', 'b', '2026-04-19T00:02:00.000Z'), ]; expect(getPathToLeaf(entries, 'c').map(entry => entry.id)).toEqual(['a', 'b', 'c']); }); it('getPathToLeaf throws when the leaf entry is missing', () => { const entries = [createMessageEntry('a', null, '2026-04-19T00:00:00.000Z')]; expect(() => getPathToLeaf(entries, 'missing')).toThrow('Entry missing not found'); }); it('getPathToLeaf throws when a cycle is detected', () => { const entries = [ createMessageEntry('a', 'c', '2026-04-19T00:00:00.000Z'), createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'), createMessageEntry('c', 'b', '2026-04-19T00:02:00.000Z'), ]; expect(() => getPathToLeaf(entries, 'c')).toThrow('Cycle detected'); }); it('getPathToLeaf throws when a parent entry is missing', () => { const entries = [createMessageEntry('c', 'missing-parent', '2026-04-19T00:02:00.000Z')]; expect(() => getPathToLeaf(entries, 'c')).toThrow('Parent entry missing-parent not found'); }); it('findCommonAncestorEntryId returns the deepest common ancestor', () => { const entries = [ createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'), createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'), createMessageEntry('c', 'b', '2026-04-19T00:02:00.000Z'), createMessageEntry('d', 'b', '2026-04-19T00:03:00.000Z'), ]; expect(findCommonAncestorEntryId(entries, 'c', 'd')).toBe('b'); }); it('groupChildrenByParent collects root entries under ROOT_PARENT_KEY', () => { const entries = [ createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'), createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'), createMessageEntry('c', null, '2026-04-19T00:02:00.000Z'), ]; expect(groupChildrenByParent(entries)).toEqual({ [ROOT_PARENT_KEY]: ['a', 'c'], a: ['b'], }); }); it('groupChildrenByParent throws when a real entry id matches ROOT_PARENT_KEY', () => { const entries = [ createMessageEntry(ROOT_PARENT_KEY, null, '2026-04-19T00:00:00.000Z'), createMessageEntry('child', ROOT_PARENT_KEY, '2026-04-19T00:01:00.000Z'), ]; expect(() => groupChildrenByParent(entries)).toThrow( `Entry id ${ROOT_PARENT_KEY} conflicts with ROOT_PARENT_KEY`, ); }); it('resolveLatestLabels keeps the later label for the same target', () => { const entries: SessionTreeEntry[] = [ createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'), { id: 'label-1', parentId: 'a', timestamp: '2026-04-19T00:01:00.000Z', type: 'label', targetId: 'a', label: 'initial', } satisfies LabelEntry, { id: 'label-2', parentId: 'a', timestamp: '2026-04-19T00:02:00.000Z', type: 'label', targetId: 'a', label: 'latest', } satisfies LabelEntry, ]; expect(resolveLatestLabels(entries)).toEqual({ a: { label: 'latest', timestamp: '2026-04-19T00:02:00.000Z', entryId: 'label-2', }, }); }); it('resolveLatestLabels keeps the newest label when an older label appears later in the array', () => { const entries: SessionTreeEntry[] = [ createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'), { id: 'label-new', parentId: 'a', timestamp: '2026-04-19T00:02:00.000Z', type: 'label', targetId: 'a', label: 'latest', } satisfies LabelEntry, { id: 'label-old', parentId: 'a', timestamp: '2026-04-19T00:01:00.000Z', type: 'label', targetId: 'a', label: 'stale', } satisfies LabelEntry, ]; expect(resolveLatestLabels(entries)).toEqual({ a: { label: 'latest', timestamp: '2026-04-19T00:02:00.000Z', entryId: 'label-new', }, }); }); it('resolveLatestLabels clears the target label when label is undefined', () => { const entries: SessionTreeEntry[] = [ createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'), { id: 'label-1', parentId: 'a', timestamp: '2026-04-19T00:01:00.000Z', type: 'label', targetId: 'a', label: 'initial', } satisfies LabelEntry, { id: 'label-3', parentId: 'a', timestamp: '2026-04-19T00:03:00.000Z', type: 'label', targetId: 'a', label: undefined, } satisfies LabelEntry, ]; expect(resolveLatestLabels(entries)).toEqual({}); }); it('resolveLatestLabels ignores an older clear record that appears later in the array', () => { const entries: SessionTreeEntry[] = [ createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'), { id: 'label-new', parentId: 'a', timestamp: '2026-04-19T00:03:00.000Z', type: 'label', targetId: 'a', label: 'latest', } satisfies LabelEntry, { id: 'label-old-clear', parentId: 'a', timestamp: '2026-04-19T00:02:00.000Z', type: 'label', targetId: 'a', label: undefined, } satisfies LabelEntry, ]; expect(resolveLatestLabels(entries)).toEqual({ a: { label: 'latest', timestamp: '2026-04-19T00:03:00.000Z', entryId: 'label-new', }, }); }); it('buildEntryIndex returns an id keyed map', () => { const entries = [ createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'), createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'), ]; expect(buildEntryIndex(entries)).toEqual({ a: entries[0], b: entries[1], }); }); it('buildEntryIndex stores __proto__ and constructor ids without prototype pollution', () => { const entries = [ createMessageEntry('__proto__', null, '2026-04-19T00:00:00.000Z'), createMessageEntry('constructor', null, '2026-04-19T00:01:00.000Z'), ]; const index = buildEntryIndex(entries); expect(index.__proto__).toBe(entries[0]); expect(index.constructor).toBe(entries[1]); expect(Object.getPrototypeOf(index)).toBeNull(); }); it('groupChildrenByParent stores __proto__ and constructor parent ids without prototype pollution', () => { const entries = [ createMessageEntry('__proto__', null, '2026-04-19T00:00:00.000Z'), createMessageEntry('constructor', '__proto__', '2026-04-19T00:01:00.000Z'), createMessageEntry('leaf', 'constructor', '2026-04-19T00:02:00.000Z'), ]; const groups = groupChildrenByParent(entries); expect(groups[ROOT_PARENT_KEY]).toEqual(['__proto__']); expect(groups.__proto__).toEqual(['constructor']); expect(groups.constructor).toEqual(['leaf']); expect(Object.getPrototypeOf(groups)).toBeNull(); }); it('resolveLatestLabels stores __proto__ and constructor target ids without prototype pollution', () => { const entries: SessionTreeEntry[] = [ createMessageEntry('__proto__', null, '2026-04-19T00:00:00.000Z'), createMessageEntry('constructor', null, '2026-04-19T00:00:01.000Z'), { id: 'label-proto', parentId: '__proto__', timestamp: '2026-04-19T00:00:02.000Z', type: 'label', targetId: '__proto__', label: 'proto-label', } satisfies LabelEntry, { id: 'label-constructor', parentId: 'constructor', timestamp: '2026-04-19T00:00:03.000Z', type: 'label', targetId: 'constructor', label: 'constructor-label', } satisfies LabelEntry, ]; const labels = resolveLatestLabels(entries); expect(labels.__proto__).toEqual({ label: 'proto-label', timestamp: '2026-04-19T00:00:02.000Z', entryId: 'label-proto', }); expect(labels.constructor).toEqual({ label: 'constructor-label', timestamp: '2026-04-19T00:00:03.000Z', entryId: 'label-constructor', }); expect(Object.getPrototypeOf(labels)).toBeNull(); }); });