Feature/semantic similarity (#36)

* fix: mixed up migrations

* feat: semantic similarity first take

* feat: semantic similarity first round of fixes

* feat: more work on making semantic similarity work properly

* feat: getPostBySlug for the AI

* feat: show similarity in post-link-insert-modal

* chore: remove done doc

---------

Co-authored-by: hugo <hugoms@me.com>
This commit is contained in:
Georg Bauer
2026-03-05 22:05:32 +01:00
committed by GitHub
parent 8ac8305e01
commit 7e1e8981a3
64 changed files with 6429 additions and 499 deletions

View File

@@ -14,6 +14,7 @@ function createMockDeps(): BlogToolDeps {
return {
postEngine: {
getPost: vi.fn(),
getPostBySlug: vi.fn(),
getAllPosts: vi.fn(),
getPostsFiltered: vi.fn(),
searchPostsFiltered: vi.fn(),
@@ -72,12 +73,13 @@ describe('Blog Tools — createBlogTools', () => {
tools = createBlogTools(deps);
});
it('returns all 17 tools', () => {
it('returns all 18 tools', () => {
const names = Object.keys(tools);
expect(names).toHaveLength(17);
expect(names).toHaveLength(18);
expect(names).toContain('check_term');
expect(names).toContain('search_posts');
expect(names).toContain('read_post');
expect(names).toContain('read_post_by_slug');
expect(names).toContain('list_posts');
expect(names).toContain('get_media');
expect(names).toContain('list_media');
@@ -243,6 +245,52 @@ describe('Blog Tools — read_post', () => {
});
});
// ---------------------------------------------------------------------------
// read_post_by_slug
// ---------------------------------------------------------------------------
describe('Blog Tools — read_post_by_slug', () => {
let deps: BlogToolDeps;
let tools: ReturnType<typeof createBlogTools>;
beforeEach(() => {
deps = createMockDeps();
tools = createBlogTools(deps);
});
it('returns post with backlinks and outlinks when found by slug', async () => {
vi.mocked(deps.postEngine.getPostBySlug).mockResolvedValueOnce(samplePost);
vi.mocked(deps.postEngine.getLinkedBy).mockResolvedValueOnce([
{ id: 'post-2', title: 'Related', slug: 'related' },
]);
const result = await tools.read_post_by_slug.execute!(
{ slug: 'hello-world' },
{ toolCallId: 'tc1', messages: [], abortSignal: new AbortController().signal },
);
expect(result).toMatchObject({
success: true,
post: {
id: 'post-1',
title: 'Hello World',
slug: 'hello-world',
content: '# Hello\n\nWorld',
backlinks: [{ id: 'post-2', title: 'Related' }],
},
});
expect(deps.postEngine.getPostBySlug).toHaveBeenCalledWith('hello-world');
});
it('returns error for nonexistent slug', async () => {
vi.mocked(deps.postEngine.getPostBySlug).mockResolvedValueOnce(null);
const result = await tools.read_post_by_slug.execute!(
{ slug: 'nope' },
{ toolCallId: 'tc1', messages: [], abortSignal: new AbortController().signal },
);
expect(result).toMatchObject({ success: false, error: 'Post not found' });
});
});
// ---------------------------------------------------------------------------
// list_posts
// ---------------------------------------------------------------------------