diff --git a/tests/parser-filter.test.ts b/tests/parser-filter.test.ts new file mode 100644 index 0000000..4c34722 --- /dev/null +++ b/tests/parser-filter.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from 'vitest' + +import { filterProjectsByName } from '../src/parser.js' +import type { ProjectSummary } from '../src/types.js' + +function makeProject(project: string, projectPath = project): ProjectSummary { + return { + project, + projectPath, + sessions: [], + totalCostUSD: 0, + totalApiCalls: 0, + } +} + +describe('filterProjectsByName', () => { + const projects = [ + makeProject('codeburn', '/Users/alice/codeburn'), + makeProject('AgentSeal', '/Users/alice/projects/AgentSeal'), + makeProject('dashboard', '/Users/alice/AgentSeal/dashboard'), + makeProject('sandbox', '/tmp/sandbox'), + ] + + it('returns all projects when no filters given', () => { + expect(filterProjectsByName(projects)).toEqual(projects) + expect(filterProjectsByName(projects, [], [])).toEqual(projects) + expect(filterProjectsByName(projects, undefined, undefined)).toEqual(projects) + }) + + it('include matches project name (case-insensitive substring)', () => { + const result = filterProjectsByName(projects, ['codeburn']) + expect(result.map(p => p.project)).toEqual(['codeburn']) + }) + + it('include is case-insensitive', () => { + const result = filterProjectsByName(projects, ['AGENTSEAL']) + expect(result.map(p => p.project).sort()).toEqual(['AgentSeal', 'dashboard']) + }) + + it('include matches substring in path when name does not match', () => { + const result = filterProjectsByName(projects, ['alice/projects']) + expect(result.map(p => p.project)).toEqual(['AgentSeal']) + }) + + it('include uses OR semantics across patterns', () => { + const result = filterProjectsByName(projects, ['codeburn', 'sandbox']) + expect(result.map(p => p.project).sort()).toEqual(['codeburn', 'sandbox']) + }) + + it('exclude removes matching projects (AND-negation across patterns)', () => { + const result = filterProjectsByName(projects, undefined, ['codeburn', 'sandbox']) + expect(result.map(p => p.project).sort()).toEqual(['AgentSeal', 'dashboard']) + }) + + it('exclude matches path substring', () => { + const result = filterProjectsByName(projects, undefined, ['/tmp']) + expect(result.map(p => p.project)).not.toContain('sandbox') + }) + + it('exclude is applied after include', () => { + const result = filterProjectsByName(projects, ['AgentSeal'], ['dashboard']) + expect(result.map(p => p.project)).toEqual(['AgentSeal']) + }) + + it('returns empty array when no project matches include', () => { + expect(filterProjectsByName(projects, ['does-not-exist'])).toEqual([]) + }) + + it('empty-string pattern matches every project', () => { + const resultInclude = filterProjectsByName(projects, ['']) + expect(resultInclude).toHaveLength(projects.length) + const resultExclude = filterProjectsByName(projects, undefined, ['']) + expect(resultExclude).toEqual([]) + }) + + it('does not mutate the input array', () => { + const input = [makeProject('a'), makeProject('b')] + const snapshot = [...input] + filterProjectsByName(input, ['a'], ['b']) + expect(input).toEqual(snapshot) + }) +})