microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/context-working

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/tests/collections/Validate-Collections.Tests.ps1

1375lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4
5BeforeAll {
6 . $PSScriptRoot/../../collections/Validate-Collections.ps1
7}
8
9Describe 'Test-KindSuffix' {
10 It 'Returns empty for valid agent path' {
11 $result = Test-KindSuffix -Kind 'agent' -ItemPath '.github/agents/rpi-agent.agent.md' -RepoRoot $TestDrive
12 $result | Should -BeNullOrEmpty
13 }
14
15 It 'Returns empty for valid prompt path' {
16 $result = Test-KindSuffix -Kind 'prompt' -ItemPath '.github/prompts/gen-plan.prompt.md' -RepoRoot $TestDrive
17 $result | Should -BeNullOrEmpty
18 }
19
20 It 'Returns empty for valid instruction path' {
21 $result = Test-KindSuffix -Kind 'instruction' -ItemPath '.github/instructions/csharp.instructions.md' -RepoRoot $TestDrive
22 $result | Should -BeNullOrEmpty
23 }
24
25 It 'Returns empty for valid skill path with SKILL.md' {
26 $skillDir = Join-Path $TestDrive '.github/skills/video-to-gif'
27 New-Item -ItemType Directory -Path $skillDir -Force | Out-Null
28 Set-Content -Path (Join-Path $skillDir 'SKILL.md') -Value '# Skill'
29
30 $result = Test-KindSuffix -Kind 'skill' -ItemPath '.github/skills/video-to-gif' -RepoRoot $TestDrive
31 $result | Should -BeNullOrEmpty
32 }
33
34 It 'Returns error for invalid agent suffix' {
35 $result = Test-KindSuffix -Kind 'agent' -ItemPath '.github/agents/bad.prompt.md' -RepoRoot $TestDrive
36 $result | Should -Match "kind 'agent' expects"
37 }
38
39 It 'Returns error for invalid prompt suffix' {
40 $result = Test-KindSuffix -Kind 'prompt' -ItemPath '.github/prompts/bad.agent.md' -RepoRoot $TestDrive
41 $result | Should -Match "kind 'prompt' expects"
42 }
43
44 It 'Returns error when SKILL.md missing for skill kind' {
45 $emptySkillDir = Join-Path $TestDrive '.github/skills/no-skill'
46 New-Item -ItemType Directory -Path $emptySkillDir -Force | Out-Null
47
48 $result = Test-KindSuffix -Kind 'skill' -ItemPath '.github/skills/no-skill' -RepoRoot $TestDrive
49 $result | Should -Match "kind 'skill' expects SKILL.md"
50 }
51}
52
53Describe 'Get-CollectionItemKey' {
54 It 'Builds correct composite key' {
55 $result = Get-CollectionItemKey -Kind 'agent' -ItemPath '.github/agents/rpi-agent.agent.md'
56 $result | Should -Be 'agent|.github/agents/rpi-agent.agent.md'
57 }
58
59 It 'Builds key for instruction kind' {
60 $result = Get-CollectionItemKey -Kind 'instruction' -ItemPath '.github/instructions/csharp.instructions.md'
61 $result | Should -Be 'instruction|.github/instructions/csharp.instructions.md'
62 }
63}
64
65Describe 'Invoke-CollectionValidation - repo-specific path rejection' {
66 BeforeAll {
67 Import-Module PowerShell-Yaml -ErrorAction Stop
68
69 $script:repoRoot = Join-Path $TestDrive 'repo'
70 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
71
72 # Create artifact directories and files
73 $instrDir = Join-Path $script:repoRoot '.github/instructions'
74 $agentsDir = Join-Path $script:repoRoot '.github/agents'
75 $sharedDir = Join-Path $instrDir 'shared'
76 $hveCoreAgentsDir = Join-Path $agentsDir 'hve-core'
77
78 New-Item -ItemType Directory -Path $instrDir -Force | Out-Null
79 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
80 New-Item -ItemType Directory -Path $sharedDir -Force | Out-Null
81 New-Item -ItemType Directory -Path $hveCoreAgentsDir -Force | Out-Null
82
83 # Root-level (repo-specific) files
84 Set-Content -Path (Join-Path $instrDir 'workflows.instructions.md') -Value '---\ndescription: repo-specific\n---'
85 Set-Content -Path (Join-Path $agentsDir 'internal.agent.md') -Value '---\ndescription: repo-specific agent\n---'
86
87 # Subdirectory (collection-scoped) files
88 Set-Content -Path (Join-Path $sharedDir 'hve-core-location.instructions.md') -Value '---\ndescription: shared\n---'
89 Set-Content -Path (Join-Path $hveCoreAgentsDir 'rpi-agent.agent.md') -Value '---\ndescription: distributable agent\n---'
90 }
91
92 BeforeEach {
93 # Clear collection files between tests to prevent cross-contamination
94 if (Test-Path $script:collectionsDir) {
95 Remove-Item -Path $script:collectionsDir -Recurse -Force
96 }
97 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
98 }
99
100 It 'Fails validation for root-level instruction' {
101 $manifest = [ordered]@{
102 id = 'test-reject-instr'
103 name = 'Test Reject Instruction'
104 description = 'Tests repo-specific instruction rejection'
105 items = @(
106 [ordered]@{
107 path = '.github/instructions/workflows.instructions.md'
108 kind = 'instruction'
109 }
110 )
111 }
112 $yaml = ConvertTo-Yaml -Data $manifest
113 Set-Content -Path (Join-Path $script:collectionsDir 'test-reject-instr.collection.yml') -Value $yaml
114
115 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
116 $result.Success | Should -BeFalse
117 $result.ErrorCount | Should -BeGreaterOrEqual 1
118 }
119
120 It 'Passes validation for instruction in subdirectory' {
121 $manifest = [ordered]@{
122 id = 'test-allow-location'
123 name = 'Test Allow Location'
124 description = 'Tests that subdirectory instructions are allowed'
125 items = @(
126 [ordered]@{
127 path = '.github/instructions/shared/hve-core-location.instructions.md'
128 kind = 'instruction'
129 }
130 )
131 }
132 $yaml = ConvertTo-Yaml -Data $manifest
133 Set-Content -Path (Join-Path $script:collectionsDir 'test-allow-location.collection.yml') -Value $yaml
134
135 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
136 $result.Success | Should -BeTrue
137 }
138
139 It 'Fails validation for root-level agent' {
140 $manifest = [ordered]@{
141 id = 'test-reject-agent'
142 name = 'Test Reject Agent'
143 description = 'Tests repo-specific agent rejection'
144 items = @(
145 [ordered]@{
146 path = '.github/agents/internal.agent.md'
147 kind = 'agent'
148 }
149 )
150 }
151 $yaml = ConvertTo-Yaml -Data $manifest
152 Set-Content -Path (Join-Path $script:collectionsDir 'test-reject-agent.collection.yml') -Value $yaml
153
154 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
155 $result.Success | Should -BeFalse
156 $result.ErrorCount | Should -BeGreaterOrEqual 1
157 }
158
159 It 'Passes validation for agent in subdirectory' {
160 $manifest = [ordered]@{
161 id = 'test-allow-agent'
162 name = 'Test Allow Agent'
163 description = 'Tests that subdirectory agents pass'
164 items = @(
165 [ordered]@{
166 path = '.github/agents/hve-core/rpi-agent.agent.md'
167 kind = 'agent'
168 }
169 )
170 }
171 $yaml = ConvertTo-Yaml -Data $manifest
172 Set-Content -Path (Join-Path $script:collectionsDir 'test-allow-agent.collection.yml') -Value $yaml
173
174 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
175 $result.Success | Should -BeTrue
176 }
177}
178
179Describe 'Invoke-CollectionValidation - collection-level maturity' {
180 BeforeAll {
181 Import-Module PowerShell-Yaml -ErrorAction Stop
182
183 $script:repoRoot = Join-Path $TestDrive 'maturity-repo'
184 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
185
186 # Create a valid artifact for items to reference
187 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
188 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
189 Set-Content -Path (Join-Path $agentsDir 'test.agent.md') -Value '---\ndescription: test agent\n---'
190 }
191
192 BeforeEach {
193 if (Test-Path $script:collectionsDir) {
194 Remove-Item -Path $script:collectionsDir -Recurse -Force
195 }
196 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
197 }
198
199 It 'Passes validation for collection with maturity: experimental' {
200 $manifest = [ordered]@{
201 id = 'test-maturity-experimental'
202 name = 'Test'
203 description = 'Tests experimental maturity'
204 maturity = 'experimental'
205 items = @(
206 [ordered]@{
207 path = '.github/agents/test/test.agent.md'
208 kind = 'agent'
209 }
210 )
211 }
212 $yaml = ConvertTo-Yaml -Data $manifest
213 Set-Content -Path (Join-Path $script:collectionsDir 'test-maturity-experimental.collection.yml') -Value $yaml
214
215 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
216 $result.Success | Should -BeTrue
217 }
218
219 It 'Passes validation for collection with maturity: stable' {
220 $manifest = [ordered]@{
221 id = 'test-maturity-stable'
222 name = 'Test'
223 description = 'Tests stable maturity'
224 maturity = 'stable'
225 items = @(
226 [ordered]@{
227 path = '.github/agents/test/test.agent.md'
228 kind = 'agent'
229 }
230 )
231 }
232 $yaml = ConvertTo-Yaml -Data $manifest
233 Set-Content -Path (Join-Path $script:collectionsDir 'test-maturity-stable.collection.yml') -Value $yaml
234
235 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
236 $result.Success | Should -BeTrue
237 }
238
239 It 'Passes validation for collection with maturity: preview' {
240 $manifest = [ordered]@{
241 id = 'test-maturity-preview'
242 name = 'Test'
243 description = 'Tests preview maturity'
244 maturity = 'preview'
245 items = @(
246 [ordered]@{
247 path = '.github/agents/test/test.agent.md'
248 kind = 'agent'
249 }
250 )
251 }
252 $yaml = ConvertTo-Yaml -Data $manifest
253 Set-Content -Path (Join-Path $script:collectionsDir 'test-maturity-preview.collection.yml') -Value $yaml
254
255 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
256 $result.Success | Should -BeTrue
257 }
258
259 It 'Passes validation for collection with maturity: deprecated' {
260 $manifest = [ordered]@{
261 id = 'test-maturity-deprecated'
262 name = 'Test'
263 description = 'Tests deprecated maturity'
264 maturity = 'deprecated'
265 items = @(
266 [ordered]@{
267 path = '.github/agents/test/test.agent.md'
268 kind = 'agent'
269 }
270 )
271 }
272 $yaml = ConvertTo-Yaml -Data $manifest
273 Set-Content -Path (Join-Path $script:collectionsDir 'test-maturity-deprecated.collection.yml') -Value $yaml
274
275 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
276 $result.Success | Should -BeTrue
277 }
278
279 It 'Fails validation for collection with invalid maturity: beta' {
280 $manifest = [ordered]@{
281 id = 'test-maturity-beta'
282 name = 'Test'
283 description = 'Tests invalid maturity'
284 maturity = 'beta'
285 items = @(
286 [ordered]@{
287 path = '.github/agents/test/test.agent.md'
288 kind = 'agent'
289 }
290 )
291 }
292 $yaml = ConvertTo-Yaml -Data $manifest
293 Set-Content -Path (Join-Path $script:collectionsDir 'test-maturity-beta.collection.yml') -Value $yaml
294
295 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
296 $result.Success | Should -BeFalse
297 $result.ErrorCount | Should -BeGreaterOrEqual 1
298 }
299
300 It 'Passes validation for collection with omitted maturity' {
301 $manifest = [ordered]@{
302 id = 'test-maturity-omitted'
303 name = 'Test'
304 description = 'Tests omitted maturity'
305 items = @(
306 [ordered]@{
307 path = '.github/agents/test/test.agent.md'
308 kind = 'agent'
309 }
310 )
311 }
312 $yaml = ConvertTo-Yaml -Data $manifest
313 Set-Content -Path (Join-Path $script:collectionsDir 'test-maturity-omitted.collection.yml') -Value $yaml
314
315 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
316 $result.Success | Should -BeTrue
317 }
318}
319
320Describe 'Invoke-CollectionValidation - collection-to-folder name consistency' {
321 BeforeAll {
322 Import-Module PowerShell-Yaml -ErrorAction Stop
323
324 $script:repoRoot = Join-Path $TestDrive 'folder-consistency-repo'
325 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
326
327 # Matching folder structure
328 $matchDir = Join-Path $script:repoRoot '.github/agents/my-collection'
329 New-Item -ItemType Directory -Path $matchDir -Force | Out-Null
330 Set-Content -Path (Join-Path $matchDir 'match.agent.md') -Value '---\ndescription: matching agent\n---'
331
332 # Mismatched folder structure
333 $mismatchDir = Join-Path $script:repoRoot '.github/agents/wrong-folder'
334 New-Item -ItemType Directory -Path $mismatchDir -Force | Out-Null
335 Set-Content -Path (Join-Path $mismatchDir 'mismatch.agent.md') -Value '---\ndescription: mismatched agent\n---'
336
337 # Shared folder structure
338 $sharedDir = Join-Path $script:repoRoot '.github/instructions/shared'
339 New-Item -ItemType Directory -Path $sharedDir -Force | Out-Null
340 Set-Content -Path (Join-Path $sharedDir 'shared.instructions.md') -Value '---\ndescription: shared instruction\n---'
341
342 # rai-planning sub-domain folder structure (shared across themed collections)
343 $raiPlanningDir = Join-Path $script:repoRoot '.github/instructions/rai-planning'
344 New-Item -ItemType Directory -Path $raiPlanningDir -Force | Out-Null
345 Set-Content -Path (Join-Path $raiPlanningDir 'rai.instructions.md') -Value '---\ndescription: rai-planning instruction\n---'
346
347 # hve-core folder structure (cross-collection reference allowed without warning)
348 $hveCoreDir = Join-Path $script:repoRoot '.github/agents/hve-core'
349 New-Item -ItemType Directory -Path $hveCoreDir -Force | Out-Null
350 Set-Content -Path (Join-Path $hveCoreDir 'core.agent.md') -Value '---\ndescription: hve-core agent\n---'
351 }
352
353 BeforeEach {
354 if (Test-Path $script:collectionsDir) {
355 Remove-Item -Path $script:collectionsDir -Recurse -Force
356 }
357 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
358 }
359
360 It 'Passes when collection-id matches folder name' {
361 Mock Write-Host {}
362
363 $manifest = [ordered]@{
364 id = 'my-collection'
365 name = 'My Collection'
366 description = 'Collection with matching folder'
367 items = @(
368 [ordered]@{
369 path = '.github/agents/my-collection/match.agent.md'
370 kind = 'agent'
371 }
372 )
373 }
374 $yaml = ConvertTo-Yaml -Data $manifest
375 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
376
377 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
378 $result.Success | Should -BeTrue
379 $result.ErrorCount | Should -Be 0
380 Should -Not -Invoke Write-Host -ParameterFilter {
381 $Object -match 'WARN collection.*my-collection'
382 }
383 }
384
385 It 'Warns but does not fail when collection-id does not match folder name' {
386 Mock Write-Host {}
387
388 $manifest = [ordered]@{
389 id = 'my-collection'
390 name = 'My Collection'
391 description = 'Collection with mismatched folder'
392 items = @(
393 [ordered]@{
394 path = '.github/agents/wrong-folder/mismatch.agent.md'
395 kind = 'agent'
396 }
397 )
398 }
399 $yaml = ConvertTo-Yaml -Data $manifest
400 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
401
402 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
403 $result.Success | Should -BeTrue
404 $result.ErrorCount | Should -Be 0
405 Should -Invoke Write-Host -ParameterFilter {
406 $Object -match 'WARN collection.*wrong-folder'
407 }
408 }
409
410 It 'Allows items from hve-core/ folder in any collection' {
411 Mock Write-Host {}
412
413 $manifest = [ordered]@{
414 id = 'my-collection'
415 name = 'My Collection'
416 description = 'Collection referencing hve-core item'
417 items = @(
418 [ordered]@{
419 path = '.github/agents/hve-core/core.agent.md'
420 kind = 'agent'
421 }
422 )
423 }
424 $yaml = ConvertTo-Yaml -Data $manifest
425 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
426
427 # Register hve-core as a known collection ID (mirrors real-world hve-core.collection.yml)
428 $hveCoreManifest = [ordered]@{
429 id = 'hve-core'
430 name = 'HVE Core'
431 description = 'HVE Core collection'
432 items = @()
433 }
434 $hveYaml = ConvertTo-Yaml -Data $hveCoreManifest
435 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core.collection.yml') -Value $hveYaml
436 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core.collection.md') -Value '# HVE Core'
437
438 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
439 $result.Success | Should -BeTrue
440 $result.ErrorCount | Should -Be 0
441 Should -Not -Invoke Write-Host -ParameterFilter {
442 $Object -match 'WARN collection'
443 }
444 }
445
446 It 'Allows items from shared/ folder in any collection' {
447 Mock Write-Host {}
448
449 $manifest = [ordered]@{
450 id = 'my-collection'
451 name = 'My Collection'
452 description = 'Collection referencing shared item'
453 items = @(
454 [ordered]@{
455 path = '.github/instructions/shared/shared.instructions.md'
456 kind = 'instruction'
457 }
458 )
459 }
460 $yaml = ConvertTo-Yaml -Data $manifest
461 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
462
463 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
464 $result.Success | Should -BeTrue
465 $result.ErrorCount | Should -Be 0
466 Should -Not -Invoke Write-Host -ParameterFilter {
467 $Object -match 'WARN collection'
468 }
469 }
470
471 It 'Allows items from rai-planning/ folder in any collection' {
472 Mock Write-Host {}
473
474 $manifest = [ordered]@{
475 id = 'my-collection'
476 name = 'My Collection'
477 description = 'Collection referencing rai-planning item'
478 items = @(
479 [ordered]@{
480 path = '.github/instructions/rai-planning/rai.instructions.md'
481 kind = 'instruction'
482 }
483 )
484 }
485 $yaml = ConvertTo-Yaml -Data $manifest
486 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
487
488 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
489 $result.Success | Should -BeTrue
490 $result.ErrorCount | Should -Be 0
491 Should -Not -Invoke Write-Host -ParameterFilter {
492 $Object -match 'WARN collection'
493 }
494 }
495
496 It 'Allows hve-core-all to reference items from any folder' {
497 Mock Write-Host {}
498
499 $manifest = [ordered]@{
500 id = 'hve-core-all'
501 name = 'HVE Core All'
502 description = 'Aggregate collection'
503 items = @(
504 [ordered]@{
505 path = '.github/agents/my-collection/match.agent.md'
506 kind = 'agent'
507 },
508 [ordered]@{
509 path = '.github/agents/wrong-folder/mismatch.agent.md'
510 kind = 'agent'
511 },
512 [ordered]@{
513 path = '.github/instructions/shared/shared.instructions.md'
514 kind = 'instruction'
515 },
516 [ordered]@{
517 path = '.github/instructions/rai-planning/rai.instructions.md'
518 kind = 'instruction'
519 },
520 [ordered]@{
521 path = '.github/agents/hve-core/core.agent.md'
522 kind = 'agent'
523 }
524 )
525 }
526 $yaml = ConvertTo-Yaml -Data $manifest
527 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value $yaml
528 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
529
530 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
531 $result.Success | Should -BeTrue
532 $result.ErrorCount | Should -Be 0
533 Should -Not -Invoke Write-Host -ParameterFilter {
534 $Object -match 'WARN collection'
535 }
536 }
537
538 It 'Emits warning output for mismatched folder name without failing' {
539 Mock Write-Host {}
540
541 $manifest = [ordered]@{
542 id = 'my-collection'
543 name = 'My Collection'
544 description = 'Mismatch for warning output test'
545 items = @(
546 [ordered]@{
547 path = '.github/agents/wrong-folder/mismatch.agent.md'
548 kind = 'agent'
549 }
550 )
551 }
552 $yaml = ConvertTo-Yaml -Data $manifest
553 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
554
555 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
556 # Advisory warning uses Write-Host WARN; validation still passes
557 $result.Success | Should -BeTrue
558 $result.ErrorCount | Should -Be 0
559 Should -Invoke Write-Host -ParameterFilter {
560 $Object -match 'WARN collection.*wrong-folder'
561 }
562 }
563}
564
565Describe 'Invoke-CollectionValidation - error paths' {
566 BeforeAll {
567 Import-Module PowerShell-Yaml -ErrorAction Stop
568
569 $script:repoRoot = Join-Path $TestDrive 'error-repo'
570 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
571
572 # Create valid artifacts for reference
573 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
574 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
575 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---\ndescription: agent a\n---'
576 Set-Content -Path (Join-Path $agentsDir 'b.agent.md') -Value '---\ndescription: agent b\n---'
577
578 $instrDir = Join-Path $script:repoRoot '.github/instructions/test'
579 New-Item -ItemType Directory -Path $instrDir -Force | Out-Null
580 Set-Content -Path (Join-Path $instrDir 'test.instructions.md') -Value '---\ndescription: test\n---'
581 }
582
583 BeforeEach {
584 if (Test-Path $script:collectionsDir) {
585 Remove-Item -Path $script:collectionsDir -Recurse -Force
586 }
587 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
588 }
589
590 It 'Returns success with zero collections when directory is empty' {
591 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
592 $result.Success | Should -BeTrue
593 $result.CollectionCount | Should -Be 0
594 }
595
596 It 'Fails when required field is missing' {
597 $yaml = @"
598name: No ID Collection
599description: Missing id field
600items:
601 - path: .github/agents/test/a.agent.md
602 kind: agent
603"@
604 Set-Content -Path (Join-Path $script:collectionsDir 'no-id.collection.yml') -Value $yaml
605
606 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
607 $result.Success | Should -BeFalse
608 }
609
610 It 'Fails for invalid id format' {
611 $manifest = [ordered]@{
612 id = 'INVALID_ID!'
613 name = 'Bad ID'
614 description = 'Invalid id format'
615 items = @(
616 [ordered]@{
617 path = '.github/agents/test/a.agent.md'
618 kind = 'agent'
619 }
620 )
621 }
622 $yaml = ConvertTo-Yaml -Data $manifest
623 Set-Content -Path (Join-Path $script:collectionsDir 'bad-id.collection.yml') -Value $yaml
624
625 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
626 $result.Success | Should -BeFalse
627 }
628
629 It 'Fails for duplicate ids across collections' {
630 $manifest = [ordered]@{
631 id = 'dup-id'
632 name = 'First'
633 description = 'First collection'
634 items = @(
635 [ordered]@{
636 path = '.github/agents/test/a.agent.md'
637 kind = 'agent'
638 }
639 )
640 }
641 $yaml = ConvertTo-Yaml -Data $manifest
642 Set-Content -Path (Join-Path $script:collectionsDir 'dup1.collection.yml') -Value $yaml
643 Set-Content -Path (Join-Path $script:collectionsDir 'dup2.collection.yml') -Value $yaml
644
645 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
646 $result.Success | Should -BeFalse
647 }
648
649 It 'Fails when item path does not exist' {
650 $manifest = [ordered]@{
651 id = 'missing-path'
652 name = 'Missing'
653 description = 'Item path missing'
654 items = @(
655 [ordered]@{
656 path = '.github/agents/test/nonexistent.agent.md'
657 kind = 'agent'
658 }
659 )
660 }
661 $yaml = ConvertTo-Yaml -Data $manifest
662 Set-Content -Path (Join-Path $script:collectionsDir 'missing-path.collection.yml') -Value $yaml
663
664 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
665 $result.Success | Should -BeFalse
666 }
667
668 It 'Fails when item has no kind' {
669 $yaml = @"
670id: no-kind
671name: No Kind
672description: Item missing kind
673items:
674 - path: .github/agents/test/a.agent.md
675"@
676 Set-Content -Path (Join-Path $script:collectionsDir 'no-kind.collection.yml') -Value $yaml
677
678 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
679 $result.Success | Should -BeFalse
680 }
681
682 It 'Fails for invalid item maturity' {
683 $manifest = [ordered]@{
684 id = 'bad-item-mat'
685 name = 'Bad Item Maturity'
686 description = 'Item with invalid maturity'
687 items = @(
688 [ordered]@{
689 path = '.github/agents/test/a.agent.md'
690 kind = 'agent'
691 maturity = 'alpha'
692 }
693 )
694 }
695 $yaml = ConvertTo-Yaml -Data $manifest
696 Set-Content -Path (Join-Path $script:collectionsDir 'bad-item-mat.collection.yml') -Value $yaml
697
698 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
699 $result.Success | Should -BeFalse
700 }
701
702 It 'Fails for kind-suffix mismatch' {
703 $manifest = [ordered]@{
704 id = 'suffix-mismatch'
705 name = 'Suffix Mismatch'
706 description = 'Agent path with wrong suffix'
707 items = @(
708 [ordered]@{
709 path = '.github/instructions/test/test.instructions.md'
710 kind = 'agent'
711 }
712 )
713 }
714 $yaml = ConvertTo-Yaml -Data $manifest
715 Set-Content -Path (Join-Path $script:collectionsDir 'suffix-mismatch.collection.yml') -Value $yaml
716
717 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
718 $result.Success | Should -BeFalse
719 }
720
721 It 'Fails for instruction kind with wrong suffix' {
722 $manifest = [ordered]@{
723 id = 'instr-suffix'
724 name = 'Instruction Suffix'
725 description = 'Instruction item with agent suffix'
726 items = @(
727 [ordered]@{
728 path = '.github/agents/test/a.agent.md'
729 kind = 'instruction'
730 }
731 )
732 }
733 $yaml = ConvertTo-Yaml -Data $manifest
734 Set-Content -Path (Join-Path $script:collectionsDir 'instr-suffix.collection.yml') -Value $yaml
735
736 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
737 $result.Success | Should -BeFalse
738 }
739
740 It 'Detects duplicate artifact keys at distinct paths' {
741 # Two agents at different paths that resolve to the same artifact key
742 $agentsDir2 = Join-Path $script:repoRoot '.github/agents/other'
743 New-Item -ItemType Directory -Path $agentsDir2 -Force | Out-Null
744 Set-Content -Path (Join-Path $agentsDir2 'a.agent.md') -Value '---\ndescription: same name\n---'
745
746 $manifest = [ordered]@{
747 id = 'dup-artifact'
748 name = 'Dup Artifact'
749 description = 'Same artifact key from different paths'
750 items = @(
751 [ordered]@{
752 path = '.github/agents/test/a.agent.md'
753 kind = 'agent'
754 },
755 [ordered]@{
756 path = '.github/agents/other/a.agent.md'
757 kind = 'agent'
758 }
759 )
760 }
761 $yaml = ConvertTo-Yaml -Data $manifest
762 Set-Content -Path (Join-Path $script:collectionsDir 'dup-artifact.collection.yml') -Value $yaml
763
764 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
765 $result.Success | Should -BeFalse
766 }
767
768 It 'Detects shared item missing canonical entry' {
769 # Two collections share the same item but neither is hve-core-all;
770 # hve-core-all exists but does not include a.agent.md - Check 4 fires.
771 $manifest1 = [ordered]@{
772 id = 'share-one'
773 name = 'Share One'
774 description = 'First sharer'
775 items = @(
776 [ordered]@{
777 path = '.github/agents/test/a.agent.md'
778 kind = 'agent'
779 }
780 )
781 }
782 $manifest2 = [ordered]@{
783 id = 'share-two'
784 name = 'Share Two'
785 description = 'Second sharer'
786 items = @(
787 [ordered]@{
788 path = '.github/agents/test/a.agent.md'
789 kind = 'agent'
790 }
791 )
792 }
793 $canonical = [ordered]@{
794 id = 'hve-core-all'
795 name = 'All'
796 description = 'Canonical - missing a.agent.md'
797 items = @(
798 [ordered]@{
799 path = '.github/agents/test/b.agent.md'
800 kind = 'agent'
801 },
802 [ordered]@{
803 path = '.github/instructions/test/test.instructions.md'
804 kind = 'instruction'
805 }
806 )
807 }
808 $yaml1 = ConvertTo-Yaml -Data $manifest1
809 $yaml2 = ConvertTo-Yaml -Data $manifest2
810 $yaml3 = ConvertTo-Yaml -Data $canonical
811 Set-Content -Path (Join-Path $script:collectionsDir 'share-one.collection.yml') -Value $yaml1
812 Set-Content -Path (Join-Path $script:collectionsDir 'share-two.collection.yml') -Value $yaml2
813 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value $yaml3
814 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
815
816 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
817 $result.Success | Should -BeFalse
818 }
819
820 It 'Detects maturity conflict with canonical collection' {
821 # hve-core-all has the item as stable, another collection has it as experimental
822 $canonical = [ordered]@{
823 id = 'hve-core-all'
824 name = 'All'
825 description = 'Canonical collection'
826 items = @(
827 [ordered]@{
828 path = '.github/agents/test/a.agent.md'
829 kind = 'agent'
830 maturity = 'stable'
831 }
832 )
833 }
834 $other = [ordered]@{
835 id = 'conflict-col'
836 name = 'Conflict'
837 description = 'Conflicting maturity'
838 items = @(
839 [ordered]@{
840 path = '.github/agents/test/a.agent.md'
841 kind = 'agent'
842 maturity = 'experimental'
843 }
844 )
845 }
846 $yaml1 = ConvertTo-Yaml -Data $canonical
847 $yaml2 = ConvertTo-Yaml -Data $other
848 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value $yaml1
849 Set-Content -Path (Join-Path $script:collectionsDir 'conflict-col.collection.yml') -Value $yaml2
850
851 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
852 $result.Success | Should -BeFalse
853 }
854}
855
856Describe 'Invoke-CollectionValidation - new checks' {
857 BeforeAll {
858 Import-Module PowerShell-Yaml -ErrorAction Stop
859
860 $script:repoRoot = Join-Path $TestDrive 'new-checks-repo'
861 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
862
863 # Standard artifact - used by most tests
864 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
865 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
866 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---' -Force
867
868 # Orphan artifact - on disk but not necessarily in manifests
869 $orphanDir = Join-Path $script:repoRoot '.github/agents/orphan'
870 New-Item -ItemType Directory -Path $orphanDir -Force | Out-Null
871 Set-Content -Path (Join-Path $orphanDir 'orphan.agent.md') -Value '---' -Force
872 }
873
874 BeforeEach {
875 if (Test-Path $script:collectionsDir) { Remove-Item -Path $script:collectionsDir -Recurse -Force }
876 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
877
878 # Reset agent dirs to pristine state - prevents artifact leakage between tests
879 $agentsBaseDir = Join-Path $script:repoRoot '.github/agents'
880 if (Test-Path $agentsBaseDir) { Remove-Item -Path $agentsBaseDir -Recurse -Force }
881 New-Item -ItemType Directory -Path (Join-Path $agentsBaseDir 'test') -Force | Out-Null
882 Set-Content -Path (Join-Path $agentsBaseDir 'test/a.agent.md') -Value '---' -Force
883 New-Item -ItemType Directory -Path (Join-Path $agentsBaseDir 'orphan') -Force | Out-Null
884 Set-Content -Path (Join-Path $agentsBaseDir 'orphan/orphan.agent.md') -Value '---' -Force
885 }
886
887 # Check 3: companion .collection.md
888
889 It 'Warns but passes when .collection.md companion is missing' {
890 $manifest = [ordered]@{
891 id = 'no-companion'; name = 'No Companion'; description = 'Missing companion md'
892 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
893 }
894 Set-Content -Path (Join-Path $script:collectionsDir 'no-companion.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
895 $canonical = [ordered]@{
896 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
897 items = @(
898 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
899 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
900 )
901 }
902 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
903 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
904
905 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
906 $result.Success | Should -BeTrue
907 $result.ErrorCount | Should -Be 0
908 }
909
910 It 'Passes cleanly when .collection.md companion is present' {
911 $manifest = [ordered]@{
912 id = 'has-companion'; name = 'Has Companion'; description = 'With md'
913 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
914 }
915 Set-Content -Path (Join-Path $script:collectionsDir 'has-companion.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
916 Set-Content -Path (Join-Path $script:collectionsDir 'has-companion.collection.md') -Value '# Has Companion'
917 $canonical = [ordered]@{
918 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
919 items = @(
920 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
921 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
922 )
923 }
924 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
925 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
926
927 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
928 $result.Success | Should -BeTrue
929 }
930
931 # Check 2: intra-collection duplicate
932
933 It 'Fails when the same item appears twice in one collection' {
934 $manifest = [ordered]@{
935 id = 'intra-dup'; name = 'Intra Dup'; description = 'Dup item'
936 items = @(
937 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
938 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' }
939 )
940 }
941 Set-Content -Path (Join-Path $script:collectionsDir 'intra-dup.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
942
943 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
944 $result.Success | Should -BeFalse
945 $result.ErrorCount | Should -BeGreaterOrEqual 1
946 }
947
948 It 'Passes when all items in a collection are distinct' {
949 $agentsDir2 = Join-Path $script:repoRoot '.github/agents/test2'
950 New-Item -ItemType Directory -Path $agentsDir2 -Force | Out-Null
951 Set-Content -Path (Join-Path $agentsDir2 'b.agent.md') -Value '---' -Force
952
953 $manifest = [ordered]@{
954 id = 'distinct-items'; name = 'Distinct'; description = 'Distinct items'
955 items = @(
956 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
957 [ordered]@{ path = '.github/agents/test2/b.agent.md'; kind = 'agent' }
958 )
959 }
960 $canonical = [ordered]@{
961 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
962 items = @(
963 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
964 [ordered]@{ path = '.github/agents/test2/b.agent.md'; kind = 'agent' },
965 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
966 )
967 }
968 Set-Content -Path (Join-Path $script:collectionsDir 'distinct-items.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
969 Set-Content -Path (Join-Path $script:collectionsDir 'distinct-items.collection.md') -Value '# Distinct'
970 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
971 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
972
973 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
974 $result.Success | Should -BeTrue
975 }
976
977 # Check 4: hve-core-all coverage
978
979 It 'Fails when a themed collection item is absent from hve-core-all' {
980 $manifest = [ordered]@{
981 id = 'themed-only'; name = 'Themed Only'; description = 'Item not in hve-core-all'
982 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
983 }
984 # Canonical exists but does NOT include a.agent.md - only orphan - so Check 4 fires
985 $canonical = [ordered]@{
986 id = 'hve-core-all'; name = 'All'; description = 'Canonical - missing themed item'
987 items = @([ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' })
988 }
989 Set-Content -Path (Join-Path $script:collectionsDir 'themed-only.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
990 Set-Content -Path (Join-Path $script:collectionsDir 'themed-only.collection.md') -Value '# Themed'
991 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
992 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
993
994 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
995 $result.Success | Should -BeFalse
996 $result.ErrorCount | Should -BeGreaterOrEqual 1
997 }
998
999 It 'Passes when all themed items are present in hve-core-all' {
1000 $themed = [ordered]@{
1001 id = 'themed-covered'; name = 'Themed Covered'; description = 'Covered by canonical'
1002 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1003 }
1004 $canonical = [ordered]@{
1005 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1006 items = @(
1007 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1008 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1009 )
1010 }
1011 Set-Content -Path (Join-Path $script:collectionsDir 'themed-covered.collection.yml') -Value (ConvertTo-Yaml -Data $themed)
1012 Set-Content -Path (Join-Path $script:collectionsDir 'themed-covered.collection.md') -Value '# Themed Covered'
1013 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1014 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1015
1016 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1017 $result.Success | Should -BeTrue
1018 }
1019
1020 # Check 1: orphan detection
1021
1022 It 'Fails when an on-disk artifact is absent from hve-core-all' {
1023 # manifest and canonical cover a.agent.md but NOT orphan/orphan.agent.md
1024 $manifest = [ordered]@{
1025 id = 'partial-coverage'; name = 'Partial'; description = 'Missing orphan'
1026 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1027 }
1028 $canonical = [ordered]@{
1029 id = 'hve-core-all'; name = 'All'; description = 'Canonical - missing orphan'
1030 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1031 }
1032 Set-Content -Path (Join-Path $script:collectionsDir 'partial-coverage.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1033 Set-Content -Path (Join-Path $script:collectionsDir 'partial-coverage.collection.md') -Value '# Partial'
1034 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1035 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1036
1037 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1038 $result.Success | Should -BeFalse
1039 $result.ErrorCount | Should -BeGreaterOrEqual 1
1040 }
1041
1042 It 'Warns but passes when artifact is in hve-core-all but not in any themed collection' {
1043 # Themed covers only a.agent.md; canonical covers both - orphan is canonical-only
1044 $themed = [ordered]@{
1045 id = 'themed-partial'; name = 'Themed Partial'; description = 'Missing orphan in themed'
1046 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1047 }
1048 $canonical = [ordered]@{
1049 id = 'hve-core-all'; name = 'All'; description = 'Canonical - covers orphan'
1050 items = @(
1051 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1052 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1053 )
1054 }
1055 Set-Content -Path (Join-Path $script:collectionsDir 'themed-partial.collection.yml') -Value (ConvertTo-Yaml -Data $themed)
1056 Set-Content -Path (Join-Path $script:collectionsDir 'themed-partial.collection.md') -Value '# Themed Partial'
1057 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1058 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1059
1060 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1061 $result.Success | Should -BeTrue
1062 $result.ErrorCount | Should -Be 0
1063 }
1064}
1065
1066Describe 'Invoke-CollectionValidation - marker validation' -Tag 'Unit' {
1067 BeforeAll {
1068 $script:repoRoot = Join-Path $TestDrive 'marker-validation'
1069 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
1070 # Create artifact directories
1071 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
1072 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
1073 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---' -Force
1074 $orphanDir = Join-Path $script:repoRoot '.github/agents/orphan'
1075 New-Item -ItemType Directory -Path $orphanDir -Force | Out-Null
1076 Set-Content -Path (Join-Path $orphanDir 'orphan.agent.md') -Value '---' -Force
1077 }
1078
1079 BeforeEach {
1080 if (Test-Path $script:collectionsDir) {
1081 Remove-Item -Path $script:collectionsDir -Recurse -Force
1082 }
1083 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
1084 }
1085
1086 It 'Passes when collection.md has valid matched marker pairs' {
1087 $manifest = [ordered]@{
1088 id = 'valid-markers'; name = 'Valid Markers'; description = 'Matched markers'
1089 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1090 }
1091 Set-Content -Path (Join-Path $script:collectionsDir 'valid-markers.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1092 $mdContent = @"
1093# Valid Markers
1094
1095<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
1096Generated content.
1097<!-- END AUTO-GENERATED ARTIFACTS -->
1098"@
1099 Set-Content -Path (Join-Path $script:collectionsDir 'valid-markers.collection.md') -Value $mdContent
1100 $canonical = [ordered]@{
1101 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1102 items = @(
1103 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1104 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1105 )
1106 }
1107 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1108 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1109
1110 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1111 $result.Success | Should -BeTrue
1112 $result.ErrorCount | Should -Be 0
1113 }
1114
1115 It 'Warns but passes when begin marker exists without end marker' {
1116 $manifest = [ordered]@{
1117 id = 'begin-only'; name = 'Begin Only'; description = 'Missing end'
1118 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1119 }
1120 Set-Content -Path (Join-Path $script:collectionsDir 'begin-only.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1121 $mdContent = @"
1122# Begin Only
1123
1124<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
1125Content without end marker.
1126"@
1127 Set-Content -Path (Join-Path $script:collectionsDir 'begin-only.collection.md') -Value $mdContent
1128 $canonical = [ordered]@{
1129 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1130 items = @(
1131 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1132 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1133 )
1134 }
1135 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1136 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1137
1138 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1139 $result.Success | Should -BeTrue
1140 $result.ErrorCount | Should -Be 0
1141 }
1142
1143 It 'Warns but passes when end marker exists without begin marker' {
1144 $manifest = [ordered]@{
1145 id = 'end-only'; name = 'End Only'; description = 'Missing begin'
1146 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1147 }
1148 Set-Content -Path (Join-Path $script:collectionsDir 'end-only.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1149 $mdContent = @"
1150# End Only
1151
1152Content without begin marker.
1153<!-- END AUTO-GENERATED ARTIFACTS -->
1154"@
1155 Set-Content -Path (Join-Path $script:collectionsDir 'end-only.collection.md') -Value $mdContent
1156 $canonical = [ordered]@{
1157 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1158 items = @(
1159 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1160 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1161 )
1162 }
1163 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1164 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1165
1166 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1167 $result.Success | Should -BeTrue
1168 $result.ErrorCount | Should -Be 0
1169 }
1170
1171 It 'Does not warn when collection.md has no markers (backward compat)' {
1172 $manifest = [ordered]@{
1173 id = 'no-markers'; name = 'No Markers'; description = 'Legacy no markers'
1174 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1175 }
1176 Set-Content -Path (Join-Path $script:collectionsDir 'no-markers.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1177 Set-Content -Path (Join-Path $script:collectionsDir 'no-markers.collection.md') -Value '# No Markers - legacy content without any markers'
1178 $canonical = [ordered]@{
1179 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1180 items = @(
1181 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1182 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1183 )
1184 }
1185 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1186 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1187
1188 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1189 $result.Success | Should -BeTrue
1190 $result.ErrorCount | Should -Be 0
1191 }
1192
1193 It 'Warns but passes when markers appear in wrong order' {
1194 $manifest = [ordered]@{
1195 id = 'reversed'; name = 'Reversed'; description = 'Wrong order'
1196 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1197 }
1198 Set-Content -Path (Join-Path $script:collectionsDir 'reversed.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1199 $mdContent = @"
1200# Reversed
1201
1202<!-- END AUTO-GENERATED ARTIFACTS -->
1203Content.
1204<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
1205"@
1206 Set-Content -Path (Join-Path $script:collectionsDir 'reversed.collection.md') -Value $mdContent
1207 $canonical = [ordered]@{
1208 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1209 items = @(
1210 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1211 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1212 )
1213 }
1214 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1215 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1216
1217 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1218 $result.Success | Should -BeTrue
1219 $result.ErrorCount | Should -Be 0
1220 }
1221}
1222
1223Describe 'Collection validation JSON reporting' {
1224 BeforeAll {
1225 Import-Module PowerShell-Yaml -ErrorAction Stop
1226 $script:repoRoot = Join-Path $TestDrive 'json-reporting-repo'
1227 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
1228 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
1229 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
1230 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---' -Force
1231 }
1232
1233 BeforeEach {
1234 if (Test-Path $script:collectionsDir) {
1235 Remove-Item -Path $script:collectionsDir -Recurse -Force
1236 }
1237 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
1238 }
1239
1240 It 'Includes structured validation results in the return payload' {
1241 $yaml = @"
1242name: No ID Collection
1243description: Missing id field
1244items:
1245 - path: .github/agents/test/a.agent.md
1246 kind: agent
1247"@
1248 Set-Content -Path (Join-Path $script:collectionsDir 'no-id.collection.yml') -Value $yaml
1249 Set-Content -Path (Join-Path $script:collectionsDir 'no-id.collection.md') -Value '# No ID' -Force
1250
1251 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1252
1253 $result.Success | Should -BeFalse
1254 $result.Results | Should -Not -BeNullOrEmpty
1255 $missingField = @($result.Results | Where-Object { $_.ErrorType -eq 'MissingRequiredField' })
1256 $missingField | Should -Not -BeNullOrEmpty
1257 $missingField[0].Collection | Should -Be 'no-id'
1258 $missingField[0].Message | Should -Match "missing required field 'id'"
1259 }
1260
1261 It 'Exports JSON report with expected schema' {
1262 $manifest = [ordered]@{
1263 id = 'hve-core-all'
1264 name = 'All'
1265 description = 'Canonical'
1266 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1267 }
1268 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1269 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1270
1271 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1272 $outputPath = Join-Path $TestDrive 'collection-validation-results.json'
1273 Export-CollectionValidationReport -ValidationResult $result -OutputPath $outputPath
1274 $report = Get-Content -Path $outputPath -Raw | ConvertFrom-Json
1275
1276 $report.Timestamp | Should -Not -BeNullOrEmpty
1277 $report.TotalCollections | Should -Be 1
1278 $report.ErrorCount | Should -Be 0
1279 $report.PSObject.Properties.Name | Should -Contain 'Results'
1280 $report.Results | ForEach-Object {
1281 $_.PSObject.Properties.Name | Should -Contain 'Collection'
1282 $_.PSObject.Properties.Name | Should -Contain 'Severity'
1283 $_.PSObject.Properties.Name | Should -Contain 'ErrorType'
1284 $_.PSObject.Properties.Name | Should -Contain 'Message'
1285 }
1286 }
1287
1288 It 'Differentiates Severity between warnings and errors in results' {
1289 $yaml = @"
1290name: No ID Collection
1291description: Missing id field
1292items:
1293 - path: .github/agents/test/a.agent.md
1294 kind: agent
1295"@
1296 Set-Content -Path (Join-Path $script:collectionsDir 'no-id.collection.yml') -Value $yaml
1297
1298 # Also create a valid companion-less collection to generate a Warning alongside the Error
1299 $validYaml = @"
1300id: some-collection
1301name: Some Collection
1302description: Valid collection missing companion md
1303items:
1304 - path: .github/agents/test/a.agent.md
1305 kind: agent
1306"@
1307 Set-Content -Path (Join-Path $script:collectionsDir 'some-collection.collection.yml') -Value $validYaml
1308
1309 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1310
1311 $errors = @($result.Results | Where-Object { $_.Severity -eq 'Error' })
1312 $warnings = @($result.Results | Where-Object { $_.Severity -eq 'Warning' })
1313
1314 $errors | Should -Not -BeNullOrEmpty
1315 $warnings | Should -Not -BeNullOrEmpty
1316 $errors[0].ErrorType | Should -Be 'MissingRequiredField'
1317 $warnings | Where-Object { $_.ErrorType -eq 'MissingCompanionCollectionMd' } | Should -Not -BeNullOrEmpty
1318 }
1319
1320 It 'Creates output directory when it does not exist' {
1321 $manifest = [ordered]@{
1322 id = 'hve-core-all'
1323 name = 'All'
1324 description = 'Canonical'
1325 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1326 }
1327 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1328 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1329
1330 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1331 $newDir = Join-Path $TestDrive 'nonexistent-logs-dir'
1332 $outputPath = Join-Path $newDir 'results.json'
1333
1334 Test-Path $newDir | Should -BeFalse
1335 Export-CollectionValidationReport -ValidationResult $result -OutputPath $outputPath
1336 Test-Path $newDir | Should -BeTrue
1337 Test-Path $outputPath | Should -BeTrue
1338 }
1339
1340 It 'Captures multiple distinct ErrorType values in a single run' {
1341 $yaml = @"
1342id: multi-error
1343name: Multi Error Collection
1344description: Has both a path-not-found and a missing-kind error
1345items:
1346 - path: .github/agents/test/nonexistent.agent.md
1347 kind: agent
1348 - path: .github/agents/test/a.agent.md
1349"@
1350 Set-Content -Path (Join-Path $script:collectionsDir 'multi-error.collection.yml') -Value $yaml
1351
1352 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1353
1354 $result.Success | Should -BeFalse
1355 $errorTypes = $result.Results | Select-Object -ExpandProperty ErrorType
1356 $errorTypes | Should -Contain 'PathNotFound'
1357 $errorTypes | Should -Contain 'MissingItemKind'
1358 }
1359
1360 It 'Returns a Results key even when a collection passes validation' {
1361 $manifest = [ordered]@{
1362 id = 'hve-core-all'
1363 name = 'All'
1364 description = 'Canonical'
1365 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1366 }
1367 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1368 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1369
1370 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1371
1372 $result.Success | Should -BeTrue
1373 $result.Keys | Should -Contain 'Results'
1374 }
1375}
1376