Skip to content

Commit 3d4ff26

Browse files
committed
test: add coverage for parseJsonFilter and generateSearchGuide
Also update README search examples to show --filter and --guide flags.
1 parent 2b39cec commit 3d4ff26

2 files changed

Lines changed: 95 additions & 2 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ skilld update tailwindcss
152152

153153
# Search docs across installed skills
154154
skilld search "useFetch options" -p nuxt
155+
skilld search "error" -p nuxt --filter '{"type":"issue"}'
156+
skilld search --guide -p nuxt
155157

156158
# Target a specific agent
157159
skilld add react --agent cursor
@@ -180,7 +182,7 @@ skilld config
180182
| `skilld` | Interactive wizard (first run) or status menu (existing skills) |
181183
| `skilld add <pkg...>` | Add skills for package(s), space or comma-separated |
182184
| `skilld update [pkg]` | Update outdated skills (all or specific) |
183-
| `skilld search <query>` | Search indexed docs (`-p` to filter by package) |
185+
| `skilld search <query>` | Search indexed docs (`-p` package, `--filter` JSON, `--limit`, `--guide`) |
184186
| `skilld list` | List installed skills (`--json` for machine-readable output) |
185187
| `skilld info` | Show skill info and config |
186188
| `skilld config` | Configure agent, model, preferences |

test/unit/search.test.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { SearchSnippet } from '../../src/retriv/types'
22
import { describe, expect, it } from 'vitest'
3-
import { parseFilterPrefix } from '../../src/commands/search'
3+
import { generateSearchGuide, parseFilterPrefix, parseJsonFilter } from '../../src/commands/search'
44
import { normalizeScores, scoreLabel } from '../../src/core/formatting'
55

66
function snippet(overrides: Partial<SearchSnippet> = {}): SearchSnippet {
@@ -57,6 +57,97 @@ describe('parseFilterPrefix', () => {
5757
})
5858
})
5959

60+
describe('parseJsonFilter', () => {
61+
it('parses valid JSON object', () => {
62+
expect(parseJsonFilter('{"type":"issue"}')).toEqual({ type: 'issue' })
63+
})
64+
65+
it('parses filter with operators', () => {
66+
expect(parseJsonFilter('{"type":{"$in":["doc","issue"]}}')).toEqual({
67+
type: { $in: ['doc', 'issue'] },
68+
})
69+
})
70+
71+
it('parses $prefix operator', () => {
72+
expect(parseJsonFilter('{"source":{"$prefix":"docs/api/"}}')).toEqual({
73+
source: { $prefix: 'docs/api/' },
74+
})
75+
})
76+
77+
it('parses numeric operators', () => {
78+
expect(parseJsonFilter('{"number":{"$gt":100}}')).toEqual({
79+
number: { $gt: 100 },
80+
})
81+
})
82+
83+
it('parses $exists operator', () => {
84+
expect(parseJsonFilter('{"number":{"$exists":true}}')).toEqual({
85+
number: { $exists: true },
86+
})
87+
})
88+
89+
it('parses multiple fields', () => {
90+
expect(parseJsonFilter('{"type":"issue","number":{"$lt":50}}')).toEqual({
91+
type: 'issue',
92+
number: { $lt: 50 },
93+
})
94+
})
95+
96+
it('returns null for invalid JSON', () => {
97+
expect(parseJsonFilter('not json')).toBeNull()
98+
})
99+
100+
it('returns null for JSON array', () => {
101+
expect(parseJsonFilter('[1,2,3]')).toBeNull()
102+
})
103+
104+
it('returns null for JSON string', () => {
105+
expect(parseJsonFilter('"hello"')).toBeNull()
106+
})
107+
108+
it('returns null for JSON number', () => {
109+
expect(parseJsonFilter('42')).toBeNull()
110+
})
111+
112+
it('returns null for JSON null', () => {
113+
expect(parseJsonFilter('null')).toBeNull()
114+
})
115+
})
116+
117+
describe('generateSearchGuide', () => {
118+
it('generates generic guide without package', () => {
119+
const guide = generateSearchGuide()
120+
expect(guide).toContain('skilld search guide')
121+
expect(guide).toContain('-p <package>')
122+
expect(guide).toContain('$prefix')
123+
expect(guide).toContain('$in')
124+
expect(guide).toContain('--filter')
125+
expect(guide).toContain('--limit')
126+
})
127+
128+
it('tailors guide to specific package', () => {
129+
const guide = generateSearchGuide('vue')
130+
expect(guide).toContain('Search guide for vue')
131+
expect(guide).toContain('-p vue')
132+
expect(guide).toContain('e.g. "vue"')
133+
expect(guide).not.toContain('<package>')
134+
})
135+
136+
it('includes all metadata fields', () => {
137+
const guide = generateSearchGuide()
138+
expect(guide).toContain('package')
139+
expect(guide).toContain('source')
140+
expect(guide).toContain('type')
141+
expect(guide).toContain('number')
142+
})
143+
144+
it('includes all filter operators', () => {
145+
const guide = generateSearchGuide()
146+
for (const op of ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$prefix', '$exists'])
147+
expect(guide).toContain(op)
148+
})
149+
})
150+
60151
describe('normalizeScores', () => {
61152
it('normalizes best result to 100', () => {
62153
const results = [snippet({ score: 0.08 }), snippet({ score: 0.04 }), snippet({ score: 0.02 })]

0 commit comments

Comments
 (0)