Skip to content

Commit ad8fb83

Browse files
committed
add tests
1 parent 5abe61e commit ad8fb83

File tree

2 files changed

+118
-5
lines changed

2 files changed

+118
-5
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { glob, grep } from '@/lib/copilot/vfs/operations'
6+
7+
function vfsFromEntries(entries: [string, string][]): Map<string, string> {
8+
return new Map(entries)
9+
}
10+
11+
describe('glob', () => {
12+
it('matches one path segment for single star (files listing pattern)', () => {
13+
const files = vfsFromEntries([
14+
['files/a/meta.json', '{}'],
15+
['files/a/b/meta.json', '{}'],
16+
['uploads/x.png', ''],
17+
])
18+
const hits = glob(files, 'files/*/meta.json')
19+
expect(hits).toContain('files/a/meta.json')
20+
expect(hits).not.toContain('files/a/b/meta.json')
21+
})
22+
23+
it('matches nested paths with double star', () => {
24+
const files = vfsFromEntries([
25+
['workflows/W/state.json', ''],
26+
['workflows/W/sub/state.json', ''],
27+
])
28+
const hits = glob(files, 'workflows/**/state.json')
29+
expect(hits.sort()).toEqual(['workflows/W/state.json', 'workflows/W/sub/state.json'].sort())
30+
})
31+
32+
it('includes virtual directory prefixes when pattern matches descendants', () => {
33+
const files = vfsFromEntries([['files/a/meta.json', '{}']])
34+
const hits = glob(files, 'files/**')
35+
expect(hits).toContain('files')
36+
expect(hits).toContain('files/a')
37+
expect(hits).toContain('files/a/meta.json')
38+
})
39+
40+
it('treats braces literally when nobrace is set (matches old builder)', () => {
41+
const files = vfsFromEntries([
42+
['weird{brace}/x', ''],
43+
['weirdA/x', ''],
44+
])
45+
const hits = glob(files, 'weird{brace}/*')
46+
expect(hits).toContain('weird{brace}/x')
47+
expect(hits).not.toContain('weirdA/x')
48+
})
49+
})
50+
51+
describe('grep', () => {
52+
it('returns content matches per line in default mode', () => {
53+
const files = vfsFromEntries([['a.txt', 'hello\nworld\nhello']])
54+
const matches = grep(files, 'hello', undefined, { outputMode: 'content' })
55+
expect(matches).toHaveLength(2)
56+
expect(matches[0]).toMatchObject({ path: 'a.txt', line: 1, content: 'hello' })
57+
expect(matches[1]).toMatchObject({ path: 'a.txt', line: 3, content: 'hello' })
58+
})
59+
60+
it('strips CR before end-of-line matching on CRLF content', () => {
61+
const files = vfsFromEntries([['x.txt', 'foo\r\n']])
62+
const matches = grep(files, 'foo$', undefined, { outputMode: 'content' })
63+
expect(matches).toHaveLength(1)
64+
expect(matches[0]?.content).toBe('foo')
65+
})
66+
67+
it('counts matching lines', () => {
68+
const files = vfsFromEntries([['a.txt', 'a\nb\na']])
69+
const counts = grep(files, 'a', undefined, { outputMode: 'count' })
70+
expect(counts).toEqual([{ path: 'a.txt', count: 2 }])
71+
})
72+
73+
it('files_with_matches scans whole file (can match across newlines with dot-all style pattern)', () => {
74+
const files = vfsFromEntries([['a.txt', 'foo\nbar']])
75+
const multiline = grep(files, 'foo[\\s\\S]*bar', undefined, {
76+
outputMode: 'files_with_matches',
77+
})
78+
expect(multiline).toContain('a.txt')
79+
80+
const lineOnly = grep(files, 'foo[\\s\\S]*bar', undefined, { outputMode: 'content' })
81+
expect(lineOnly).toHaveLength(0)
82+
})
83+
84+
it('scopes to directory prefix without matching unrelated prefixes', () => {
85+
const files = vfsFromEntries([
86+
['workflows/a/x', 'needle'],
87+
['workflowsManual/x', 'needle'],
88+
])
89+
const hits = grep(files, 'needle', 'workflows', { outputMode: 'files_with_matches' })
90+
expect(hits).toContain('workflows/a/x')
91+
expect(hits).not.toContain('workflowsManual/x')
92+
})
93+
94+
it('scopes with glob pattern when path contains metacharacters', () => {
95+
const files = vfsFromEntries([
96+
['workflows/A/state.json', '{"x":1}'],
97+
['workflows/B/sub/state.json', '{"x":1}'],
98+
['workflows/C/other.json', '{"x":1}'],
99+
])
100+
const hits = grep(files, '1', 'workflows/*/state.json', { outputMode: 'files_with_matches' })
101+
expect(hits).toEqual(['workflows/A/state.json'])
102+
})
103+
104+
it('returns empty array for invalid regex pattern', () => {
105+
const files = vfsFromEntries([['a.txt', 'x']])
106+
expect(grep(files, '(unclosed', undefined, { outputMode: 'content' })).toEqual([])
107+
})
108+
109+
it('respects ignoreCase', () => {
110+
const files = vfsFromEntries([['a.txt', 'Hello']])
111+
const hits = grep(files, 'hello', undefined, { outputMode: 'content', ignoreCase: true })
112+
expect(hits).toHaveLength(1)
113+
})
114+
})

apps/sim/lib/copilot/vfs/operations.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ const VFS_GLOB_OPTIONS: micromatch.Options = {
4545
noext: true,
4646
}
4747

48-
/**
49-
* Returns true when `filePath` is `scope` or a descendant path (`scope/...`), matching how
50-
* `grep -r pattern dir` limits to a directory. If `scope` looks like a glob, filters with
51-
* micromatch `isMatch` and {@link VFS_GLOB_OPTIONS}.
52-
*/
5348
/**
5449
* Splits VFS text into lines for line-oriented grep. Strips a trailing CR so Windows-style
5550
* CRLF payloads still match patterns anchored at line end (`$`).
@@ -58,6 +53,10 @@ function splitLinesForGrep(content: string): string[] {
5853
return content.split('\n').map((line) => line.replace(/\r$/, ''))
5954
}
6055

56+
/**
57+
* Returns true when `filePath` is `scope` or a descendant path (`scope/...`). If `scope` looks
58+
* like a glob, filters with micromatch `isMatch` and `VFS_GLOB_OPTIONS`.
59+
*/
6160
function pathWithinGrepScope(filePath: string, scope: string): boolean {
6261
const looksLikeGlob =
6362
/[*?[{]/.test(scope) || scope.includes('!(') || scope.includes('@(') || scope.includes('+(')

0 commit comments

Comments
 (0)