prjct-cli 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Tests for HierarchicalAgentResolver
3
+ * PRJ-101: Hierarchical scope system
4
+ */
5
+
6
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
7
+ import fs from 'node:fs/promises'
8
+ import os from 'node:os'
9
+ import path from 'node:path'
10
+ import HierarchicalAgentResolver from '../../services/hierarchical-agent-resolver'
11
+
12
+ let testDir: string
13
+ let resolver: HierarchicalAgentResolver
14
+
15
+ beforeEach(async () => {
16
+ testDir = path.join(os.tmpdir(), `prjct-har-test-${Date.now()}`)
17
+ await fs.mkdir(testDir, { recursive: true })
18
+ resolver = new HierarchicalAgentResolver(testDir)
19
+ })
20
+
21
+ afterEach(async () => {
22
+ try {
23
+ await fs.rm(testDir, { recursive: true, force: true })
24
+ } catch {
25
+ // Ignore cleanup errors
26
+ }
27
+ })
28
+
29
+ // =============================================================================
30
+ // Basic Resolution Tests
31
+ // =============================================================================
32
+
33
+ describe('HierarchicalAgentResolver - Basic', () => {
34
+ test('resolves agents from root AGENTS.md', async () => {
35
+ await fs.writeFile(
36
+ path.join(testDir, 'AGENTS.md'),
37
+ `## Backend
38
+
39
+ Backend development specialist.
40
+
41
+ ### Triggers
42
+ - api
43
+ - endpoint
44
+ - server
45
+
46
+ ### Rules
47
+ - Use async/await
48
+ - Validate inputs
49
+ `
50
+ )
51
+
52
+ const result = await resolver.resolveAgentsForPath(testDir)
53
+
54
+ expect(result.agents).toHaveLength(1)
55
+ expect(result.agents[0].name).toBe('Backend')
56
+ expect(result.agents[0].description).toBe('Backend development specialist.')
57
+ expect(result.agents[0].triggers).toContain('api')
58
+ expect(result.agents[0].rules).toContain('Use async/await')
59
+ })
60
+
61
+ test('returns empty result when no AGENTS.md exists', async () => {
62
+ const result = await resolver.resolveAgentsForPath(testDir)
63
+
64
+ expect(result.agents).toHaveLength(0)
65
+ expect(result.discoveredFiles).toHaveLength(0)
66
+ })
67
+
68
+ test('tracks discovered files', async () => {
69
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Agent\n\nTest.')
70
+
71
+ const result = await resolver.resolveAgentsForPath(testDir)
72
+
73
+ expect(result.discoveredFiles).toHaveLength(1)
74
+ expect(result.discoveredFiles[0]).toBe(path.join(testDir, 'AGENTS.md'))
75
+ })
76
+
77
+ test('resolveRootAgents uses root path', async () => {
78
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Root\n\nRoot agent.')
79
+
80
+ const subDir = path.join(testDir, 'sub')
81
+ await fs.mkdir(subDir)
82
+ await fs.writeFile(path.join(subDir, 'AGENTS.md'), '## Sub\n\nSub agent.')
83
+
84
+ // resolveRootAgents should return root agents
85
+ const result = await resolver.resolveRootAgents()
86
+
87
+ // Root has only 1 agent (no inheritance from children)
88
+ expect(result.agents).toHaveLength(1)
89
+ expect(result.agents[0].name).toBe('Root')
90
+ })
91
+ })
92
+
93
+ // =============================================================================
94
+ // Get Agent By Name Tests
95
+ // =============================================================================
96
+
97
+ describe('HierarchicalAgentResolver - getAgentByName', () => {
98
+ test('finds agent by name', async () => {
99
+ await fs.writeFile(
100
+ path.join(testDir, 'AGENTS.md'),
101
+ `## Frontend
102
+
103
+ Frontend specialist.
104
+
105
+ ## Backend
106
+
107
+ Backend specialist.
108
+ `
109
+ )
110
+
111
+ const agent = await resolver.getAgentByName('Backend')
112
+
113
+ expect(agent).not.toBeNull()
114
+ expect(agent?.name).toBe('Backend')
115
+ })
116
+
117
+ test('returns null for non-existent agent', async () => {
118
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Frontend\n\nFrontend.')
119
+
120
+ const agent = await resolver.getAgentByName('NonExistent')
121
+
122
+ expect(agent).toBeNull()
123
+ })
124
+
125
+ test('case-insensitive search', async () => {
126
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Backend\n\nBackend.')
127
+
128
+ const agent = await resolver.getAgentByName('BACKEND')
129
+
130
+ expect(agent).not.toBeNull()
131
+ expect(agent?.name).toBe('Backend')
132
+ })
133
+ })
134
+
135
+ // =============================================================================
136
+ // Get All Agent Names Tests
137
+ // =============================================================================
138
+
139
+ describe('HierarchicalAgentResolver - getAllAgentNames', () => {
140
+ test('returns all unique agent names', async () => {
141
+ await fs.writeFile(
142
+ path.join(testDir, 'AGENTS.md'),
143
+ `## Alpha
144
+
145
+ Alpha agent.
146
+
147
+ ## Beta
148
+
149
+ Beta agent.
150
+ `
151
+ )
152
+
153
+ const names = await resolver.getAllAgentNames()
154
+
155
+ expect(names).toEqual(['Alpha', 'Beta'])
156
+ })
157
+
158
+ test('includes agents from nested files', async () => {
159
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Root\n\nRoot.')
160
+
161
+ const subDir = path.join(testDir, 'sub')
162
+ await fs.mkdir(subDir)
163
+ await fs.writeFile(path.join(subDir, 'AGENTS.md'), '## Sub\n\nSub.')
164
+
165
+ const names = await resolver.getAllAgentNames()
166
+
167
+ expect(names.sort()).toEqual(['Root', 'Sub'])
168
+ })
169
+
170
+ test('deduplicates agent names', async () => {
171
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Shared\n\nRoot shared.')
172
+
173
+ const subDir = path.join(testDir, 'sub')
174
+ await fs.mkdir(subDir)
175
+ await fs.writeFile(path.join(subDir, 'AGENTS.md'), '## Shared\n\nSub shared.')
176
+
177
+ const names = await resolver.getAllAgentNames()
178
+
179
+ expect(names).toEqual(['Shared'])
180
+ })
181
+ })
182
+
183
+ // =============================================================================
184
+ // Agent Existence Check Tests
185
+ // =============================================================================
186
+
187
+ describe('HierarchicalAgentResolver - agentExists', () => {
188
+ test('returns true for existing agent', async () => {
189
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Frontend\n\nFrontend.')
190
+
191
+ const exists = await resolver.agentExists('Frontend')
192
+
193
+ expect(exists).toBe(true)
194
+ })
195
+
196
+ test('returns false for non-existing agent', async () => {
197
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Frontend\n\nFrontend.')
198
+
199
+ const exists = await resolver.agentExists('Backend')
200
+
201
+ expect(exists).toBe(false)
202
+ })
203
+
204
+ test('case-insensitive check', async () => {
205
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Frontend\n\nFrontend.')
206
+
207
+ const exists = await resolver.agentExists('FRONTEND')
208
+
209
+ expect(exists).toBe(true)
210
+ })
211
+ })
212
+
213
+ // =============================================================================
214
+ // Markdown Generation Tests
215
+ // =============================================================================
216
+
217
+ describe('HierarchicalAgentResolver - generateAgentMarkdown', () => {
218
+ test('generates markdown with all sections', async () => {
219
+ await fs.writeFile(
220
+ path.join(testDir, 'AGENTS.md'),
221
+ `## Backend
222
+
223
+ Backend development specialist.
224
+
225
+ ### Domain
226
+ backend
227
+
228
+ ### Triggers
229
+ - api
230
+ - server
231
+
232
+ ### Rules
233
+ - Use TypeScript
234
+ - Log errors
235
+
236
+ ### Patterns
237
+ \`\`\`typescript
238
+ export async function handler() {}
239
+ \`\`\`
240
+ `
241
+ )
242
+
243
+ const result = await resolver.resolveAgentsForPath(testDir)
244
+ const md = resolver.generateAgentMarkdown(result.agents[0])
245
+
246
+ expect(md).toContain('# Backend Agent')
247
+ expect(md).toContain('Backend development specialist.')
248
+ expect(md).toContain('## DOMAIN AUTHORITY')
249
+ expect(md).toContain('backend domain')
250
+ expect(md).toContain('## Triggers')
251
+ expect(md).toContain('- api')
252
+ expect(md).toContain('## Rules')
253
+ expect(md).toContain('- Use TypeScript')
254
+ expect(md).toContain('## Patterns')
255
+ })
256
+
257
+ test('includes source attribution', async () => {
258
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Agent\n\nTest agent.')
259
+
260
+ const result = await resolver.resolveAgentsForPath(testDir)
261
+ const md = resolver.generateAgentMarkdown(result.agents[0])
262
+
263
+ expect(md).toContain('*Resolved from:')
264
+ expect(md).toContain('AGENTS.md')
265
+ })
266
+ })
267
+
268
+ // =============================================================================
269
+ // Hierarchical Resolution Tests
270
+ // =============================================================================
271
+
272
+ describe('HierarchicalAgentResolver - Hierarchy', () => {
273
+ test('merges triggers from parent and child', async () => {
274
+ await fs.writeFile(
275
+ path.join(testDir, 'AGENTS.md'),
276
+ `## Shared
277
+
278
+ Base agent.
279
+
280
+ ### Triggers
281
+ - root-trigger
282
+ `
283
+ )
284
+
285
+ const childDir = path.join(testDir, 'child')
286
+ await fs.mkdir(childDir)
287
+ await fs.writeFile(
288
+ path.join(childDir, 'AGENTS.md'),
289
+ `## Shared
290
+
291
+ Extended agent.
292
+
293
+ ### Triggers
294
+ - child-trigger
295
+ `
296
+ )
297
+
298
+ const result = await resolver.resolveAgentsForPath(childDir)
299
+
300
+ expect(result.agents[0].triggers).toContain('root-trigger')
301
+ expect(result.agents[0].triggers).toContain('child-trigger')
302
+ })
303
+
304
+ test('tracks overridden agents', async () => {
305
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Agent\n\nRoot.')
306
+
307
+ const childDir = path.join(testDir, 'child')
308
+ await fs.mkdir(childDir)
309
+ await fs.writeFile(path.join(childDir, 'AGENTS.md'), '## Agent @override\n\nChild override.')
310
+
311
+ const result = await resolver.resolveAgentsForPath(childDir)
312
+
313
+ expect(result.overriddenAgents).toContain('Agent')
314
+ expect(result.agents[0].wasOverridden).toBe(true)
315
+ })
316
+
317
+ test('path-specific resolution', async () => {
318
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Global\n\nGlobal.')
319
+
320
+ const apiDir = path.join(testDir, 'packages', 'api')
321
+ await fs.mkdir(apiDir, { recursive: true })
322
+ await fs.writeFile(path.join(apiDir, 'AGENTS.md'), '## API\n\nAPI specific.')
323
+
324
+ const webDir = path.join(testDir, 'packages', 'web')
325
+ await fs.mkdir(webDir, { recursive: true })
326
+ await fs.writeFile(path.join(webDir, 'AGENTS.md'), '## Web\n\nWeb specific.')
327
+
328
+ // Resolve for API path
329
+ const apiResult = await resolver.resolveAgentsForPath(apiDir)
330
+ expect(apiResult.agents.map((a) => a.name).sort()).toEqual(['API', 'Global'])
331
+
332
+ // Resolve for Web path
333
+ const webResult = await resolver.resolveAgentsForPath(webDir)
334
+ expect(webResult.agents.map((a) => a.name).sort()).toEqual(['Global', 'Web'])
335
+ })
336
+ })
337
+
338
+ // =============================================================================
339
+ // Agent File Tree Tests
340
+ // =============================================================================
341
+
342
+ describe('HierarchicalAgentResolver - getAgentFileTree', () => {
343
+ test('returns hierarchical tree structure', async () => {
344
+ await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Root\n\nRoot.')
345
+
346
+ const childDir = path.join(testDir, 'child')
347
+ await fs.mkdir(childDir)
348
+ await fs.writeFile(path.join(childDir, 'AGENTS.md'), '## Child\n\nChild.')
349
+
350
+ const tree = await resolver.getAgentFileTree()
351
+
352
+ expect(tree).toHaveLength(2)
353
+ const root = tree.find((n) => n.depth === 0)
354
+ const child = tree.find((n) => n.depth > 0)
355
+
356
+ expect(root?.children).toContain(child)
357
+ expect(child?.parent).toBe(root)
358
+ })
359
+ })