/** * OpenCodeManager Tool Execution Tests * * Tests the executeTool method for post-related tools, * specifically that backlinks and linksTo are included in results. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; // Mock dependencies before importing the class vi.mock('../../src/main/engine/ChatEngine', () => ({ ChatEngine: class { getSetting = vi.fn(); setSetting = vi.fn(); getSelectedModel = vi.fn(); getDefaultSystemPrompt = vi.fn(); }, })); vi.mock('../../src/main/engine/PostEngine', () => ({ getPostEngine: vi.fn(() => ({})), })); vi.mock('../../src/main/engine/MediaEngine', () => ({ getMediaEngine: vi.fn(() => ({})), })); vi.mock('../../src/main/database', () => ({ getDatabase: vi.fn(() => ({})), })); import { OpenCodeManager } from '../../src/main/engine/OpenCodeManager'; function createMockPostEngine() { return { getPost: vi.fn(), searchPosts: vi.fn(), searchPostsFiltered: vi.fn(), getAllPosts: vi.fn(), getPostsFiltered: vi.fn(), getDashboardStats: vi.fn().mockResolvedValue({ totalPosts: 0 }), getLinkedBy: vi.fn().mockResolvedValue([]), getLinksTo: vi.fn().mockResolvedValue([]), getTagsWithCounts: vi.fn().mockResolvedValue([]), getCategoriesWithCounts: vi.fn().mockResolvedValue([]), getBlogStats: vi.fn().mockResolvedValue({}), }; } function createMockMediaEngine() { return { getAllMedia: vi.fn(), getMedia: vi.fn(), getThumbnailDataUrl: vi.fn(), }; } function createMockPostMediaEngine() { return { getLinkedMediaDataForPost: vi.fn().mockResolvedValue([]), getLinkedPostsForMedia: vi.fn().mockResolvedValue([]), }; } function createManager(postEngine: ReturnType, mediaEngine?: ReturnType, postMediaEngine?: ReturnType) { const manager = new OpenCodeManager( { getSetting: vi.fn(), setSetting: vi.fn() } as never, postEngine as never, (mediaEngine ?? createMockMediaEngine()) as never, (postMediaEngine ?? createMockPostMediaEngine()) as never, () => null, ); return manager; } describe('OpenCodeManager tool execution – backlinks & linksTo', () => { let mockPostEngine: ReturnType; let manager: OpenCodeManager; beforeEach(() => { vi.clearAllMocks(); mockPostEngine = createMockPostEngine(); manager = createManager(mockPostEngine); }); describe('read_post', () => { it('includes backlinks and linksTo in the response', async () => { const post = { id: 'p1', title: 'Target Post', slug: 'target-post', content: '# Hello', excerpt: 'Hello', status: 'published', author: 'Test', categories: ['article'], tags: ['test'], createdAt: new Date('2025-01-01'), updatedAt: new Date('2025-01-02'), publishedAt: new Date('2025-01-01'), }; mockPostEngine.getPost.mockResolvedValue(post); mockPostEngine.getLinkedBy.mockResolvedValue([ { id: 'p2', title: 'Linking Post A', slug: 'linking-a' }, { id: 'p3', title: 'Linking Post B', slug: 'linking-b' }, ]); mockPostEngine.getLinksTo.mockResolvedValue([ { id: 'p4', title: 'Linked Target', slug: 'linked-target' }, ]); const result = await (manager as any).executeTool('read_post', { postId: 'p1' }); expect(result.success).toBe(true); expect(result.post.backlinks).toEqual([ { id: 'p2', title: 'Linking Post A', slug: 'linking-a' }, { id: 'p3', title: 'Linking Post B', slug: 'linking-b' }, ]); expect(result.post.linksTo).toEqual([ { id: 'p4', title: 'Linked Target', slug: 'linked-target' }, ]); expect(mockPostEngine.getLinkedBy).toHaveBeenCalledWith('p1'); expect(mockPostEngine.getLinksTo).toHaveBeenCalledWith('p1'); }); it('returns empty backlinks and linksTo arrays when none exist', async () => { const post = { id: 'p1', title: 'Lonely Post', slug: 'lonely-post', content: '# Alone', excerpt: '', status: 'draft', categories: [], tags: [], createdAt: new Date(), updatedAt: new Date(), }; mockPostEngine.getPost.mockResolvedValue(post); mockPostEngine.getLinkedBy.mockResolvedValue([]); mockPostEngine.getLinksTo.mockResolvedValue([]); const result = await (manager as any).executeTool('read_post', { postId: 'p1' }); expect(result.success).toBe(true); expect(result.post.backlinks).toEqual([]); expect(result.post.linksTo).toEqual([]); }); }); describe('search_posts', () => { it('includes backlinks and linksTo for each post in search results', async () => { const posts = [ { id: 'p1', title: 'Post One', slug: 'post-one', excerpt: '', status: 'published', categories: [], tags: [], createdAt: new Date(), updatedAt: new Date() }, { id: 'p2', title: 'Post Two', slug: 'post-two', excerpt: '', status: 'published', categories: [], tags: [], createdAt: new Date(), updatedAt: new Date() }, ]; mockPostEngine.searchPostsFiltered.mockResolvedValue(posts); mockPostEngine.getLinkedBy .mockResolvedValueOnce([{ id: 'p3', title: 'Linker', slug: 'linker' }]) .mockResolvedValueOnce([]); mockPostEngine.getLinksTo .mockResolvedValueOnce([{ id: 'p4', title: 'Target', slug: 'target' }]) .mockResolvedValueOnce([{ id: 'p5', title: 'Other', slug: 'other' }]); const result = await (manager as any).executeTool('search_posts', { query: 'test' }); expect(result.success).toBe(true); expect(result.posts[0].backlinks).toEqual([{ id: 'p3', title: 'Linker', slug: 'linker' }]); expect(result.posts[0].linksTo).toEqual([{ id: 'p4', title: 'Target', slug: 'target' }]); expect(result.posts[1].backlinks).toEqual([]); expect(result.posts[1].linksTo).toEqual([{ id: 'p5', title: 'Other', slug: 'other' }]); expect(mockPostEngine.getLinkedBy).toHaveBeenCalledTimes(2); expect(mockPostEngine.getLinksTo).toHaveBeenCalledTimes(2); }); }); describe('list_posts', () => { it('includes backlinks and linksTo for each post in listed results', async () => { const posts = [ { id: 'p1', title: 'Post A', slug: 'post-a', status: 'published', categories: [], tags: [], createdAt: new Date(), updatedAt: new Date() }, ]; mockPostEngine.getAllPosts.mockResolvedValue({ items: posts, total: 1 }); mockPostEngine.getLinkedBy.mockResolvedValue([ { id: 'px', title: 'Cross Ref', slug: 'cross-ref' }, ]); mockPostEngine.getLinksTo.mockResolvedValue([ { id: 'py', title: 'Forward Ref', slug: 'forward-ref' }, ]); const result = await (manager as any).executeTool('list_posts', {}); expect(result.success).toBe(true); expect(result.posts[0].backlinks).toEqual([{ id: 'px', title: 'Cross Ref', slug: 'cross-ref' }]); expect(result.posts[0].linksTo).toEqual([{ id: 'py', title: 'Forward Ref', slug: 'forward-ref' }]); expect(mockPostEngine.getLinkedBy).toHaveBeenCalledWith('p1'); expect(mockPostEngine.getLinksTo).toHaveBeenCalledWith('p1'); }); it('includes backlinks and linksTo for filtered list results', async () => { const posts = [ { id: 'p5', title: 'Tagged Post', slug: 'tagged', status: 'published', categories: [], tags: ['js'], createdAt: new Date(), updatedAt: new Date() }, ]; mockPostEngine.getPostsFiltered.mockResolvedValue(posts); mockPostEngine.getLinkedBy.mockResolvedValue([]); mockPostEngine.getLinksTo.mockResolvedValue([]); const result = await (manager as any).executeTool('list_posts', { tags: ['js'] }); expect(result.success).toBe(true); expect(result.posts[0].backlinks).toEqual([]); expect(result.posts[0].linksTo).toEqual([]); expect(mockPostEngine.getLinkedBy).toHaveBeenCalledWith('p5'); expect(mockPostEngine.getLinksTo).toHaveBeenCalledWith('p5'); }); }); });