test(chronik): extract applyClientFilter helper with full test coverage

Addresses review concern: the fuer-dich predicate (youMentioned ||
youParticipated) had zero test coverage after feedFilters.test.ts was
deleted. The new clientFilter module is a pure function that is directly
testable, and the test explicitly documents why MENTION_CREATED items
without the youMentioned flag are now excluded (they would have shown
mentions directed at OTHER users under the old feedFilters.ts logic).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-21 22:43:52 +02:00
parent 330c6227bc
commit 42cf7715d2
3 changed files with 114 additions and 7 deletions

View File

@@ -11,6 +11,7 @@ import ChronikEmptyState from '$lib/components/chronik/ChronikEmptyState.svelte'
import ChronikErrorCard from '$lib/components/chronik/ChronikErrorCard.svelte';
import type { components } from '$lib/generated/api';
import type { FilterValue } from './+page.server';
import { applyClientFilter } from './clientFilter';
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
@@ -83,13 +84,7 @@ async function onMarkAllRead() {
await notificationStore.markAllRead();
}
// fuer-dich cannot be expressed as a server-side kinds filter — youMentioned/youParticipated
// are user-scoped flags that require client-side narrowing
const displayFeed = $derived(
data.filter === 'fuer-dich'
? data.activityFeed.filter((item) => item.youMentioned || item.youParticipated)
: data.activityFeed
);
const displayFeed = $derived(applyClientFilter(data.activityFeed, data.filter));
const isEmpty = $derived(displayFeed.length === 0);
const emptyVariant = $derived<'first-run' | 'filter-empty' | 'inbox-zero'>(

View File

@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest';
import { applyClientFilter } from './clientFilter';
import type { components } from '$lib/generated/api';
type Item = components['schemas']['ActivityFeedItemDTO'];
function makeItem(overrides: Partial<Item> = {}): Item {
return {
kind: 'FILE_UPLOADED',
documentId: 'd1',
documentTitle: 'Brief A',
happenedAt: '2026-04-20T10:00:00Z',
youMentioned: false,
youParticipated: false,
count: 1,
actor: null,
happenedAtUntil: null,
...overrides
};
}
describe('applyClientFilter', () => {
describe('non-fuer-dich filters pass through unchanged', () => {
it('alle returns all items', () => {
const items = [makeItem(), makeItem({ kind: 'COMMENT_ADDED' })];
expect(applyClientFilter(items, 'alle')).toHaveLength(2);
});
it('hochgeladen passes through (server already filtered by kinds)', () => {
const items = [makeItem({ kind: 'FILE_UPLOADED' }), makeItem({ kind: 'COMMENT_ADDED' })];
expect(applyClientFilter(items, 'hochgeladen')).toHaveLength(2);
});
it('transkription passes through (server already filtered by kinds)', () => {
const items = [makeItem({ kind: 'TEXT_SAVED' })];
expect(applyClientFilter(items, 'transkription')).toHaveLength(1);
});
it('kommentare passes through (server already filtered by kinds)', () => {
const items = [makeItem({ kind: 'COMMENT_ADDED' })];
expect(applyClientFilter(items, 'kommentare')).toHaveLength(1);
});
});
describe('fuer-dich applies youMentioned || youParticipated predicate', () => {
it('includes items where youMentioned is true', () => {
const items = [makeItem({ youMentioned: true })];
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(1);
});
it('includes items where youParticipated is true', () => {
const items = [makeItem({ youParticipated: true })];
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(1);
});
it('includes items where both flags are true', () => {
const items = [makeItem({ youMentioned: true, youParticipated: true })];
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(1);
});
it('excludes items where neither flag is set', () => {
const items = [makeItem({ kind: 'COMMENT_ADDED' })];
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(0);
});
it('MENTION_CREATED without youMentioned flag is excluded', () => {
// youMentioned is set by the backend only when this event is directed at the current user.
// Unlike the old feedFilters.ts, we no longer include MENTION_CREATED unconditionally —
// that incorrectly showed mentions directed at OTHER users.
const items = [
makeItem({ kind: 'MENTION_CREATED', youMentioned: false, youParticipated: false })
];
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(0);
});
it('MENTION_CREATED with youMentioned true is included', () => {
const items = [makeItem({ kind: 'MENTION_CREATED', youMentioned: true })];
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(1);
});
it('filters mixed items to only personally relevant ones', () => {
const items = [
makeItem({ kind: 'FILE_UPLOADED' }),
makeItem({ kind: 'COMMENT_ADDED', youParticipated: true }),
makeItem({ kind: 'MENTION_CREATED', youMentioned: true }),
makeItem({ kind: 'TEXT_SAVED' })
];
const result = applyClientFilter(items, 'fuer-dich');
expect(result).toHaveLength(2);
expect(result[0].kind).toBe('COMMENT_ADDED');
expect(result[1].kind).toBe('MENTION_CREATED');
});
});
});

View File

@@ -0,0 +1,18 @@
import type { components } from '$lib/generated/api';
import type { FilterValue } from './+page.server';
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
// All server-side filters (hochgeladen, transkription, kommentare) are already applied via
// kinds param — the server returns only matching items, so client-side is a no-op.
// fuer-dich is the only filter that requires client-side narrowing: youMentioned and
// youParticipated are user-scoped flags that cannot be expressed as a kinds filter.
export function applyClientFilter(
items: ActivityFeedItemDTO[],
filter: FilterValue
): ActivityFeedItemDTO[] {
if (filter === 'fuer-dich') {
return items.filter((item) => item.youMentioned || item.youParticipated);
}
return items;
}