agent-ecosystem/test/renderer/utils/chipUtils.test.ts
iliya 80034542ec feat: enhance editor file handling and task management features
- Improved EditorFileWatcher to debounce content change events, optimizing git status cache invalidation for rapid file saves.
- Updated ProjectScanner to derive project and display names from actual paths, enhancing reliability in project identification.
- Enhanced IPC methods for creating tasks and sending messages directly from the editor context menu, streamlining team collaboration.
- Refactored task relationship handling to ensure accurate linking of tasks based on user actions.
- Introduced animations for new chat messages and comments, improving user experience in chat history and task comments sections.
2026-03-02 20:08:03 +02:00

253 lines
8.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { chipToken } from '@renderer/types/inlineChip';
import {
createChipFromSelection,
findChipBoundary,
isInsideChip,
reconcileChips,
removeChipTokenFromText,
snapCursorToChipBoundary,
} from '@renderer/utils/chipUtils';
import type { InlineChip } from '@renderer/types/inlineChip';
import type { EditorSelectionAction } from '@shared/types/editor';
function makeChip(overrides: Partial<InlineChip> = {}): InlineChip {
return {
id: 'chip-test-1',
filePath: '/src/auth.ts',
fileName: 'auth.ts',
fromLine: 10,
toLine: 15,
codeText: 'const x = 1;',
language: 'typescript',
...overrides,
};
}
function makeAction(overrides: Partial<EditorSelectionAction> = {}): EditorSelectionAction {
return {
type: 'sendMessage',
filePath: '/src/auth.ts',
fromLine: 10,
toLine: 15,
selectedText: 'const x = 1;',
formattedContext: '**auth.ts** (lines 10-15):\n```typescript\nconst x = 1;\n```',
...overrides,
};
}
describe('createChipFromSelection', () => {
it('creates a chip from an EditorSelectionAction', () => {
const action = makeAction();
const chip = createChipFromSelection(action, []);
expect(chip).not.toBeNull();
expect(chip!.filePath).toBe('/src/auth.ts');
expect(chip!.fileName).toBe('auth.ts');
expect(chip!.fromLine).toBe(10);
expect(chip!.toLine).toBe(15);
expect(chip!.codeText).toBe('const x = 1;');
expect(chip!.language).toBe('typescript');
expect(chip!.id).toMatch(/^chip-/);
});
it('returns null for duplicate (same filePath + lines)', () => {
const existing = makeChip();
const action = makeAction();
expect(createChipFromSelection(action, [existing])).toBeNull();
});
it('allows different line ranges for the same file', () => {
const existing = makeChip({ fromLine: 1, toLine: 5 });
const action = makeAction({ fromLine: 10, toLine: 15 });
expect(createChipFromSelection(action, [existing])).not.toBeNull();
});
it('creates a file-mention chip when selectedText is empty and lines are null', () => {
const action = makeAction({
selectedText: '',
fromLine: null,
toLine: null,
displayPath: 'src/auth.ts',
});
const chip = createChipFromSelection(action, []);
expect(chip).not.toBeNull();
expect(chip!.fromLine).toBeNull();
expect(chip!.toLine).toBeNull();
expect(chip!.codeText).toBe('');
expect(chip!.displayPath).toBe('src/auth.ts');
expect(chip!.fileName).toBe('auth.ts');
});
it('deduplicates file-mention chips by filePath', () => {
const existing = makeChip({ fromLine: null, toLine: null, codeText: '' });
const action = makeAction({ selectedText: '', fromLine: null, toLine: null });
expect(createChipFromSelection(action, [existing])).toBeNull();
});
it('creates file-mention chip when fromLine is null', () => {
const action = makeAction({ fromLine: null, selectedText: 'code' });
const chip = createChipFromSelection(action, []);
expect(chip).not.toBeNull();
expect(chip!.fromLine).toBeNull();
});
});
describe('findChipBoundary', () => {
it('returns boundary when cursor is at chip end', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `Hello ${token} world`;
const cursorPos = 6 + token.length; // at end of token
const boundary = findChipBoundary(text, [chip], cursorPos);
expect(boundary).not.toBeNull();
expect(boundary!.start).toBe(6);
expect(boundary!.end).toBe(6 + token.length);
expect(boundary!.chip).toBe(chip);
});
it('returns boundary when cursor is at chip start', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `Hello ${token} world`;
const boundary = findChipBoundary(text, [chip], 6);
expect(boundary).not.toBeNull();
expect(boundary!.start).toBe(6);
});
it('returns boundary when cursor is inside chip', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `Hello ${token} world`;
const boundary = findChipBoundary(text, [chip], 8);
expect(boundary).not.toBeNull();
});
it('returns null when cursor is not near any chip', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `Hello ${token} world`;
// cursor at beginning
expect(findChipBoundary(text, [chip], 0)).toBeNull();
});
it('handles multiple chips', () => {
const chip1 = makeChip({ id: 'c1', fileName: 'a.ts', fromLine: 1, toLine: 1 });
const chip2 = makeChip({ id: 'c2', fileName: 'b.ts', fromLine: 2, toLine: 2 });
const t1 = chipToken(chip1);
const t2 = chipToken(chip2);
const text = `${t1} ${t2}`;
const boundary = findChipBoundary(text, [chip1, chip2], t1.length + 1);
expect(boundary).not.toBeNull();
expect(boundary!.chip.id).toBe('c2');
});
});
describe('isInsideChip', () => {
it('returns true when cursor is strictly inside', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `X${token}Y`;
expect(isInsideChip(text, [chip], 3)).toBe(true);
});
it('returns false at chip start boundary', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `X${token}Y`;
expect(isInsideChip(text, [chip], 1)).toBe(false);
});
it('returns false at chip end boundary', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `X${token}Y`;
expect(isInsideChip(text, [chip], 1 + token.length)).toBe(false);
});
it('returns false outside chip', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `X${token}Y`;
expect(isInsideChip(text, [chip], 0)).toBe(false);
});
});
describe('snapCursorToChipBoundary', () => {
it('snaps to nearest start when cursor is closer to start', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `X${token}Y`;
// position 2 is inside, closer to start (1)
const snapped = snapCursorToChipBoundary(text, [chip], 2);
expect(snapped).toBe(1);
});
it('snaps to nearest end when cursor is closer to end', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `X${token}Y`;
// position close to end
const nearEnd = 1 + token.length - 1;
const snapped = snapCursorToChipBoundary(text, [chip], nearEnd);
expect(snapped).toBe(1 + token.length);
});
it('returns original position if not inside chip', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `X${token}Y`;
expect(snapCursorToChipBoundary(text, [chip], 0)).toBe(0);
});
});
describe('reconcileChips', () => {
it('keeps chips whose tokens are present', () => {
const chip = makeChip();
const text = `Hello ${chipToken(chip)} world`;
expect(reconcileChips([chip], text)).toEqual([chip]);
});
it('removes chips whose tokens are missing', () => {
const chip = makeChip();
expect(reconcileChips([chip], 'Hello world')).toEqual([]);
});
it('handles partial removal (only some chips gone)', () => {
const chip1 = makeChip({ id: 'c1', fileName: 'a.ts', fromLine: 1, toLine: 1 });
const chip2 = makeChip({ id: 'c2', fileName: 'b.ts', fromLine: 2, toLine: 2 });
const text = chipToken(chip1); // only chip1 token present
const result = reconcileChips([chip1, chip2], text);
expect(result).toEqual([chip1]);
});
});
describe('removeChipTokenFromText', () => {
it('removes the token from text', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `Hello ${token} world`;
expect(removeChipTokenFromText(text, chip)).toBe('Hello world');
});
it('removes trailing newline', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `${token}\nHello`;
expect(removeChipTokenFromText(text, chip)).toBe('Hello');
});
it('does not alter text when token not found', () => {
const chip = makeChip();
const text = 'Hello world';
expect(removeChipTokenFromText(text, chip)).toBe('Hello world');
});
it('removes token from middle of text', () => {
const chip = makeChip();
const token = chipToken(chip);
const text = `A\n${token}\nB`;
expect(removeChipTokenFromText(text, chip)).toBe('A\nB');
});
});