From 70fc714df53b7a2cd7621ec08250766df963e113 Mon Sep 17 00:00:00 2001 From: hugo Date: Sun, 15 Feb 2026 22:07:36 +0100 Subject: [PATCH] fix: test hardening --- src/main/engine/TagEngine.ts | 5 +++- tests/engine/PostMediaEngine.test.ts | 6 +++++ tests/engine/TagEngine.test.ts | 37 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/main/engine/TagEngine.ts b/src/main/engine/TagEngine.ts index 8462ac2..33a31bd 100644 --- a/src/main/engine/TagEngine.ts +++ b/src/main/engine/TagEngine.ts @@ -192,7 +192,10 @@ export class TagEngine extends EventEmitter { tagName: string, transform: (postTags: string[]) => string[] ): Promise<{ total: number; process: (onEachUpdated: (updated: number, total: number) => void) => Promise }> { - const postsToUpdate = await this.queryPostsContainingTag(tagName); + const rawPostsToUpdate = await this.queryPostsContainingTag(tagName); + const postsToUpdate = Array.from( + new Map(rawPostsToUpdate.map((row) => [row.postId, row])).values() + ); const total = postsToUpdate.length; return { diff --git a/tests/engine/PostMediaEngine.test.ts b/tests/engine/PostMediaEngine.test.ts index b4903ec..413eee6 100644 --- a/tests/engine/PostMediaEngine.test.ts +++ b/tests/engine/PostMediaEngine.test.ts @@ -62,6 +62,7 @@ function createSelectChain(mockData: any[] = []) { let insertedValues: any[] = []; let updateCalls: any[] = []; let deleteCalled = false; +let deleteCallCount = 0; let selectMockData: any[] = []; function createDrizzleMock() { @@ -87,6 +88,7 @@ function createDrizzleMock() { delete: vi.fn(() => ({ where: vi.fn(() => { deleteCalled = true; + deleteCallCount++; return Promise.resolve(); }), })), @@ -126,6 +128,7 @@ describe('PostMediaEngine', () => { insertedValues = []; updateCalls = []; deleteCalled = false; + deleteCallCount = 0; selectMockData = []; resetMockCounters(); @@ -236,6 +239,7 @@ describe('PostMediaEngine', () => { await engine.unlinkMediaFromPost(postId, mediaId); expect(deleteCalled).toBe(true); + expect(deleteCallCount).toBe(1); }); it('should update media sidecar to remove postId', async () => { @@ -407,6 +411,7 @@ describe('PostMediaEngine', () => { expect(result.unlinked).toHaveLength(3); // deleteCalled flag is set to true when any delete is called expect(deleteCalled).toBe(true); + expect(deleteCallCount).toBe(3); }); it('should emit mediaBatchUnlinked event once at the end', async () => { @@ -473,6 +478,7 @@ describe('PostMediaEngine', () => { const result = await engine.unlinkManyFromPost(postId, mediaIds); expect(result.unlinked).toEqual(['media-1', 'media-2']); + expect(deleteCallCount).toBe(2); expect(mockUpdateMedia).toHaveBeenCalledTimes(2); }); }); diff --git a/tests/engine/TagEngine.test.ts b/tests/engine/TagEngine.test.ts index eedb99d..b096361 100644 --- a/tests/engine/TagEngine.test.ts +++ b/tests/engine/TagEngine.test.ts @@ -302,6 +302,24 @@ describe('TagEngine', () => { await expect(tagEngine.deleteTag('non-existent')).rejects.toThrow('Tag not found'); }); + + it('should only update each post once when query returns duplicate rows', async () => { + mockSelectDataQueue = [ + [{ id: 'tag-1', name: 'react', color: null, projectId: 'default', createdAt: new Date(), updatedAt: new Date() }], + ]; + + mockLocalClient.execute.mockResolvedValueOnce({ + rows: [ + { id: 'post-1', tags: '["react", "typescript"]' }, + { id: 'post-1', tags: '["react", "typescript"]' }, + ], + }); + + const result = await tagEngine.deleteTag('tag-1'); + + expect(result.postsUpdated).toBe(1); + expect(mockPostEngine.syncPublishedPostFile).toHaveBeenCalledTimes(1); + }); }); describe('mergeTags', () => { @@ -403,6 +421,25 @@ describe('TagEngine', () => { newName: 'new-name', })); }); + + it('should only update each post once when query returns duplicate rows', async () => { + mockSelectDataQueue = [ + [{ id: 'tag-1', name: 'old-name', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }], + [], // no duplicate target tag + ]; + + mockLocalClient.execute.mockResolvedValueOnce({ + rows: [ + { id: 'post-1', tags: '["old-name"]' }, + { id: 'post-1', tags: '["old-name"]' }, + ], + }); + + const result = await tagEngine.renameTag('tag-1', 'new-name'); + + expect(result.postsUpdated).toBe(1); + expect(mockPostEngine.syncPublishedPostFile).toHaveBeenCalledTimes(1); + }); }); describe('getTag', () => {