feat: posts also get structured forward links

This commit is contained in:
2026-03-01 08:24:24 +01:00
parent cabb1536c3
commit 61bca755e0
4 changed files with 72 additions and 23 deletions

View File

@@ -2,7 +2,7 @@
* OpenCodeManager Tool Execution Tests
*
* Tests the executeTool method for post-related tools,
* specifically that backlinks are included in results.
* specifically that backlinks and linksTo are included in results.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
@@ -73,7 +73,7 @@ function createManager(postEngine: ReturnType<typeof createMockPostEngine>, medi
return manager;
}
describe('OpenCodeManager tool execution backlinks', () => {
describe('OpenCodeManager tool execution backlinks & linksTo', () => {
let mockPostEngine: ReturnType<typeof createMockPostEngine>;
let manager: OpenCodeManager;
@@ -84,7 +84,7 @@ describe('OpenCodeManager tool execution backlinks', () => {
});
describe('read_post', () => {
it('includes backlinks in the response', async () => {
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',
@@ -97,6 +97,9 @@ describe('OpenCodeManager tool execution backlinks', () => {
{ 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' });
@@ -105,10 +108,14 @@ describe('OpenCodeManager tool execution backlinks', () => {
{ 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 array when no backlinks exist', async () => {
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',
@@ -117,16 +124,18 @@ describe('OpenCodeManager tool execution backlinks', () => {
};
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 for each post in search results', async () => {
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() },
@@ -135,18 +144,24 @@ describe('OpenCodeManager tool execution backlinks', () => {
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 for each post in listed results', async () => {
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() },
];
@@ -154,26 +169,34 @@ describe('OpenCodeManager tool execution backlinks', () => {
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 for filtered list results', async () => {
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');
});
});
});