diff --git a/apps/web/src/lib/server/tasks.search.test.ts b/apps/web/src/lib/server/tasks.search.test.ts index 48a0781c..4b35f59e 100644 --- a/apps/web/src/lib/server/tasks.search.test.ts +++ b/apps/web/src/lib/server/tasks.search.test.ts @@ -35,10 +35,32 @@ describe('searchTasks', () => { expect(results.map((task) => task.id)).toEqual([myTask.id]); }); - it('still includes explicit includeIds even when another user initiated them', async () => { + it('only includes explicit includeIds initiated by the current user', async () => { const me = await userFactory.create({}); const other = await userFactory.create({}); + const myRecentTask = await taskFactory.create({ + title: 'Recent task', + initiatorUserId: me.id, + activityAt: 5_000, + timestamp: 5_000, + }); + await runFactory.create({ + taskId: myRecentTask.id, + payloadKind: TaskPayloadKind.StandardTask, + }); + + const myIncludedTask = await taskFactory.create({ + title: 'Pinned task', + initiatorUserId: me.id, + activityAt: 3_000, + timestamp: 3_000, + }); + await runFactory.create({ + taskId: myIncludedTask.id, + payloadKind: TaskPayloadKind.StandardTask, + }); + const theirTask = await taskFactory.create({ title: 'Shared review', initiatorUserId: other.id, @@ -52,11 +74,14 @@ describe('searchTasks', () => { const results = await searchTasks({ userId: me.id, - limit: 20, - includeIds: [theirTask.id], + limit: 1, + includeIds: [myIncludedTask.id, theirTask.id], }); - expect(results.map((task) => task.id)).toEqual([theirTask.id]); + expect(results.map((task) => task.id)).toEqual([ + myRecentTask.id, + myIncludedTask.id, + ]); }); it('filters title ILIKE search to the current user', async () => { diff --git a/apps/web/src/lib/server/tasks.ts b/apps/web/src/lib/server/tasks.ts index bd036c07..aa746833 100644 --- a/apps/web/src/lib/server/tasks.ts +++ b/apps/web/src/lib/server/tasks.ts @@ -441,8 +441,7 @@ export const getTasks = async ({ * * Simple search by title with ILIKE, scoped to tasks initiated by the * current user (matches the default tasks list). Explicit `includeIds` - * (pins / recently visited) can still surface specific visible tasks the - * user has interacted with, even when they were not the initiator. + * ensure the user's pinned / recently visited tasks are included. */ type SearchTaskResult = { @@ -497,9 +496,8 @@ export const searchTasks = async ({ .orderBy(desc(tasks.activityAt), desc(tasks.id)) .limit(limit); - // Fetch pinned / visited tasks that aren't already in the search results. - // Absolute visibility is required, but initiator is not — pins and direct - // visits can include other users' tasks the current user already opened. + // Fetch the current user's pinned / visited tasks that aren't already in the + // search results. const searchResultIds = new Set(searchResults.map((t) => t.id)); const missingIds = (includeIds ?? []).filter( @@ -517,7 +515,13 @@ export const searchTasks = async ({ activityAt: tasks.activityAt, }) .from(tasks) - .where(and(...visibleConditions, inArray(tasks.id, missingIds))) + .where( + and( + ...visibleConditions, + eq(tasks.initiatorUserId, userId), + inArray(tasks.id, missingIds), + ), + ) .orderBy(desc(tasks.activityAt), desc(tasks.id)); }