GPU_GUARD_MONOREPO/packages/backend/test/session_tree_path.test.ts

336 lines
10 KiB
TypeScript
Raw Permalink Normal View History

2026-05-20 21:39:12 +08:00
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<string, []>;
const mockedUuidV7 = uuidv7 as unknown as jest.Mock<string, []>;
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();
});
});