microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/add-second-skill-package

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

819lines · 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 - error paths' {
321 BeforeAll {
322 Import-Module PowerShell-Yaml -ErrorAction Stop
323
324 $script:repoRoot = Join-Path $TestDrive 'error-repo'
325 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
326
327 # Create valid artifacts for reference
328 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
329 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
330 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---\ndescription: agent a\n---'
331 Set-Content -Path (Join-Path $agentsDir 'b.agent.md') -Value '---\ndescription: agent b\n---'
332
333 $instrDir = Join-Path $script:repoRoot '.github/instructions/test'
334 New-Item -ItemType Directory -Path $instrDir -Force | Out-Null
335 Set-Content -Path (Join-Path $instrDir 'test.instructions.md') -Value '---\ndescription: test\n---'
336 }
337
338 BeforeEach {
339 if (Test-Path $script:collectionsDir) {
340 Remove-Item -Path $script:collectionsDir -Recurse -Force
341 }
342 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
343 }
344
345 It 'Returns success with zero collections when directory is empty' {
346 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
347 $result.Success | Should -BeTrue
348 $result.CollectionCount | Should -Be 0
349 }
350
351 It 'Fails when required field is missing' {
352 $yaml = @"
353name: No ID Collection
354description: Missing id field
355items:
356 - path: .github/agents/test/a.agent.md
357 kind: agent
358"@
359 Set-Content -Path (Join-Path $script:collectionsDir 'no-id.collection.yml') -Value $yaml
360
361 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
362 $result.Success | Should -BeFalse
363 }
364
365 It 'Fails for invalid id format' {
366 $manifest = [ordered]@{
367 id = 'INVALID_ID!'
368 name = 'Bad ID'
369 description = 'Invalid id format'
370 items = @(
371 [ordered]@{
372 path = '.github/agents/test/a.agent.md'
373 kind = 'agent'
374 }
375 )
376 }
377 $yaml = ConvertTo-Yaml -Data $manifest
378 Set-Content -Path (Join-Path $script:collectionsDir 'bad-id.collection.yml') -Value $yaml
379
380 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
381 $result.Success | Should -BeFalse
382 }
383
384 It 'Fails for duplicate ids across collections' {
385 $manifest = [ordered]@{
386 id = 'dup-id'
387 name = 'First'
388 description = 'First collection'
389 items = @(
390 [ordered]@{
391 path = '.github/agents/test/a.agent.md'
392 kind = 'agent'
393 }
394 )
395 }
396 $yaml = ConvertTo-Yaml -Data $manifest
397 Set-Content -Path (Join-Path $script:collectionsDir 'dup1.collection.yml') -Value $yaml
398 Set-Content -Path (Join-Path $script:collectionsDir 'dup2.collection.yml') -Value $yaml
399
400 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
401 $result.Success | Should -BeFalse
402 }
403
404 It 'Fails when item path does not exist' {
405 $manifest = [ordered]@{
406 id = 'missing-path'
407 name = 'Missing'
408 description = 'Item path missing'
409 items = @(
410 [ordered]@{
411 path = '.github/agents/test/nonexistent.agent.md'
412 kind = 'agent'
413 }
414 )
415 }
416 $yaml = ConvertTo-Yaml -Data $manifest
417 Set-Content -Path (Join-Path $script:collectionsDir 'missing-path.collection.yml') -Value $yaml
418
419 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
420 $result.Success | Should -BeFalse
421 }
422
423 It 'Fails when item has no kind' {
424 $yaml = @"
425id: no-kind
426name: No Kind
427description: Item missing kind
428items:
429 - path: .github/agents/test/a.agent.md
430"@
431 Set-Content -Path (Join-Path $script:collectionsDir 'no-kind.collection.yml') -Value $yaml
432
433 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
434 $result.Success | Should -BeFalse
435 }
436
437 It 'Fails for invalid item maturity' {
438 $manifest = [ordered]@{
439 id = 'bad-item-mat'
440 name = 'Bad Item Maturity'
441 description = 'Item with invalid maturity'
442 items = @(
443 [ordered]@{
444 path = '.github/agents/test/a.agent.md'
445 kind = 'agent'
446 maturity = 'alpha'
447 }
448 )
449 }
450 $yaml = ConvertTo-Yaml -Data $manifest
451 Set-Content -Path (Join-Path $script:collectionsDir 'bad-item-mat.collection.yml') -Value $yaml
452
453 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
454 $result.Success | Should -BeFalse
455 }
456
457 It 'Fails for kind-suffix mismatch' {
458 $manifest = [ordered]@{
459 id = 'suffix-mismatch'
460 name = 'Suffix Mismatch'
461 description = 'Agent path with wrong suffix'
462 items = @(
463 [ordered]@{
464 path = '.github/instructions/test/test.instructions.md'
465 kind = 'agent'
466 }
467 )
468 }
469 $yaml = ConvertTo-Yaml -Data $manifest
470 Set-Content -Path (Join-Path $script:collectionsDir 'suffix-mismatch.collection.yml') -Value $yaml
471
472 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
473 $result.Success | Should -BeFalse
474 }
475
476 It 'Fails for instruction kind with wrong suffix' {
477 $manifest = [ordered]@{
478 id = 'instr-suffix'
479 name = 'Instruction Suffix'
480 description = 'Instruction item with agent suffix'
481 items = @(
482 [ordered]@{
483 path = '.github/agents/test/a.agent.md'
484 kind = 'instruction'
485 }
486 )
487 }
488 $yaml = ConvertTo-Yaml -Data $manifest
489 Set-Content -Path (Join-Path $script:collectionsDir 'instr-suffix.collection.yml') -Value $yaml
490
491 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
492 $result.Success | Should -BeFalse
493 }
494
495 It 'Detects duplicate artifact keys at distinct paths' {
496 # Two agents at different paths that resolve to the same artifact key
497 $agentsDir2 = Join-Path $script:repoRoot '.github/agents/other'
498 New-Item -ItemType Directory -Path $agentsDir2 -Force | Out-Null
499 Set-Content -Path (Join-Path $agentsDir2 'a.agent.md') -Value '---\ndescription: same name\n---'
500
501 $manifest = [ordered]@{
502 id = 'dup-artifact'
503 name = 'Dup Artifact'
504 description = 'Same artifact key from different paths'
505 items = @(
506 [ordered]@{
507 path = '.github/agents/test/a.agent.md'
508 kind = 'agent'
509 },
510 [ordered]@{
511 path = '.github/agents/other/a.agent.md'
512 kind = 'agent'
513 }
514 )
515 }
516 $yaml = ConvertTo-Yaml -Data $manifest
517 Set-Content -Path (Join-Path $script:collectionsDir 'dup-artifact.collection.yml') -Value $yaml
518
519 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
520 $result.Success | Should -BeFalse
521 }
522
523 It 'Detects shared item missing canonical entry' {
524 # Two collections share the same item but neither is hve-core-all;
525 # hve-core-all exists but does not include a.agent.md - Check 4 fires.
526 $manifest1 = [ordered]@{
527 id = 'share-one'
528 name = 'Share One'
529 description = 'First sharer'
530 items = @(
531 [ordered]@{
532 path = '.github/agents/test/a.agent.md'
533 kind = 'agent'
534 }
535 )
536 }
537 $manifest2 = [ordered]@{
538 id = 'share-two'
539 name = 'Share Two'
540 description = 'Second sharer'
541 items = @(
542 [ordered]@{
543 path = '.github/agents/test/a.agent.md'
544 kind = 'agent'
545 }
546 )
547 }
548 $canonical = [ordered]@{
549 id = 'hve-core-all'
550 name = 'All'
551 description = 'Canonical - missing a.agent.md'
552 items = @(
553 [ordered]@{
554 path = '.github/agents/test/b.agent.md'
555 kind = 'agent'
556 },
557 [ordered]@{
558 path = '.github/instructions/test/test.instructions.md'
559 kind = 'instruction'
560 }
561 )
562 }
563 $yaml1 = ConvertTo-Yaml -Data $manifest1
564 $yaml2 = ConvertTo-Yaml -Data $manifest2
565 $yaml3 = ConvertTo-Yaml -Data $canonical
566 Set-Content -Path (Join-Path $script:collectionsDir 'share-one.collection.yml') -Value $yaml1
567 Set-Content -Path (Join-Path $script:collectionsDir 'share-two.collection.yml') -Value $yaml2
568 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value $yaml3
569 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
570
571 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
572 $result.Success | Should -BeFalse
573 }
574
575 It 'Detects maturity conflict with canonical collection' {
576 # hve-core-all has the item as stable, another collection has it as experimental
577 $canonical = [ordered]@{
578 id = 'hve-core-all'
579 name = 'All'
580 description = 'Canonical collection'
581 items = @(
582 [ordered]@{
583 path = '.github/agents/test/a.agent.md'
584 kind = 'agent'
585 maturity = 'stable'
586 }
587 )
588 }
589 $other = [ordered]@{
590 id = 'conflict-col'
591 name = 'Conflict'
592 description = 'Conflicting maturity'
593 items = @(
594 [ordered]@{
595 path = '.github/agents/test/a.agent.md'
596 kind = 'agent'
597 maturity = 'experimental'
598 }
599 )
600 }
601 $yaml1 = ConvertTo-Yaml -Data $canonical
602 $yaml2 = ConvertTo-Yaml -Data $other
603 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value $yaml1
604 Set-Content -Path (Join-Path $script:collectionsDir 'conflict-col.collection.yml') -Value $yaml2
605
606 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
607 $result.Success | Should -BeFalse
608 }
609}
610
611Describe 'Invoke-CollectionValidation - new checks' {
612 BeforeAll {
613 Import-Module PowerShell-Yaml -ErrorAction Stop
614
615 $script:repoRoot = Join-Path $TestDrive 'new-checks-repo'
616 $script:collectionsDir = Join-Path $script:repoRoot 'collections'
617
618 # Standard artifact - used by most tests
619 $agentsDir = Join-Path $script:repoRoot '.github/agents/test'
620 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
621 Set-Content -Path (Join-Path $agentsDir 'a.agent.md') -Value '---' -Force
622
623 # Orphan artifact - on disk but not necessarily in manifests
624 $orphanDir = Join-Path $script:repoRoot '.github/agents/orphan'
625 New-Item -ItemType Directory -Path $orphanDir -Force | Out-Null
626 Set-Content -Path (Join-Path $orphanDir 'orphan.agent.md') -Value '---' -Force
627 }
628
629 BeforeEach {
630 if (Test-Path $script:collectionsDir) { Remove-Item -Path $script:collectionsDir -Recurse -Force }
631 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
632
633 # Reset agent dirs to pristine state - prevents artifact leakage between tests
634 $agentsBaseDir = Join-Path $script:repoRoot '.github/agents'
635 if (Test-Path $agentsBaseDir) { Remove-Item -Path $agentsBaseDir -Recurse -Force }
636 New-Item -ItemType Directory -Path (Join-Path $agentsBaseDir 'test') -Force | Out-Null
637 Set-Content -Path (Join-Path $agentsBaseDir 'test/a.agent.md') -Value '---' -Force
638 New-Item -ItemType Directory -Path (Join-Path $agentsBaseDir 'orphan') -Force | Out-Null
639 Set-Content -Path (Join-Path $agentsBaseDir 'orphan/orphan.agent.md') -Value '---' -Force
640 }
641
642 # Check 3: companion .collection.md
643
644 It 'Warns but passes when .collection.md companion is missing' {
645 $manifest = [ordered]@{
646 id = 'no-companion'; name = 'No Companion'; description = 'Missing companion md'
647 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
648 }
649 Set-Content -Path (Join-Path $script:collectionsDir 'no-companion.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
650 $canonical = [ordered]@{
651 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
652 items = @(
653 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
654 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
655 )
656 }
657 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
658 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
659
660 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
661 $result.Success | Should -BeTrue
662 $result.ErrorCount | Should -Be 0
663 }
664
665 It 'Passes cleanly when .collection.md companion is present' {
666 $manifest = [ordered]@{
667 id = 'has-companion'; name = 'Has Companion'; description = 'With md'
668 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
669 }
670 Set-Content -Path (Join-Path $script:collectionsDir 'has-companion.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
671 Set-Content -Path (Join-Path $script:collectionsDir 'has-companion.collection.md') -Value '# Has Companion'
672 $canonical = [ordered]@{
673 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
674 items = @(
675 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
676 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
677 )
678 }
679 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
680 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
681
682 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
683 $result.Success | Should -BeTrue
684 }
685
686 # Check 2: intra-collection duplicate
687
688 It 'Fails when the same item appears twice in one collection' {
689 $manifest = [ordered]@{
690 id = 'intra-dup'; name = 'Intra Dup'; description = 'Dup item'
691 items = @(
692 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
693 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' }
694 )
695 }
696 Set-Content -Path (Join-Path $script:collectionsDir 'intra-dup.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
697
698 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
699 $result.Success | Should -BeFalse
700 $result.ErrorCount | Should -BeGreaterOrEqual 1
701 }
702
703 It 'Passes when all items in a collection are distinct' {
704 $agentsDir2 = Join-Path $script:repoRoot '.github/agents/test2'
705 New-Item -ItemType Directory -Path $agentsDir2 -Force | Out-Null
706 Set-Content -Path (Join-Path $agentsDir2 'b.agent.md') -Value '---' -Force
707
708 $manifest = [ordered]@{
709 id = 'distinct-items'; name = 'Distinct'; description = 'Distinct items'
710 items = @(
711 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
712 [ordered]@{ path = '.github/agents/test2/b.agent.md'; kind = 'agent' }
713 )
714 }
715 $canonical = [ordered]@{
716 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
717 items = @(
718 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
719 [ordered]@{ path = '.github/agents/test2/b.agent.md'; kind = 'agent' },
720 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
721 )
722 }
723 Set-Content -Path (Join-Path $script:collectionsDir 'distinct-items.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
724 Set-Content -Path (Join-Path $script:collectionsDir 'distinct-items.collection.md') -Value '# Distinct'
725 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
726 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
727
728 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
729 $result.Success | Should -BeTrue
730 }
731
732 # Check 4: hve-core-all coverage
733
734 It 'Fails when a themed collection item is absent from hve-core-all' {
735 $manifest = [ordered]@{
736 id = 'themed-only'; name = 'Themed Only'; description = 'Item not in hve-core-all'
737 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
738 }
739 # Canonical exists but does NOT include a.agent.md - only orphan - so Check 4 fires
740 $canonical = [ordered]@{
741 id = 'hve-core-all'; name = 'All'; description = 'Canonical - missing themed item'
742 items = @([ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' })
743 }
744 Set-Content -Path (Join-Path $script:collectionsDir 'themed-only.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
745 Set-Content -Path (Join-Path $script:collectionsDir 'themed-only.collection.md') -Value '# Themed'
746 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
747 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
748
749 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
750 $result.Success | Should -BeFalse
751 $result.ErrorCount | Should -BeGreaterOrEqual 1
752 }
753
754 It 'Passes when all themed items are present in hve-core-all' {
755 $themed = [ordered]@{
756 id = 'themed-covered'; name = 'Themed Covered'; description = 'Covered by canonical'
757 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
758 }
759 $canonical = [ordered]@{
760 id = 'hve-core-all'; name = 'All'; description = 'Canonical'
761 items = @(
762 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
763 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
764 )
765 }
766 Set-Content -Path (Join-Path $script:collectionsDir 'themed-covered.collection.yml') -Value (ConvertTo-Yaml -Data $themed)
767 Set-Content -Path (Join-Path $script:collectionsDir 'themed-covered.collection.md') -Value '# Themed Covered'
768 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
769 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
770
771 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
772 $result.Success | Should -BeTrue
773 }
774
775 # Check 1: orphan detection
776
777 It 'Fails when an on-disk artifact is absent from hve-core-all' {
778 # manifest and canonical cover a.agent.md but NOT orphan/orphan.agent.md
779 $manifest = [ordered]@{
780 id = 'partial-coverage'; name = 'Partial'; description = 'Missing orphan'
781 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
782 }
783 $canonical = [ordered]@{
784 id = 'hve-core-all'; name = 'All'; description = 'Canonical - missing orphan'
785 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
786 }
787 Set-Content -Path (Join-Path $script:collectionsDir 'partial-coverage.collection.yml') -Value (ConvertTo-Yaml -Data $manifest)
788 Set-Content -Path (Join-Path $script:collectionsDir 'partial-coverage.collection.md') -Value '# Partial'
789 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
790 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
791
792 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
793 $result.Success | Should -BeFalse
794 $result.ErrorCount | Should -BeGreaterOrEqual 1
795 }
796
797 It 'Warns but passes when artifact is in hve-core-all but not in any themed collection' {
798 # Themed covers only a.agent.md; canonical covers both - orphan is canonical-only
799 $themed = [ordered]@{
800 id = 'themed-partial'; name = 'Themed Partial'; description = 'Missing orphan in themed'
801 items = @([ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' })
802 }
803 $canonical = [ordered]@{
804 id = 'hve-core-all'; name = 'All'; description = 'Canonical - covers orphan'
805 items = @(
806 [ordered]@{ path = '.github/agents/test/a.agent.md'; kind = 'agent' },
807 [ordered]@{ path = '.github/agents/orphan/orphan.agent.md'; kind = 'agent' }
808 )
809 }
810 Set-Content -Path (Join-Path $script:collectionsDir 'themed-partial.collection.yml') -Value (ConvertTo-Yaml -Data $themed)
811 Set-Content -Path (Join-Path $script:collectionsDir 'themed-partial.collection.md') -Value '# Themed Partial'
812 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') -Value (ConvertTo-Yaml -Data $canonical)
813 Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.md') -Value '# All'
814
815 $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot
816 $result.Success | Should -BeTrue
817 $result.ErrorCount | Should -Be 0
818 }
819}
820