microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/a11y-pr1-scripts-validators

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

1409lines · 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 # accessibility sub-domain folder structure (shared across themed collections)
348 $accessibilityDir = Join-Path $script:repoRoot '.github/instructions/accessibility'
349 New-Item -ItemType Directory -Path $accessibilityDir -Force | Out-Null
350 Set-Content -Path (Join-Path $accessibilityDir 'accessibility.instructions.md') -Value '---\ndescription: accessibility instruction\n---'
351
352 # hve-core folder structure (cross-collection reference allowed without warning)
353 $hveCoreDir = Join-Path $script:repoRoot '.github/agents/hve-core'
354 New-Item -ItemType Directory -Path $hveCoreDir -Force | Out-Null
355 Set-Content -Path (Join-Path $hveCoreDir 'core.agent.md') -Value '---\ndescription: hve-core agent\n---'
356 }
357
358 BeforeEach {
359 if (Test-Path $script:collectionsDir) {
360 Remove-Item -Path $script:collectionsDir -Recurse -Force
361 }
362 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
363 }
364
365 It 'Passes when collection-id matches folder name' {
366 Mock Write-Host {}
367
368 $manifest = [ordered]@{
369 id = 'my-collection'
370 name = 'My Collection'
371 description = 'Collection with matching folder'
372 items = @(
373 [ordered]@{
374 path = '.github/agents/my-collection/match.agent.md'
375 kind = 'agent'
376 }
377 )
378 }
379 $yaml = ConvertTo-Yaml -Data $manifest
380 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
381
382 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
383 $result.Success | Should -BeTrue
384 $result.ErrorCount | Should -Be 0
385 Should -Not -Invoke Write-Host -ParameterFilter {
386 $Object -match 'WARN collection.*my-collection'
387 }
388 }
389
390 It 'Warns but does not fail when collection-id does not match folder name' {
391 Mock Write-Host {}
392
393 $manifest = [ordered]@{
394 id = 'my-collection'
395 name = 'My Collection'
396 description = 'Collection with mismatched folder'
397 items = @(
398 [ordered]@{
399 path = '.github/agents/wrong-folder/mismatch.agent.md'
400 kind = 'agent'
401 }
402 )
403 }
404 $yaml = ConvertTo-Yaml -Data $manifest
405 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
406
407 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
408 $result.Success | Should -BeTrue
409 $result.ErrorCount | Should -Be 0
410 Should -Invoke Write-Host -ParameterFilter {
411 $Object -match 'WARN collection.*wrong-folder'
412 }
413 }
414
415 It 'Allows items from hve-core/ folder in any collection' {
416 Mock Write-Host {}
417
418 $manifest = [ordered]@{
419 id = 'my-collection'
420 name = 'My Collection'
421 description = 'Collection referencing hve-core item'
422 items = @(
423 [ordered]@{
424 path = '.github/agents/hve-core/core.agent.md'
425 kind = 'agent'
426 }
427 )
428 }
429 $yaml = ConvertTo-Yaml -Data $manifest
430 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
431
432 # Register hve-core as a known collection ID (mirrors real-world hve-core.collection.yml)
433 $hveCoreManifest = [ordered]@{
434 id = 'hve-core'
435 name = 'HVE Core'
436 description = 'HVE Core collection'
437 items = @()
438 }
439 $hveYaml = ConvertTo-Yaml -Data $hveCoreManifest
440 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core.collection.yml') -Value $hveYaml
441 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core.collection.md') -Value '# HVE Core'
442
443 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
444 $result.Success | Should -BeTrue
445 $result.ErrorCount | Should -Be 0
446 Should -Not -Invoke Write-Host -ParameterFilter {
447 $Object -match 'WARN collection'
448 }
449 }
450
451 It 'Allows items from shared/ folder in any collection' {
452 Mock Write-Host {}
453
454 $manifest = [ordered]@{
455 id = 'my-collection'
456 name = 'My Collection'
457 description = 'Collection referencing shared item'
458 items = @(
459 [ordered]@{
460 path = '.github/instructions/shared/shared.instructions.md'
461 kind = 'instruction'
462 }
463 )
464 }
465 $yaml = ConvertTo-Yaml -Data $manifest
466 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
467
468 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
469 $result.Success | Should -BeTrue
470 $result.ErrorCount | Should -Be 0
471 Should -Not -Invoke Write-Host -ParameterFilter {
472 $Object -match 'WARN collection'
473 }
474 }
475
476 It 'Allows items from rai-planning/ folder in any collection' {
477 Mock Write-Host {}
478
479 $manifest = [ordered]@{
480 id = 'my-collection'
481 name = 'My Collection'
482 description = 'Collection referencing rai-planning item'
483 items = @(
484 [ordered]@{
485 path = '.github/instructions/rai-planning/rai.instructions.md'
486 kind = 'instruction'
487 }
488 )
489 }
490 $yaml = ConvertTo-Yaml -Data $manifest
491 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
492
493 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
494 $result.Success | Should -BeTrue
495 $result.ErrorCount | Should -Be 0
496 Should -Not -Invoke Write-Host -ParameterFilter {
497 $Object -match 'WARN collection'
498 }
499 }
500
501 It 'Allows items from accessibility/ folder in any collection' {
502 Mock Write-Host {}
503
504 $manifest = [ordered]@{
505 id = 'my-collection'
506 name = 'My Collection'
507 description = 'Collection referencing accessibility item'
508 items = @(
509 [ordered]@{
510 path = '.github/instructions/accessibility/accessibility.instructions.md'
511 kind = 'instruction'
512 }
513 )
514 }
515 $yaml = ConvertTo-Yaml -Data $manifest
516 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
517
518 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
519 $result.Success | Should -BeTrue
520 $result.ErrorCount | Should -Be 0
521 Should -Not -Invoke Write-Host -ParameterFilter {
522 $Object -match 'WARN collection'
523 }
524 }
525
526 It 'Allows hve-core-all to reference items from any folder' {
527 Mock Write-Host {}
528
529 $manifest = [ordered]@{
530 id = 'hve-core-all'
531 name = 'HVE Core All'
532 description = 'Aggregate collection'
533 items = @(
534 [ordered]@{
535 path = '.github/agents/my-collection/match.agent.md'
536 kind = 'agent'
537 },
538 [ordered]@{
539 path = '.github/agents/wrong-folder/mismatch.agent.md'
540 kind = 'agent'
541 },
542 [ordered]@{
543 path = '.github/instructions/shared/shared.instructions.md'
544 kind = 'instruction'
545 },
546 [ordered]@{
547 path = '.github/instructions/rai-planning/rai.instructions.md'
548 kind = 'instruction'
549 },
550 [ordered]@{
551 path = '.github/instructions/accessibility/accessibility.instructions.md'
552 kind = 'instruction'
553 },
554 [ordered]@{
555 path = '.github/agents/hve-core/core.agent.md'
556 kind = 'agent'
557 }
558 )
559 }
560 $yaml = ConvertTo-Yaml -Data $manifest
561 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value $yaml
562 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
563
564 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
565 $result.Success | Should -BeTrue
566 $result.ErrorCount | Should -Be 0
567 Should -Not -Invoke Write-Host -ParameterFilter {
568 $Object -match 'WARN collection'
569 }
570 }
571
572 It 'Emits warning output for mismatched folder name without failing' {
573 Mock Write-Host {}
574
575 $manifest = [ordered]@{
576 id = 'my-collection'
577 name = 'My Collection'
578 description = 'Mismatch for warning output test'
579 items = @(
580 [ordered]@{
581 path = '.github/agents/wrong-folder/mismatch.agent.md'
582 kind = 'agent'
583 }
584 )
585 }
586 $yaml = ConvertTo-Yaml -Data $manifest
587 Set-Content -Path (Join-Path $script:collectionsDir 'my-collection.collection.yml') -Value $yaml
588
589 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
590 # Advisory warning uses Write-Host WARN; validation still passes
591 $result.Success | Should -BeTrue
592 $result.ErrorCount | Should -Be 0
593 Should -Invoke Write-Host -ParameterFilter {
594 $Object -match 'WARN collection.*wrong-folder'
595 }
596 }
597}
598
599Describe 'Invoke-CollectionValidation - error paths' {
600 BeforeAll {
601 Import-Module PowerShell-Yaml -ErrorAction Stop
602
603 $script:repoRoot = Join-Path $TestDrive 'error-repo'
604 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
605
606 # Create valid artifacts for reference
607 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
608 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
609 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---\ndescription: agent a\n---'
610 Set-Content -Path (Join-Path $agentsDir 'b.agent.md') -Value '---\ndescription: agent b\n---'
611
612 $instrDir = Join-Path $script:repoRoot '.github/instructions/test'
613 New-Item -ItemType Directory -Path $instrDir -Force | Out-Null
614 Set-Content -Path (Join-Path $instrDir 'test.instructions.md') -Value '---\ndescription: test\n---'
615 }
616
617 BeforeEach {
618 if (Test-Path $script:collectionsDir) {
619 Remove-Item -Path $script:collectionsDir -Recurse -Force
620 }
621 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
622 }
623
624 It 'Returns success with zero collections when directory is empty' {
625 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
626 $result.Success | Should -BeTrue
627 $result.CollectionCount | Should -Be 0
628 }
629
630 It 'Fails when required field is missing' {
631 $yaml = @"
632name: No ID Collection
633description: Missing id field
634items:
635 - path: .github/agents/test/a.agent.md
636 kind: agent
637"@
638 Set-Content -Path (Join-Path $script:collectionsDir 'no-id.collection.yml') -Value $yaml
639
640 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
641 $result.Success | Should -BeFalse
642 }
643
644 It 'Fails for invalid id format' {
645 $manifest = [ordered]@{
646 id = 'INVALID_ID!'
647 name = 'Bad ID'
648 description = 'Invalid id format'
649 items = @(
650 [ordered]@{
651 path = '.github/agents/test/a.agent.md'
652 kind = 'agent'
653 }
654 )
655 }
656 $yaml = ConvertTo-Yaml -Data $manifest
657 Set-Content -Path (Join-Path $script:collectionsDir 'bad-id.collection.yml') -Value $yaml
658
659 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
660 $result.Success | Should -BeFalse
661 }
662
663 It 'Fails for duplicate ids across collections' {
664 $manifest = [ordered]@{
665 id = 'dup-id'
666 name = 'First'
667 description = 'First collection'
668 items = @(
669 [ordered]@{
670 path = '.github/agents/test/a.agent.md'
671 kind = 'agent'
672 }
673 )
674 }
675 $yaml = ConvertTo-Yaml -Data $manifest
676 Set-Content -Path (Join-Path $script:collectionsDir 'dup1.collection.yml') -Value $yaml
677 Set-Content -Path (Join-Path $script:collectionsDir 'dup2.collection.yml') -Value $yaml
678
679 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
680 $result.Success | Should -BeFalse
681 }
682
683 It 'Fails when item path does not exist' {
684 $manifest = [ordered]@{
685 id = 'missing-path'
686 name = 'Missing'
687 description = 'Item path missing'
688 items = @(
689 [ordered]@{
690 path = '.github/agents/test/nonexistent.agent.md'
691 kind = 'agent'
692 }
693 )
694 }
695 $yaml = ConvertTo-Yaml -Data $manifest
696 Set-Content -Path (Join-Path $script:collectionsDir 'missing-path.collection.yml') -Value $yaml
697
698 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
699 $result.Success | Should -BeFalse
700 }
701
702 It 'Fails when item has no kind' {
703 $yaml = @"
704id: no-kind
705name: No Kind
706description: Item missing kind
707items:
708 - path: .github/agents/test/a.agent.md
709"@
710 Set-Content -Path (Join-Path $script:collectionsDir 'no-kind.collection.yml') -Value $yaml
711
712 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
713 $result.Success | Should -BeFalse
714 }
715
716 It 'Fails for invalid item maturity' {
717 $manifest = [ordered]@{
718 id = 'bad-item-mat'
719 name = 'Bad Item Maturity'
720 description = 'Item with invalid maturity'
721 items = @(
722 [ordered]@{
723 path = '.github/agents/test/a.agent.md'
724 kind = 'agent'
725 maturity = 'alpha'
726 }
727 )
728 }
729 $yaml = ConvertTo-Yaml -Data $manifest
730 Set-Content -Path (Join-Path $script:collectionsDir 'bad-item-mat.collection.yml') -Value $yaml
731
732 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
733 $result.Success | Should -BeFalse
734 }
735
736 It 'Fails for kind-suffix mismatch' {
737 $manifest = [ordered]@{
738 id = 'suffix-mismatch'
739 name = 'Suffix Mismatch'
740 description = 'Agent path with wrong suffix'
741 items = @(
742 [ordered]@{
743 path = '.github/instructions/test/test.instructions.md'
744 kind = 'agent'
745 }
746 )
747 }
748 $yaml = ConvertTo-Yaml -Data $manifest
749 Set-Content -Path (Join-Path $script:collectionsDir 'suffix-mismatch.collection.yml') -Value $yaml
750
751 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
752 $result.Success | Should -BeFalse
753 }
754
755 It 'Fails for instruction kind with wrong suffix' {
756 $manifest = [ordered]@{
757 id = 'instr-suffix'
758 name = 'Instruction Suffix'
759 description = 'Instruction item with agent suffix'
760 items = @(
761 [ordered]@{
762 path = '.github/agents/test/a.agent.md'
763 kind = 'instruction'
764 }
765 )
766 }
767 $yaml = ConvertTo-Yaml -Data $manifest
768 Set-Content -Path (Join-Path $script:collectionsDir 'instr-suffix.collection.yml') -Value $yaml
769
770 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
771 $result.Success | Should -BeFalse
772 }
773
774 It 'Detects duplicate artifact keys at distinct paths' {
775 # Two agents at different paths that resolve to the same artifact key
776 $agentsDir2 = Join-Path $script:repoRoot '.github/agents/other'
777 New-Item -ItemType Directory -Path $agentsDir2 -Force | Out-Null
778 Set-Content -Path (Join-Path $agentsDir2 'a.agent.md') -Value '---\ndescription: same name\n---'
779
780 $manifest = [ordered]@{
781 id = 'dup-artifact'
782 name = 'Dup Artifact'
783 description = 'Same artifact key from different paths'
784 items = @(
785 [ordered]@{
786 path = '.github/agents/test/a.agent.md'
787 kind = 'agent'
788 },
789 [ordered]@{
790 path = '.github/agents/other/a.agent.md'
791 kind = 'agent'
792 }
793 )
794 }
795 $yaml = ConvertTo-Yaml -Data $manifest
796 Set-Content -Path (Join-Path $script:collectionsDir 'dup-artifact.collection.yml') -Value $yaml
797
798 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
799 $result.Success | Should -BeFalse
800 }
801
802 It 'Detects shared item missing canonical entry' {
803 # Two collections share the same item but neither is hve-core-all;
804 # hve-core-all exists but does not include a.agent.md - Check 4 fires.
805 $manifest1 = [ordered]@{
806 id = 'share-one'
807 name = 'Share One'
808 description = 'First sharer'
809 items = @(
810 [ordered]@{
811 path = '.github/agents/test/a.agent.md'
812 kind = 'agent'
813 }
814 )
815 }
816 $manifest2 = [ordered]@{
817 id = 'share-two'
818 name = 'Share Two'
819 description = 'Second sharer'
820 items = @(
821 [ordered]@{
822 path = '.github/agents/test/a.agent.md'
823 kind = 'agent'
824 }
825 )
826 }
827 $canonical = [ordered]@{
828 id = 'hve-core-all'
829 name = 'All'
830 description = 'Canonical - missing a.agent.md'
831 items = @(
832 [ordered]@{
833 path = '.github/agents/test/b.agent.md'
834 kind = 'agent'
835 },
836 [ordered]@{
837 path = '.github/instructions/test/test.instructions.md'
838 kind = 'instruction'
839 }
840 )
841 }
842 $yaml1 = ConvertTo-Yaml -Data $manifest1
843 $yaml2 = ConvertTo-Yaml -Data $manifest2
844 $yaml3 = ConvertTo-Yaml -Data $canonical
845 Set-Content -Path (Join-Path $script:collectionsDir 'share-one.collection.yml') -Value $yaml1
846 Set-Content -Path (Join-Path $script:collectionsDir 'share-two.collection.yml') -Value $yaml2
847 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value $yaml3
848 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
849
850 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
851 $result.Success | Should -BeFalse
852 }
853
854 It 'Detects maturity conflict with canonical collection' {
855 # hve-core-all has the item as stable, another collection has it as experimental
856 $canonical = [ordered]@{
857 id = 'hve-core-all'
858 name = 'All'
859 description = 'Canonical collection'
860 items = @(
861 [ordered]@{
862 path = '.github/agents/test/a.agent.md'
863 kind = 'agent'
864 maturity = 'stable'
865 }
866 )
867 }
868 $other = [ordered]@{
869 id = 'conflict-col'
870 name = 'Conflict'
871 description = 'Conflicting maturity'
872 items = @(
873 [ordered]@{
874 path = '.github/agents/test/a.agent.md'
875 kind = 'agent'
876 maturity = 'experimental'
877 }
878 )
879 }
880 $yaml1 = ConvertTo-Yaml -Data $canonical
881 $yaml2 = ConvertTo-Yaml -Data $other
882 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value $yaml1
883 Set-Content -Path (Join-Path $script:collectionsDir 'conflict-col.collection.yml') -Value $yaml2
884
885 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
886 $result.Success | Should -BeFalse
887 }
888}
889
890Describe 'Invoke-CollectionValidation - new checks' {
891 BeforeAll {
892 Import-Module PowerShell-Yaml -ErrorAction Stop
893
894 $script:repoRoot = Join-Path $TestDrive 'new-checks-repo'
895 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
896
897 # Standard artifact - used by most tests
898 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
899 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
900 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---' -Force
901
902 # Orphan artifact - on disk but not necessarily in manifests
903 $orphanDir = Join-Path $script:repoRoot '.github/agents/orphan'
904 New-Item -ItemType Directory -Path $orphanDir -Force | Out-Null
905 Set-Content -Path (Join-Path $orphanDir 'orphan.agent.md') -Value '---' -Force
906 }
907
908 BeforeEach {
909 if (Test-Path $script:collectionsDir) { Remove-Item -Path $script:collectionsDir -Recurse -Force }
910 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
911
912 # Reset agent dirs to pristine state - prevents artifact leakage between tests
913 $agentsBaseDir = Join-Path $script:repoRoot '.github/agents'
914 if (Test-Path $agentsBaseDir) { Remove-Item -Path $agentsBaseDir -Recurse -Force }
915 New-Item -ItemType Directory -Path (Join-Path $agentsBaseDir 'test') -Force | Out-Null
916 Set-Content -Path (Join-Path $agentsBaseDir 'test/a.agent.md') -Value '---' -Force
917 New-Item -ItemType Directory -Path (Join-Path $agentsBaseDir 'orphan') -Force | Out-Null
918 Set-Content -Path (Join-Path $agentsBaseDir 'orphan/orphan.agent.md') -Value '---' -Force
919 }
920
921 # Check 3: companion .collection.md
922
923 It 'Warns but passes when .collection.md companion is missing' {
924 $manifest = [ordered]@{
925 id = 'no-companion'; name = 'No Companion'; description = 'Missing companion md'
926 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
927 }
928 Set-Content -Path (Join-Path $script:collectionsDir 'no-companion.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
929 $canonical = [ordered]@{
930 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
931 items = @(
932 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
933 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
934 )
935 }
936 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
937 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
938
939 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
940 $result.Success | Should -BeTrue
941 $result.ErrorCount | Should -Be 0
942 }
943
944 It 'Passes cleanly when .collection.md companion is present' {
945 $manifest = [ordered]@{
946 id = 'has-companion'; name = 'Has Companion'; description = 'With md'
947 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
948 }
949 Set-Content -Path (Join-Path $script:collectionsDir 'has-companion.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
950 Set-Content -Path (Join-Path $script:collectionsDir 'has-companion.collection.md') -Value '# Has Companion'
951 $canonical = [ordered]@{
952 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
953 items = @(
954 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
955 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
956 )
957 }
958 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
959 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
960
961 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
962 $result.Success | Should -BeTrue
963 }
964
965 # Check 2: intra-collection duplicate
966
967 It 'Fails when the same item appears twice in one collection' {
968 $manifest = [ordered]@{
969 id = 'intra-dup'; name = 'Intra Dup'; description = 'Dup item'
970 items = @(
971 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
972 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' }
973 )
974 }
975 Set-Content -Path (Join-Path $script:collectionsDir 'intra-dup.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
976
977 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
978 $result.Success | Should -BeFalse
979 $result.ErrorCount | Should -BeGreaterOrEqual 1
980 }
981
982 It 'Passes when all items in a collection are distinct' {
983 $agentsDir2 = Join-Path $script:repoRoot '.github/agents/test2'
984 New-Item -ItemType Directory -Path $agentsDir2 -Force | Out-Null
985 Set-Content -Path (Join-Path $agentsDir2 'b.agent.md') -Value '---' -Force
986
987 $manifest = [ordered]@{
988 id = 'distinct-items'; name = 'Distinct'; description = 'Distinct items'
989 items = @(
990 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
991 [ordered]@{ path = '.github/agents/test2/b.agent.md'; kind = 'agent' }
992 )
993 }
994 $canonical = [ordered]@{
995 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
996 items = @(
997 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
998 [ordered]@{ path = '.github/agents/test2/b.agent.md'; kind = 'agent' },
999 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1000 )
1001 }
1002 Set-Content -Path (Join-Path $script:collectionsDir 'distinct-items.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1003 Set-Content -Path (Join-Path $script:collectionsDir 'distinct-items.collection.md') -Value '# Distinct'
1004 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1005 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1006
1007 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1008 $result.Success | Should -BeTrue
1009 }
1010
1011 # Check 4: hve-core-all coverage
1012
1013 It 'Fails when a themed collection item is absent from hve-core-all' {
1014 $manifest = [ordered]@{
1015 id = 'themed-only'; name = 'Themed Only'; description = 'Item not in hve-core-all'
1016 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1017 }
1018 # Canonical exists but does NOT include a.agent.md - only orphan - so Check 4 fires
1019 $canonical = [ordered]@{
1020 id = 'hve-core-all'; name = 'All'; description = 'Canonical - missing themed item'
1021 items = @([ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' })
1022 }
1023 Set-Content -Path (Join-Path $script:collectionsDir 'themed-only.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1024 Set-Content -Path (Join-Path $script:collectionsDir 'themed-only.collection.md') -Value '# Themed'
1025 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1026 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1027
1028 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1029 $result.Success | Should -BeFalse
1030 $result.ErrorCount | Should -BeGreaterOrEqual 1
1031 }
1032
1033 It 'Passes when all themed items are present in hve-core-all' {
1034 $themed = [ordered]@{
1035 id = 'themed-covered'; name = 'Themed Covered'; description = 'Covered by canonical'
1036 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1037 }
1038 $canonical = [ordered]@{
1039 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1040 items = @(
1041 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1042 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1043 )
1044 }
1045 Set-Content -Path (Join-Path $script:collectionsDir 'themed-covered.collection.yml') -Value (ConvertTo-Yaml -Data $themed)
1046 Set-Content -Path (Join-Path $script:collectionsDir 'themed-covered.collection.md') -Value '# Themed Covered'
1047 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1048 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1049
1050 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1051 $result.Success | Should -BeTrue
1052 }
1053
1054 # Check 1: orphan detection
1055
1056 It 'Fails when an on-disk artifact is absent from hve-core-all' {
1057 # manifest and canonical cover a.agent.md but NOT orphan/orphan.agent.md
1058 $manifest = [ordered]@{
1059 id = 'partial-coverage'; name = 'Partial'; description = 'Missing orphan'
1060 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1061 }
1062 $canonical = [ordered]@{
1063 id = 'hve-core-all'; name = 'All'; description = 'Canonical - missing orphan'
1064 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1065 }
1066 Set-Content -Path (Join-Path $script:collectionsDir 'partial-coverage.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1067 Set-Content -Path (Join-Path $script:collectionsDir 'partial-coverage.collection.md') -Value '# Partial'
1068 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1069 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1070
1071 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1072 $result.Success | Should -BeFalse
1073 $result.ErrorCount | Should -BeGreaterOrEqual 1
1074 }
1075
1076 It 'Warns but passes when artifact is in hve-core-all but not in any themed collection' {
1077 # Themed covers only a.agent.md; canonical covers both - orphan is canonical-only
1078 $themed = [ordered]@{
1079 id = 'themed-partial'; name = 'Themed Partial'; description = 'Missing orphan in themed'
1080 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1081 }
1082 $canonical = [ordered]@{
1083 id = 'hve-core-all'; name = 'All'; description = 'Canonical - covers orphan'
1084 items = @(
1085 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1086 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1087 )
1088 }
1089 Set-Content -Path (Join-Path $script:collectionsDir 'themed-partial.collection.yml') -Value (ConvertTo-Yaml -Data $themed)
1090 Set-Content -Path (Join-Path $script:collectionsDir 'themed-partial.collection.md') -Value '# Themed Partial'
1091 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1092 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1093
1094 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1095 $result.Success | Should -BeTrue
1096 $result.ErrorCount | Should -Be 0
1097 }
1098}
1099
1100Describe 'Invoke-CollectionValidation - marker validation' -Tag 'Unit' {
1101 BeforeAll {
1102 $script:repoRoot = Join-Path $TestDrive 'marker-validation'
1103 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
1104 # Create artifact directories
1105 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
1106 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
1107 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---' -Force
1108 $orphanDir = Join-Path $script:repoRoot '.github/agents/orphan'
1109 New-Item -ItemType Directory -Path $orphanDir -Force | Out-Null
1110 Set-Content -Path (Join-Path $orphanDir 'orphan.agent.md') -Value '---' -Force
1111 }
1112
1113 BeforeEach {
1114 if (Test-Path $script:collectionsDir) {
1115 Remove-Item -Path $script:collectionsDir -Recurse -Force
1116 }
1117 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
1118 }
1119
1120 It 'Passes when collection.md has valid matched marker pairs' {
1121 $manifest = [ordered]@{
1122 id = 'valid-markers'; name = 'Valid Markers'; description = 'Matched markers'
1123 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1124 }
1125 Set-Content -Path (Join-Path $script:collectionsDir 'valid-markers.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1126 $mdContent = @"
1127# Valid Markers
1128
1129<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
1130Generated content.
1131<!-- END AUTO-GENERATED ARTIFACTS -->
1132"@
1133 Set-Content -Path (Join-Path $script:collectionsDir 'valid-markers.collection.md') -Value $mdContent
1134 $canonical = [ordered]@{
1135 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1136 items = @(
1137 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1138 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1139 )
1140 }
1141 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1142 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1143
1144 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1145 $result.Success | Should -BeTrue
1146 $result.ErrorCount | Should -Be 0
1147 }
1148
1149 It 'Warns but passes when begin marker exists without end marker' {
1150 $manifest = [ordered]@{
1151 id = 'begin-only'; name = 'Begin Only'; description = 'Missing end'
1152 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1153 }
1154 Set-Content -Path (Join-Path $script:collectionsDir 'begin-only.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1155 $mdContent = @"
1156# Begin Only
1157
1158<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
1159Content without end marker.
1160"@
1161 Set-Content -Path (Join-Path $script:collectionsDir 'begin-only.collection.md') -Value $mdContent
1162 $canonical = [ordered]@{
1163 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1164 items = @(
1165 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1166 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1167 )
1168 }
1169 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1170 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1171
1172 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1173 $result.Success | Should -BeTrue
1174 $result.ErrorCount | Should -Be 0
1175 }
1176
1177 It 'Warns but passes when end marker exists without begin marker' {
1178 $manifest = [ordered]@{
1179 id = 'end-only'; name = 'End Only'; description = 'Missing begin'
1180 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1181 }
1182 Set-Content -Path (Join-Path $script:collectionsDir 'end-only.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1183 $mdContent = @"
1184# End Only
1185
1186Content without begin marker.
1187<!-- END AUTO-GENERATED ARTIFACTS -->
1188"@
1189 Set-Content -Path (Join-Path $script:collectionsDir 'end-only.collection.md') -Value $mdContent
1190 $canonical = [ordered]@{
1191 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1192 items = @(
1193 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1194 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1195 )
1196 }
1197 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1198 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1199
1200 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1201 $result.Success | Should -BeTrue
1202 $result.ErrorCount | Should -Be 0
1203 }
1204
1205 It 'Does not warn when collection.md has no markers (backward compat)' {
1206 $manifest = [ordered]@{
1207 id = 'no-markers'; name = 'No Markers'; description = 'Legacy no markers'
1208 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1209 }
1210 Set-Content -Path (Join-Path $script:collectionsDir 'no-markers.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1211 Set-Content -Path (Join-Path $script:collectionsDir 'no-markers.collection.md') -Value '# No Markers - legacy content without any markers'
1212 $canonical = [ordered]@{
1213 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1214 items = @(
1215 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1216 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1217 )
1218 }
1219 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1220 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1221
1222 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1223 $result.Success | Should -BeTrue
1224 $result.ErrorCount | Should -Be 0
1225 }
1226
1227 It 'Warns but passes when markers appear in wrong order' {
1228 $manifest = [ordered]@{
1229 id = 'reversed'; name = 'Reversed'; description = 'Wrong order'
1230 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1231 }
1232 Set-Content -Path (Join-Path $script:collectionsDir 'reversed.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1233 $mdContent = @"
1234# Reversed
1235
1236<!-- END AUTO-GENERATED ARTIFACTS -->
1237Content.
1238<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
1239"@
1240 Set-Content -Path (Join-Path $script:collectionsDir 'reversed.collection.md') -Value $mdContent
1241 $canonical = [ordered]@{
1242 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
1243 items = @(
1244 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
1245 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
1246 )
1247 }
1248 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
1249 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1250
1251 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1252 $result.Success | Should -BeTrue
1253 $result.ErrorCount | Should -Be 0
1254 }
1255}
1256
1257Describe 'Collection validation JSON reporting' {
1258 BeforeAll {
1259 Import-Module PowerShell-Yaml -ErrorAction Stop
1260 $script:repoRoot = Join-Path $TestDrive 'json-reporting-repo'
1261 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
1262 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
1263 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
1264 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---' -Force
1265 }
1266
1267 BeforeEach {
1268 if (Test-Path $script:collectionsDir) {
1269 Remove-Item -Path $script:collectionsDir -Recurse -Force
1270 }
1271 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
1272 }
1273
1274 It 'Includes structured validation results in the return payload' {
1275 $yaml = @"
1276name: No ID Collection
1277description: Missing id field
1278items:
1279 - path: .github/agents/test/a.agent.md
1280 kind: agent
1281"@
1282 Set-Content -Path (Join-Path $script:collectionsDir 'no-id.collection.yml') -Value $yaml
1283 Set-Content -Path (Join-Path $script:collectionsDir 'no-id.collection.md') -Value '# No ID' -Force
1284
1285 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1286
1287 $result.Success | Should -BeFalse
1288 $result.Results | Should -Not -BeNullOrEmpty
1289 $missingField = @($result.Results | Where-Object { $_.ErrorType -eq 'MissingRequiredField' })
1290 $missingField | Should -Not -BeNullOrEmpty
1291 $missingField[0].Collection | Should -Be 'no-id'
1292 $missingField[0].Message | Should -Match "missing required field 'id'"
1293 }
1294
1295 It 'Exports JSON report with expected schema' {
1296 $manifest = [ordered]@{
1297 id = 'hve-core-all'
1298 name = 'All'
1299 description = 'Canonical'
1300 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1301 }
1302 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1303 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1304
1305 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1306 $outputPath = Join-Path $TestDrive 'collection-validation-results.json'
1307 Export-CollectionValidationReport -ValidationResult $result -OutputPath $outputPath
1308 $report = Get-Content -Path $outputPath -Raw | ConvertFrom-Json
1309
1310 $report.Timestamp | Should -Not -BeNullOrEmpty
1311 $report.TotalCollections | Should -Be 1
1312 $report.ErrorCount | Should -Be 0
1313 $report.PSObject.Properties.Name | Should -Contain 'Results'
1314 $report.Results | ForEach-Object {
1315 $_.PSObject.Properties.Name | Should -Contain 'Collection'
1316 $_.PSObject.Properties.Name | Should -Contain 'Severity'
1317 $_.PSObject.Properties.Name | Should -Contain 'ErrorType'
1318 $_.PSObject.Properties.Name | Should -Contain 'Message'
1319 }
1320 }
1321
1322 It 'Differentiates Severity between warnings and errors in results' {
1323 $yaml = @"
1324name: No ID Collection
1325description: Missing id field
1326items:
1327 - path: .github/agents/test/a.agent.md
1328 kind: agent
1329"@
1330 Set-Content -Path (Join-Path $script:collectionsDir 'no-id.collection.yml') -Value $yaml
1331
1332 # Also create a valid companion-less collection to generate a Warning alongside the Error
1333 $validYaml = @"
1334id: some-collection
1335name: Some Collection
1336description: Valid collection missing companion md
1337items:
1338 - path: .github/agents/test/a.agent.md
1339 kind: agent
1340"@
1341 Set-Content -Path (Join-Path $script:collectionsDir 'some-collection.collection.yml') -Value $validYaml
1342
1343 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1344
1345 $errors = @($result.Results | Where-Object { $_.Severity -eq 'Error' })
1346 $warnings = @($result.Results | Where-Object { $_.Severity -eq 'Warning' })
1347
1348 $errors | Should -Not -BeNullOrEmpty
1349 $warnings | Should -Not -BeNullOrEmpty
1350 $errors[0].ErrorType | Should -Be 'MissingRequiredField'
1351 $warnings | Where-Object { $_.ErrorType -eq 'MissingCompanionCollectionMd' } | Should -Not -BeNullOrEmpty
1352 }
1353
1354 It 'Creates output directory when it does not exist' {
1355 $manifest = [ordered]@{
1356 id = 'hve-core-all'
1357 name = 'All'
1358 description = 'Canonical'
1359 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1360 }
1361 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1362 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1363
1364 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1365 $newDir = Join-Path $TestDrive 'nonexistent-logs-dir'
1366 $outputPath = Join-Path $newDir 'results.json'
1367
1368 Test-Path $newDir | Should -BeFalse
1369 Export-CollectionValidationReport -ValidationResult $result -OutputPath $outputPath
1370 Test-Path $newDir | Should -BeTrue
1371 Test-Path $outputPath | Should -BeTrue
1372 }
1373
1374 It 'Captures multiple distinct ErrorType values in a single run' {
1375 $yaml = @"
1376id: multi-error
1377name: Multi Error Collection
1378description: Has both a path-not-found and a missing-kind error
1379items:
1380 - path: .github/agents/test/nonexistent.agent.md
1381 kind: agent
1382 - path: .github/agents/test/a.agent.md
1383"@
1384 Set-Content -Path (Join-Path $script:collectionsDir 'multi-error.collection.yml') -Value $yaml
1385
1386 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1387
1388 $result.Success | Should -BeFalse
1389 $errorTypes = $result.Results | Select-Object -ExpandProperty ErrorType
1390 $errorTypes | Should -Contain 'PathNotFound'
1391 $errorTypes | Should -Contain 'MissingItemKind'
1392 }
1393
1394 It 'Returns a Results key even when a collection passes validation' {
1395 $manifest = [ordered]@{
1396 id = 'hve-core-all'
1397 name = 'All'
1398 description = 'Canonical'
1399 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
1400 }
1401 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
1402 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
1403
1404 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
1405
1406 $result.Success | Should -BeTrue
1407 $result.Keys | Should -Contain 'Results'
1408 }
1409}
1410