mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-23 07:09:41 +00:00
A quoted '~/work/app' reaches the CLI unexpanded, and a pattern typed into a field has no shell behind it at all. It was neither rooted nor a substring of any stored path, so it selected nothing and said nothing. The README made it worse by telling people to quote it. A leading ~ is now expanded against the home directory and anchored like any absolute path, so the quoted form selects the same project as the one the shell would have expanded, and a wrong one is reported. Two more from reading the hot path: the compiled filter only pays for the project path key when some pattern is rooted and can read it, which the day cache runs per project, per day, per provider slice; and unmatchedRootedPatterns now calls matchesProjectPattern instead of open-coding it.
144 lines
6.2 KiB
TypeScript
144 lines
6.2 KiB
TypeScript
import { homedir } from 'node:os'
|
|
|
|
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('an absolute-path pattern does not swallow a sibling sharing its prefix', () => {
|
|
const siblings = [
|
|
makeProject('my-company', '/Users/alice/work/my-company'),
|
|
makeProject('my-company-kit', '/Users/alice/work/my-company-kit'),
|
|
makeProject('client', '/Users/alice/work/my-company/packages/client'),
|
|
]
|
|
const result = filterProjectsByName(siblings, undefined, ['/Users/alice/work/my-company'])
|
|
expect(result.map(p => p.project)).toEqual(['my-company-kit'])
|
|
})
|
|
|
|
it('anchors an absolute-path include the same way', () => {
|
|
const siblings = [
|
|
makeProject('my-company', '/Users/alice/work/my-company'),
|
|
makeProject('my-company-kit', '/Users/alice/work/my-company-kit'),
|
|
]
|
|
expect(filterProjectsByName(siblings, ['/Users/alice/work/my-company']).map(p => p.project)).toEqual(['my-company'])
|
|
})
|
|
|
|
it('keeps a plain word loose, so it still spans siblings and worktrees', () => {
|
|
const siblings = [
|
|
makeProject('my-company', '/Users/alice/work/my-company'),
|
|
makeProject('my-company-kit', '/Users/alice/work/my-company-kit'),
|
|
makeProject('other', '/Users/alice/work/other'),
|
|
]
|
|
expect(filterProjectsByName(siblings, ['my-company']).map(p => p.project)).toEqual(['my-company', 'my-company-kit'])
|
|
})
|
|
|
|
it('is trailing-slash and backslash tolerant on an absolute pattern', () => {
|
|
const win = [makeProject('app', 'C:\\Users\\alice\\app'), makeProject('app-two', 'C:\\Users\\alice\\app-two')]
|
|
expect(filterProjectsByName(win, undefined, ['C:/Users/alice/app/']).map(p => p.project)).toEqual(['app-two'])
|
|
})
|
|
|
|
it('folds case on an identified Windows pattern and keeps POSIX case identity', () => {
|
|
const posix = [makeProject('Vault', '/a/Vault'), makeProject('vault', '/a/vault')]
|
|
expect(filterProjectsByName(posix, ['/a/Vault']).map(p => p.project)).toEqual(['Vault'])
|
|
const win = [makeProject('vault', 'C:\\Work\\Vault')]
|
|
expect(filterProjectsByName(win, ['c:/work/vault']).map(p => p.project)).toEqual(['vault'])
|
|
const unc = [makeProject('vault', '\\\\Server\\Share\\Vault')]
|
|
expect(filterProjectsByName(unc, ['//server/share']).map(p => p.project)).toEqual(['vault'])
|
|
})
|
|
|
|
it('anchors a POSIX pattern against a Codex-style stripped absolute path', () => {
|
|
const codex = [makeProject('vault', 'root/vault'), makeProject('vault-ui', 'root/vault-ui')]
|
|
expect(filterProjectsByName(codex, undefined, ['/root/vault']).map(p => p.project)).toEqual(['vault-ui'])
|
|
})
|
|
|
|
it('names no project for a rooted pattern that keys to nothing', () => {
|
|
expect(filterProjectsByName(projects, ['/'])).toEqual([])
|
|
expect(filterProjectsByName(projects, undefined, ['/'])).toEqual(projects)
|
|
expect(filterProjectsByName(projects, undefined, ['//'])).toEqual(projects)
|
|
})
|
|
|
|
it('expands a leading ~ the shell did not, and anchors it', () => {
|
|
const home = homedir().replace(/\\/g, '/')
|
|
const mine = [makeProject('app', `${home}/work/app`), makeProject('app-kit', `${home}/work/app-kit`)]
|
|
expect(filterProjectsByName(mine, ['~/work/app']).map(p => p.project)).toEqual(['app'])
|
|
expect(filterProjectsByName(mine, undefined, ['~/work/app']).map(p => p.project)).toEqual(['app-kit'])
|
|
expect(filterProjectsByName(mine, ['~/nowhere'])).toEqual([])
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|