microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
ci/2086-enforce-powershell-coverage

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/tests/extension/Prepare-Extension.Tests.ps1

3015lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4
5BeforeAll {
6 . $PSScriptRoot/../../extension/Prepare-Extension.ps1
7}
8
9#region Package Generation Function Tests
10
11Describe 'Get-CollectionDisplayName' {
12 It 'Returns displayName when present' {
13 $manifest = @{ displayName = 'My Display Name'; name = 'fallback' }
14 $result = Get-CollectionDisplayName -CollectionManifest $manifest -DefaultValue 'default'
15 $result | Should -Be 'My Display Name'
16 }
17
18 It 'Derives display name from name when displayName absent' {
19 $manifest = @{ name = 'Git Workflow' }
20 $result = Get-CollectionDisplayName -CollectionManifest $manifest -DefaultValue 'default'
21 $result | Should -Be 'HVE Core - Git Workflow'
22 }
23
24 It 'Returns default when both displayName and name absent' {
25 $manifest = @{ id = 'test' }
26 $result = Get-CollectionDisplayName -CollectionManifest $manifest -DefaultValue 'Fallback'
27 $result | Should -Be 'Fallback'
28 }
29
30 It 'Ignores whitespace-only displayName' {
31 $manifest = @{ displayName = ' '; name = 'valid' }
32 $result = Get-CollectionDisplayName -CollectionManifest $manifest -DefaultValue 'default'
33 $result | Should -Be 'HVE Core - valid'
34 }
35}
36
37Describe 'Copy-TemplateWithOverrides' {
38 It 'Overrides existing properties' {
39 $template = [PSCustomObject]@{ name = 'original'; version = '1.0.0' }
40 $result = Copy-TemplateWithOverrides -Template $template -Overrides @{ name = 'overridden' }
41 $result.name | Should -Be 'overridden'
42 $result.version | Should -Be '1.0.0'
43 }
44
45 It 'Preserves template property order' {
46 $template = [PSCustomObject]@{ a = '1'; b = '2'; c = '3' }
47 $result = Copy-TemplateWithOverrides -Template $template -Overrides @{ b = 'new' }
48 $names = @($result.PSObject.Properties.Name)
49 $names[0] | Should -Be 'a'
50 $names[1] | Should -Be 'b'
51 $names[2] | Should -Be 'c'
52 }
53
54 It 'Appends new override keys not in template' {
55 $template = [PSCustomObject]@{ name = 'ext' }
56 $result = Copy-TemplateWithOverrides -Template $template -Overrides @{ name = 'ext'; extra = 'value' }
57 $result.extra | Should -Be 'value'
58 }
59
60 It 'Returns PSCustomObject' {
61 $template = [PSCustomObject]@{ name = 'ext' }
62 $result = Copy-TemplateWithOverrides -Template $template -Overrides @{}
63 $result | Should -BeOfType [PSCustomObject]
64 }
65}
66
67Describe 'Set-JsonFile' {
68 BeforeAll {
69 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
70 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
71 }
72
73 AfterAll {
74 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
75 }
76
77 It 'Creates file with JSON content' {
78 $path = Join-Path $script:tempDir 'test.json'
79 Set-JsonFile -Path $path -Content @{ name = 'test'; version = '1.0.0' }
80 Test-Path $path | Should -BeTrue
81 $content = Get-Content -Path $path -Raw | ConvertFrom-Json
82 $content.name | Should -Be 'test'
83 }
84
85 It 'Creates parent directories when missing' {
86 $path = Join-Path $script:tempDir 'nested/deep/test.json'
87 Set-JsonFile -Path $path -Content @{ key = 'value' }
88 Test-Path $path | Should -BeTrue
89 }
90}
91
92Describe 'Remove-StaleGeneratedFiles' {
93 BeforeAll {
94 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
95 $script:extDir = Join-Path $script:tempDir 'extension'
96 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
97 }
98
99 AfterAll {
100 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
101 }
102
103 It 'Removes stale package.*.json files not in expected set' {
104 $keepFile = Join-Path $script:extDir 'package.rpi.json'
105 $staleFile = Join-Path $script:extDir 'package.obsolete.json'
106 '{}' | Set-Content -Path $keepFile
107 '{}' | Set-Content -Path $staleFile
108
109 Remove-StaleGeneratedFiles -RepoRoot $script:tempDir -ExpectedFiles @($keepFile)
110
111 Test-Path $keepFile | Should -BeTrue
112 Test-Path $staleFile | Should -BeFalse
113 }
114
115 It 'Does not remove non-collection files' {
116 $regularFile = Join-Path $script:extDir 'README.md'
117 '# Test' | Set-Content -Path $regularFile
118
119 Remove-StaleGeneratedFiles -RepoRoot $script:tempDir -ExpectedFiles @()
120
121 Test-Path $regularFile | Should -BeTrue
122 }
123}
124
125Describe 'Invoke-ExtensionCollectionsGeneration' {
126 BeforeAll {
127 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
128
129 # Set up minimal repo structure
130 $collectionsDir = Join-Path $script:tempDir 'collections'
131 $templatesDir = Join-Path $script:tempDir 'extension/templates'
132 New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null
133 New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null
134
135 # Package template
136 @{
137 name = 'hve-core'
138 displayName = 'HVE Core'
139 version = '2.0.0'
140 description = 'Default description'
141 publisher = 'test-pub'
142 engines = @{ vscode = '^1.80.0' }
143 contributes = @{}
144 } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json')
145
146 # hve-core collection (flagship)
147 @"
148id: hve-core
149name: HVE Core
150displayName: HVE Core
151description: All artifacts
152"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core.collection.yml')
153
154 # ado collection
155 @"
156id: ado
157name: ADO Workflow
158displayName: HVE Core - ADO Workflow
159description: ADO workflow agents
160"@ | Set-Content -Path (Join-Path $collectionsDir 'ado.collection.yml')
161
162 # hve-core-all collection (no description to test fallback)
163 @"
164id: hve-core-all
165name: All
166displayName: HVE Core - All
167"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.yml')
168 }
169
170 AfterAll {
171 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
172 }
173
174 It 'Generates package.json for hve-core' {
175 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
176 $pkgPath = Join-Path $script:tempDir 'extension/package.json'
177 Test-Path $pkgPath | Should -BeTrue
178 $pkg = Get-Content $pkgPath -Raw | ConvertFrom-Json
179 $pkg.name | Should -Be 'hve-core'
180 $pkg.version | Should -Be '2.0.0'
181 }
182
183 It 'Generates collection package file for non-default collection' {
184 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
185 $pkgPath = Join-Path $script:tempDir 'extension/package.ado.json'
186 Test-Path $pkgPath | Should -BeTrue
187 $pkg = Get-Content $pkgPath -Raw | ConvertFrom-Json
188 $pkg.name | Should -Be 'hve-ado'
189 $pkg.displayName | Should -Be 'HVE Core - ADO Workflow'
190 }
191
192 It 'Returns array of generated file paths' {
193 $result = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
194 $result.Count | Should -Be 3
195 }
196
197 It 'Propagates version from template to all generated files' {
198 $result = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
199 foreach ($file in $result) {
200 $pkg = Get-Content $file -Raw | ConvertFrom-Json
201 $pkg.version | Should -Be '2.0.0'
202 }
203 }
204
205 It 'Removes stale collection files not matching current collections' {
206 $staleFile = Join-Path $script:tempDir 'extension/package.obsolete.json'
207 '{}' | Set-Content -Path $staleFile
208
209 Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
210
211 Test-Path $staleFile | Should -BeFalse
212 }
213
214 It 'Generates package for hve-core-all with description fallback' {
215 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
216 $pkgPath = Join-Path $script:tempDir 'extension/package.hve-core-all.json'
217 Test-Path $pkgPath | Should -BeTrue
218 $pkg = Get-Content $pkgPath -Raw | ConvertFrom-Json
219 $pkg.name | Should -Be 'hve-core-all'
220 $pkg.displayName | Should -Be 'HVE Core - All'
221 # Falls back to template description when collection lacks description
222 $pkg.description | Should -Be 'Default description'
223 }
224
225 It 'Throws when package template is missing' {
226 $badRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
227 New-Item -ItemType Directory -Path (Join-Path $badRoot 'collections') -Force | Out-Null
228 New-Item -ItemType Directory -Path (Join-Path $badRoot 'extension/templates') -Force | Out-Null
229 @"
230id: test
231"@ | Set-Content -Path (Join-Path $badRoot 'collections/test.collection.yml')
232
233 { Invoke-ExtensionCollectionsGeneration -RepoRoot $badRoot } | Should -Throw '*Package template not found*'
234
235 Remove-Item -Path $badRoot -Recurse -Force -ErrorAction SilentlyContinue
236 }
237
238 It 'Throws when no collection files exist' {
239 $emptyRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
240 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'collections') -Force | Out-Null
241 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'extension/templates') -Force | Out-Null
242 @{ name = 'test'; version = '1.0.0' } | ConvertTo-Json | Set-Content -Path (Join-Path $emptyRoot 'extension/templates/package.template.json')
243
244 { Invoke-ExtensionCollectionsGeneration -RepoRoot $emptyRoot } | Should -Throw '*No root collection files found*'
245
246 Remove-Item -Path $emptyRoot -Recurse -Force -ErrorAction SilentlyContinue
247 }
248}
249
250Describe 'New-CollectionReadme' {
251 BeforeAll {
252 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
253 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
254
255 # Resolve the real template from the repo
256 $script:repoRoot = (Get-Item "$PSScriptRoot/../../..").FullName
257 $script:templatePath = Join-Path $script:repoRoot 'extension/templates/README.template.md'
258
259 # Create mock artifact files with frontmatter descriptions
260 $agentsDir = Join-Path $script:tempDir '.github/agents'
261 $promptsDir = Join-Path $script:tempDir '.github/prompts'
262 $instrDir = Join-Path $script:tempDir '.github/instructions'
263 $skillsDir = Join-Path $script:tempDir '.github/skills/my-skill'
264 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
265 New-Item -ItemType Directory -Path $promptsDir -Force | Out-Null
266 New-Item -ItemType Directory -Path $instrDir -Force | Out-Null
267 New-Item -ItemType Directory -Path $skillsDir -Force | Out-Null
268
269 @"
270---
271description: "Alpha agent description"
272---
273# Alpha
274"@ | Set-Content -Path (Join-Path $agentsDir 'alpha.agent.md')
275
276 @"
277---
278description: "Zebra agent description"
279---
280# Zebra
281"@ | Set-Content -Path (Join-Path $agentsDir 'zebra.agent.md')
282
283 @"
284---
285description: "My prompt description"
286---
287# Prompt
288"@ | Set-Content -Path (Join-Path $promptsDir 'my-prompt.prompt.md')
289
290 @"
291---
292description: "My instruction description"
293applyTo: "**/*.ps1"
294---
295# Instruction
296"@ | Set-Content -Path (Join-Path $instrDir 'my-instr.instructions.md')
297
298 @"
299---
300name: my-skill
301description: "My skill description"
302---
303# Skill
304"@ | Set-Content -Path (Join-Path $skillsDir 'SKILL.md')
305 }
306
307 AfterAll {
308 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
309 }
310
311 It 'Generates README with title and description from collection manifest' {
312 $collection = @{
313 id = 'test-coll'
314 name = 'Test Collection'
315 description = 'A test collection for unit testing'
316 items = @()
317 }
318 $mdPath = Join-Path $script:tempDir 'test.collection.md'
319 'Body content goes here.' | Set-Content -Path $mdPath
320 $outPath = Join-Path $script:tempDir 'README.test-coll.md'
321
322 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
323
324 $content = Get-Content -Path $outPath -Raw
325 $content | Should -Match '# HVE Core - Test Collection'
326 $content | Should -Match '> A test collection for unit testing'
327 $content | Should -Match 'Body content goes here'
328 }
329
330 It 'Uses HVE Core as title for hve-core collection' {
331 $collection = @{
332 id = 'hve-core'
333 name = 'HVE Core'
334 description = 'Full bundle'
335 items = @()
336 }
337 $mdPath = Join-Path $script:tempDir 'core.collection.md'
338 'All artifacts.' | Set-Content -Path $mdPath
339 $outPath = Join-Path $script:tempDir 'README.md'
340
341 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
342
343 $content = Get-Content -Path $outPath -Raw
344 $content | Should -Match '# HVE Core'
345 $content | Should -Not -Match '# HVE Core All'
346 }
347
348 It 'Generates sorted artifact tables with descriptions grouped by kind' {
349 $collection = @{
350 id = 'multi'
351 name = 'Multi'
352 description = 'Multi-artifact test'
353 items = @(
354 @{ kind = 'agent'; path = '.github/agents/zebra.agent.md' },
355 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md' },
356 @{ kind = 'prompt'; path = '.github/prompts/my-prompt.prompt.md' },
357 @{ kind = 'instruction'; path = '.github/instructions/my-instr.instructions.md' },
358 @{ kind = 'skill'; path = '.github/skills/my-skill/' }
359 )
360 }
361 $mdPath = Join-Path $script:tempDir 'multi.collection.md'
362 'Test body.' | Set-Content -Path $mdPath
363 $outPath = Join-Path $script:tempDir 'README.multi.md'
364
365 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
366
367 $content = Get-Content -Path $outPath -Raw
368 $content | Should -Match '### Chat Agents'
369 $content | Should -Match '\| Name \| Description \|'
370 $content | Should -Match '\*\*alpha\*\*.*Alpha agent description'
371 $content | Should -Match '\*\*zebra\*\*.*Zebra agent description'
372 $content | Should -Match '### Prompts'
373 $content | Should -Match '\*\*my-prompt\*\*.*My prompt description'
374 $content | Should -Match '### Instructions'
375 $content | Should -Match '\*\*my-instr\*\*.*My instruction description'
376 $content | Should -Match '### Skills'
377 $content | Should -Match '\*\*my-skill\*\*.*My skill description'
378 }
379
380 It 'Includes Full Edition link for non-default collections' {
381 $collection = @{
382 id = 'test-edition'
383 name = 'Test Edition'
384 description = 'Test edition test'
385 items = @()
386 }
387 $mdPath = Join-Path $script:tempDir 'test-edition.collection.md'
388 'Test edition body.' | Set-Content -Path $mdPath
389 $outPath = Join-Path $script:tempDir 'README.test-edition.md'
390
391 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
392
393 $content = Get-Content -Path $outPath -Raw
394 $content | Should -Match '## Full Edition'
395 $content | Should -Match 'HVE Core.*extension'
396 }
397
398 It 'Excludes Full Edition link for hve-core' {
399 $collection = @{
400 id = 'hve-core'
401 name = 'HVE Core'
402 description = 'Flagship bundle'
403 items = @()
404 }
405 $mdPath = Join-Path $script:tempDir 'core2.collection.md'
406 'Core body.' | Set-Content -Path $mdPath
407 $outPath = Join-Path $script:tempDir 'README.core2.md'
408
409 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
410
411 $content = Get-Content -Path $outPath -Raw
412 $content | Should -Not -Match '## Full Edition'
413 }
414
415 It 'Excludes Full Edition link for hve-core-all' {
416 $collection = @{
417 id = 'hve-core-all'
418 name = 'All'
419 description = 'Full bundle'
420 items = @()
421 }
422 $mdPath = Join-Path $script:tempDir 'all2.collection.md'
423 'All body.' | Set-Content -Path $mdPath
424 $outPath = Join-Path $script:tempDir 'README.all2.md'
425
426 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
427
428 $content = Get-Content -Path $outPath -Raw
429 $content | Should -Not -Match '## Full Edition'
430 }
431
432 It 'Includes common footer sections' {
433 $collection = @{
434 id = 'footer-test'
435 name = 'Footer'
436 description = 'Footer test'
437 items = @()
438 }
439 $mdPath = Join-Path $script:tempDir 'footer.collection.md'
440 'Footer body.' | Set-Content -Path $mdPath
441 $outPath = Join-Path $script:tempDir 'README.footer.md'
442
443 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
444
445 $content = Get-Content -Path $outPath -Raw
446 $content | Should -Match '## Getting Started'
447 $content | Should -Match '## Pre-release Channel'
448 $content | Should -Match '## Requirements'
449 $content | Should -Match '## License'
450 $content | Should -Match '## Support'
451 $content | Should -Match 'Microsoft ISE HVE Essentials'
452 }
453
454 It 'Handles collection without description key' {
455 $collection = @{
456 id = 'no-desc'
457 name = 'No Description'
458 items = @()
459 }
460 $mdPath = Join-Path $script:tempDir 'no-desc.collection.md'
461 'No description body.' | Set-Content -Path $mdPath
462 $outPath = Join-Path $script:tempDir 'README.no-desc.md'
463
464 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
465
466 $content = Get-Content -Path $outPath -Raw
467 $content | Should -Match '# HVE Core - No Description'
468 $content | Should -Match 'No description body'
469 }
470
471 Context 'Maturity filtering' {
472 It 'Excludes experimental items when AllowedMaturities contains only stable' {
473 $collection = @{
474 id = 'maturity-test'
475 name = 'Maturity Test'
476 description = 'Maturity filtering test'
477 items = @(
478 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md'; maturity = 'stable' },
479 @{ kind = 'agent'; path = '.github/agents/zebra.agent.md'; maturity = 'experimental' }
480 )
481 }
482 $mdPath = Join-Path $script:tempDir 'maturity-filter.collection.md'
483 'Maturity body.' | Set-Content -Path $mdPath
484 $outPath = Join-Path $script:tempDir 'README.maturity-filter.md'
485
486 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath -AllowedMaturities @('stable')
487
488 $content = Get-Content -Path $outPath -Raw
489 $content | Should -Match 'alpha'
490 $content | Should -Not -Match 'zebra'
491 }
492
493 It 'Includes experimental items when AllowedMaturities allows them' {
494 $collection = @{
495 id = 'maturity-test2'
496 name = 'Maturity Test 2'
497 description = 'Maturity filtering test'
498 items = @(
499 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md'; maturity = 'stable' },
500 @{ kind = 'agent'; path = '.github/agents/zebra.agent.md'; maturity = 'experimental' }
501 )
502 }
503 $mdPath = Join-Path $script:tempDir 'maturity-all.collection.md'
504 'All maturity body.' | Set-Content -Path $mdPath
505 $outPath = Join-Path $script:tempDir 'README.maturity-all.md'
506
507 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath -AllowedMaturities @('stable', 'preview', 'experimental')
508
509 $content = Get-Content -Path $outPath -Raw
510 $content | Should -Match 'alpha'
511 $content | Should -Match 'zebra'
512 }
513
514 It 'Excludes deprecated items regardless of channel' {
515 $collection = @{
516 id = 'deprecated-test'
517 name = 'Deprecated Test'
518 description = 'Deprecated filtering test'
519 items = @(
520 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md'; maturity = 'stable' },
521 @{ kind = 'agent'; path = '.github/agents/zebra.agent.md'; maturity = 'deprecated' }
522 )
523 }
524 $mdPath = Join-Path $script:tempDir 'deprecated.collection.md'
525 'Deprecated body.' | Set-Content -Path $mdPath
526 $outPath = Join-Path $script:tempDir 'README.deprecated.md'
527
528 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath -AllowedMaturities @('stable', 'preview', 'experimental')
529
530 $content = Get-Content -Path $outPath -Raw
531 $content | Should -Match 'alpha'
532 $content | Should -Not -Match 'zebra'
533 }
534 }
535
536 Context 'Template marker handling' {
537 It 'Preserves intro text and replaces marker section in README' {
538 $collection = @{
539 id = 'marker-intro'
540 name = 'Marker Intro'
541 description = 'Marker intro test'
542 items = @(
543 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md' }
544 )
545 }
546 $mdPath = Join-Path $script:tempDir 'marker-intro.collection.md'
547 @"
548Hand-authored intro paragraph.
549
550<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
551
552Old stale artifact list.
553
554<!-- END AUTO-GENERATED ARTIFACTS -->
555"@ | Set-Content -Path $mdPath -Encoding utf8NoBOM
556 $outPath = Join-Path $script:tempDir 'README.marker-intro.md'
557
558 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
559
560 $content = Get-Content -Path $outPath -Raw
561 $content | Should -Match 'Hand-authored intro paragraph'
562 $content | Should -Not -Match 'Old stale artifact list'
563 }
564
565 It 'Writes back updated artifact section into collection.md' {
566 $collection = @{
567 id = 'marker-wb'
568 name = 'Marker Writeback'
569 description = 'Marker writeback test'
570 items = @(
571 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md' }
572 )
573 }
574 $mdPath = Join-Path $script:tempDir 'marker-wb.collection.md'
575 @"
576Writeback intro.
577
578## Included Artifacts
579
580<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
581
582Old content to replace.
583
584<!-- END AUTO-GENERATED ARTIFACTS -->
585"@ | Set-Content -Path $mdPath -Encoding utf8NoBOM
586 $outPath = Join-Path $script:tempDir 'README.marker-wb.md'
587
588 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
589
590 $mdContent = Get-Content -Path $mdPath -Raw
591 $mdContent | Should -Match '<!-- BEGIN AUTO-GENERATED ARTIFACTS -->'
592 $mdContent | Should -Match '<!-- END AUTO-GENERATED ARTIFACTS -->'
593 ([regex]::Matches($mdContent, '(?m)^## Included Artifacts$')).Count | Should -Be 1
594 $mdContent | Should -Match 'alpha'
595 $mdContent | Should -Not -Match 'Old content to replace'
596 }
597
598 It 'Does not inject a duplicate Included Artifacts heading inside markers' {
599 $collection = @{
600 id = 'no-dup-h2'
601 name = 'No Duplicate H2'
602 description = 'Ensures intro H2 is not duplicated inside auto-generated block'
603 items = @(
604 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md' }
605 )
606 }
607 $mdPath = Join-Path $script:tempDir 'no-dup-h2.collection.md'
608 @"
609# No Duplicate H2
610
611Intro paragraph.
612
613## Included Artifacts
614
615<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
616
617Old generated content.
618
619<!-- END AUTO-GENERATED ARTIFACTS -->
620"@ | Set-Content -Path $mdPath -Encoding utf8NoBOM
621 $outPath = Join-Path $script:tempDir 'README.no-dup-h2.md'
622
623 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
624
625 $mdContent = Get-Content -Path $mdPath -Raw
626 ([regex]::Matches($mdContent, '(?m)^##\s+Included Artifacts\s*$')).Count | Should -Be 1
627
628 $beginIndex = $mdContent.IndexOf('<!-- BEGIN AUTO-GENERATED ARTIFACTS -->')
629 $endIndex = $mdContent.IndexOf('<!-- END AUTO-GENERATED ARTIFACTS -->')
630 $beginIndex | Should -BeGreaterThan -1
631 $endIndex | Should -BeGreaterThan $beginIndex
632 $generatedSection = $mdContent.Substring($beginIndex, $endIndex - $beginIndex)
633 $generatedSection | Should -Not -Match '(?m)^##\s+Included Artifacts\s*$'
634 }
635
636 It 'Works without markers for backward compatibility' {
637 $collection = @{
638 id = 'no-markers'
639 name = 'No Markers'
640 description = 'No markers test'
641 items = @(
642 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md' }
643 )
644 }
645 $mdPath = Join-Path $script:tempDir 'no-markers.collection.md'
646 'Legacy body content without markers.' | Set-Content -Path $mdPath -Encoding utf8NoBOM
647 $outPath = Join-Path $script:tempDir 'README.no-markers.md'
648
649 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
650
651 $content = Get-Content -Path $outPath -Raw
652 $content | Should -Match 'Legacy body content without markers'
653 }
654
655 It 'Preserves footer content after end marker' {
656 $collection = @{
657 id = 'marker-footer'
658 name = 'Marker Footer'
659 description = 'Marker footer test'
660 items = @(
661 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md' }
662 )
663 }
664 $mdPath = Join-Path $script:tempDir 'marker-footer.collection.md'
665 @"
666Footer intro.
667
668<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
669
670Old artifacts.
671
672<!-- END AUTO-GENERATED ARTIFACTS -->
673
674## Prerequisites
675
676This requires setup first.
677"@ | Set-Content -Path $mdPath -Encoding utf8NoBOM
678 $outPath = Join-Path $script:tempDir 'README.marker-footer.md'
679
680 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
681
682 $readmeContent = Get-Content -Path $outPath -Raw
683 $readmeContent | Should -Match 'Footer intro'
684 $readmeContent | Should -Match 'Prerequisites'
685
686 $mdContent = Get-Content -Path $mdPath -Raw
687 $mdContent | Should -Match '<!-- BEGIN AUTO-GENERATED ARTIFACTS -->'
688 $mdContent | Should -Match '<!-- END AUTO-GENERATED ARTIFACTS -->'
689 $mdContent | Should -Match '## Prerequisites'
690 $mdContent | Should -Match 'This requires setup first'
691 }
692
693 It 'Produces identical output when run twice (idempotent regeneration)' {
694 $collection = @{
695 id = 'idempotent'
696 name = 'Idempotent'
697 description = 'Idempotency guard test'
698 items = @(
699 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md' },
700 @{ kind = 'agent'; path = '.github/agents/zebra.agent.md' },
701 @{ kind = 'prompt'; path = '.github/prompts/my-prompt.prompt.md' },
702 @{ kind = 'instruction'; path = '.github/instructions/my-instr.instructions.md' },
703 @{ kind = 'skill'; path = '.github/skills/my-skill/' }
704 )
705 }
706 $mdPath = Join-Path $script:tempDir 'idempotent.collection.md'
707 @"
708Idempotent intro.
709
710<!-- BEGIN AUTO-GENERATED ARTIFACTS -->
711
712Stale artifact placeholder.
713
714<!-- END AUTO-GENERATED ARTIFACTS -->
715
716## Prerequisites
717
718Footer content preserved across runs.
719"@ | Set-Content -Path $mdPath -Encoding utf8NoBOM
720 $outPath = Join-Path $script:tempDir 'README.idempotent.md'
721
722 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
723 $firstReadme = Get-Content -Path $outPath -Raw
724 $firstMd = Get-Content -Path $mdPath -Raw
725
726 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
727 $secondReadme = Get-Content -Path $outPath -Raw
728 $secondMd = Get-Content -Path $mdPath -Raw
729
730 $secondReadme | Should -BeExactly $firstReadme
731 $secondMd | Should -BeExactly $firstMd
732 }
733 }
734}
735
736#endregion Package Generation Function Tests
737
738Describe 'Get-AllowedMaturities' {
739 It 'Returns only stable for Stable channel' {
740 $result = Get-AllowedMaturities -Channel 'Stable'
741 $result | Should -Be @('stable')
742 }
743
744 It 'Returns all maturities for PreRelease channel' {
745 $result = Get-AllowedMaturities -Channel 'PreRelease'
746 $result | Should -Contain 'stable'
747 $result | Should -Contain 'preview'
748 $result | Should -Contain 'experimental'
749 }
750
751}
752
753Describe 'Test-CollectionMaturityEligible' {
754 It 'Returns eligible for stable collection on Stable channel' {
755 $manifest = @{ id = 'test'; maturity = 'stable' }
756 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
757 $result.IsEligible | Should -BeTrue
758 $result.Reason | Should -BeNullOrEmpty
759 }
760
761 It 'Returns eligible for stable collection on PreRelease channel' {
762 $manifest = @{ id = 'test'; maturity = 'stable' }
763 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
764 $result.IsEligible | Should -BeTrue
765 }
766
767 It 'Returns eligible for preview collection on Stable channel' {
768 $manifest = @{ id = 'test'; maturity = 'preview' }
769 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
770 $result.IsEligible | Should -BeTrue
771 }
772
773 It 'Returns eligible for preview collection on PreRelease channel' {
774 $manifest = @{ id = 'test'; maturity = 'preview' }
775 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
776 $result.IsEligible | Should -BeTrue
777 }
778
779 It 'Returns ineligible for experimental collection on Stable channel' {
780 $manifest = @{ id = 'exp-coll'; maturity = 'experimental' }
781 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
782 $result.IsEligible | Should -BeFalse
783 $result.Reason | Should -Match 'experimental.*excluded from Stable'
784 }
785
786 It 'Returns eligible for experimental collection on PreRelease channel' {
787 $manifest = @{ id = 'exp-coll'; maturity = 'experimental' }
788 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
789 $result.IsEligible | Should -BeTrue
790 }
791
792 It 'Returns ineligible for deprecated collection on Stable channel' {
793 $manifest = @{ id = 'old-coll'; maturity = 'deprecated' }
794 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
795 $result.IsEligible | Should -BeFalse
796 $result.Reason | Should -Match 'deprecated.*excluded from all channels'
797 }
798
799 It 'Returns ineligible for deprecated collection on PreRelease channel' {
800 $manifest = @{ id = 'old-coll'; maturity = 'deprecated' }
801 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
802 $result.IsEligible | Should -BeFalse
803 $result.Reason | Should -Match 'deprecated.*excluded from all channels'
804 }
805
806 It 'Returns ineligible for removed collection on Stable channel' {
807 $manifest = @{ id = 'gone-coll'; maturity = 'removed' }
808 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
809 $result.IsEligible | Should -BeFalse
810 $result.Reason | Should -Match 'removed.*excluded from all channels'
811 }
812
813 It 'Returns ineligible for removed collection on PreRelease channel' {
814 $manifest = @{ id = 'gone-coll'; maturity = 'removed' }
815 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
816 $result.IsEligible | Should -BeFalse
817 $result.Reason | Should -Match 'removed.*excluded from all channels'
818 }
819
820 It 'Defaults to stable when maturity key is absent' {
821 $manifest = @{ id = 'no-maturity' }
822 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
823 $result.IsEligible | Should -BeTrue
824 }
825
826 It 'Defaults to stable when maturity value is empty string' {
827 $manifest = @{ id = 'empty-maturity'; maturity = '' }
828 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
829 $result.IsEligible | Should -BeTrue
830 }
831
832 It 'Returns ineligible for unknown maturity value' {
833 $manifest = @{ id = 'bad-coll'; maturity = 'alpha' }
834 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
835 $result.IsEligible | Should -BeFalse
836 $result.Reason | Should -Match 'invalid maturity value'
837 }
838
839 It 'Returns hashtable with expected keys' {
840 $manifest = @{ id = 'test'; maturity = 'stable' }
841 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
842 $result.Keys | Should -Contain 'IsEligible'
843 $result.Keys | Should -Contain 'Reason'
844 }
845}
846
847Describe 'Test-PathsExist' {
848 BeforeAll {
849 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
850 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
851 $script:extDir = Join-Path $script:tempDir 'extension'
852 $script:ghDir = Join-Path $script:tempDir '.github'
853 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
854 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
855 $script:pkgJson = Join-Path $script:extDir 'package.json'
856 '{}' | Set-Content -Path $script:pkgJson
857 }
858
859 AfterAll {
860 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
861 }
862
863 It 'Returns valid when all paths exist' {
864 $result = Test-PathsExist -ExtensionDir $script:extDir -PackageJsonPath $script:pkgJson -GitHubDir $script:ghDir
865 $result.IsValid | Should -BeTrue
866 $result.MissingPaths | Should -BeNullOrEmpty
867 }
868
869 It 'Returns invalid when extension dir missing' {
870 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-ext-dir-12345')
871 $result = Test-PathsExist -ExtensionDir $nonexistentPath -PackageJsonPath $script:pkgJson -GitHubDir $script:ghDir
872 $result.IsValid | Should -BeFalse
873 $result.MissingPaths | Should -Contain $nonexistentPath
874 }
875
876 It 'Collects multiple missing paths' {
877 $missing1 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-1')
878 $missing2 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-2')
879 $missing3 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-3')
880 $result = Test-PathsExist -ExtensionDir $missing1 -PackageJsonPath $missing2 -GitHubDir $missing3
881 $result.IsValid | Should -BeFalse
882 $result.MissingPaths.Count | Should -Be 3
883 }
884}
885
886Describe 'Get-DiscoveredAgents' {
887 BeforeAll {
888 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
889 $script:agentsDir = Join-Path $script:tempDir 'agents'
890 $script:agentsSubDir = Join-Path $script:agentsDir 'test-collection'
891 New-Item -ItemType Directory -Path $script:agentsSubDir -Force | Out-Null
892
893 # Create test agent files in subdirectory (distributable)
894 @'
895---
896description: "Stable agent"
897---
898'@ | Set-Content -Path (Join-Path $script:agentsSubDir 'stable.agent.md')
899
900 @'
901---
902description: "Preview agent"
903---
904'@ | Set-Content -Path (Join-Path $script:agentsSubDir 'preview.agent.md')
905
906 # Create root-level agent (repo-specific, should be skipped)
907 @'
908---
909description: "Root-level agent"
910---
911'@ | Set-Content -Path (Join-Path $script:agentsDir 'root-agent.agent.md')
912
913 }
914
915 AfterAll {
916 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
917 }
918
919 It 'Discovers agents matching allowed maturities' {
920 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @()
921 $result.DirectoryExists | Should -BeTrue
922 $result.Agents.Count | Should -Be 2
923 }
924
925 It 'Filters agents by maturity' {
926 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('preview') -ExcludedAgents @()
927 $result.Agents.Count | Should -Be 0
928 $result.Skipped.Count | Should -Be 3
929 }
930
931 It 'Excludes specified agents' {
932 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @('stable')
933 $result.Agents.Count | Should -Be 1
934 }
935
936 It 'Returns empty when directory does not exist' {
937 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-agents-dir-12345')
938 $result = Get-DiscoveredAgents -AgentsDir $nonexistentPath -AllowedMaturities @('stable') -ExcludedAgents @()
939 $result.DirectoryExists | Should -BeFalse
940 $result.Agents | Should -BeNullOrEmpty
941 }
942
943 It 'Skips root-level repo-specific agents with correct skip reason' {
944 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @()
945 $agentNames = $result.Agents | ForEach-Object { $_.name }
946 $agentNames | Should -Not -Contain 'root-agent'
947 $skipped = $result.Skipped | Where-Object { $_.Name -eq 'root-agent' }
948 $skipped | Should -Not -BeNullOrEmpty
949 $skipped.Reason | Should -Match 'repo-specific'
950 }
951}
952
953Describe 'Get-DiscoveredPrompts' {
954 BeforeAll {
955 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
956 $script:promptsDir = Join-Path $script:tempDir 'prompts'
957 $script:promptsSubDir = Join-Path $script:promptsDir 'test-collection'
958 $script:ghDir = Join-Path $script:tempDir '.github'
959 New-Item -ItemType Directory -Path $script:promptsSubDir -Force | Out-Null
960 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
961
962 @'
963---
964description: "Test prompt"
965---
966'@ | Set-Content -Path (Join-Path $script:promptsSubDir 'test.prompt.md')
967 }
968
969 AfterAll {
970 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
971 }
972
973 It 'Discovers prompts in directory' {
974 $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
975 $result.DirectoryExists | Should -BeTrue
976 $result.Prompts.Count | Should -BeGreaterThan 0
977 }
978
979 It 'Returns empty when directory does not exist' {
980 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-prompts-dir-12345')
981 $result = Get-DiscoveredPrompts -PromptsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable')
982 $result.DirectoryExists | Should -BeFalse
983 }
984}
985
986Describe 'Get-DiscoveredInstructions' {
987 BeforeAll {
988 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
989 $script:instrDir = Join-Path $script:tempDir 'instructions'
990 $script:instrSubDir = Join-Path $script:instrDir 'test-collection'
991 $script:ghDir = Join-Path $script:tempDir '.github'
992 New-Item -ItemType Directory -Path $script:instrSubDir -Force | Out-Null
993 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
994
995 @'
996---
997description: "Test instruction"
998applyTo: "**/*.ps1"
999---
1000'@ | Set-Content -Path (Join-Path $script:instrSubDir 'test.instructions.md')
1001 }
1002
1003 AfterAll {
1004 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1005 }
1006
1007 It 'Discovers instructions in directory' {
1008 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
1009 $result.DirectoryExists | Should -BeTrue
1010 $result.Instructions.Count | Should -BeGreaterThan 0
1011 }
1012
1013 It 'Returns empty when directory does not exist' {
1014 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-instr-dir-12345')
1015 $result = Get-DiscoveredInstructions -InstructionsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable')
1016 $result.DirectoryExists | Should -BeFalse
1017 }
1018
1019 It 'Skips root-level repo-specific instructions' {
1020 @'
1021---
1022description: "Repo-specific workflow instruction"
1023applyTo: "**/.github/workflows/*.yml"
1024---
1025'@ | Set-Content -Path (Join-Path $script:instrDir 'workflows.instructions.md')
1026
1027 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
1028 $instrNames = $result.Instructions | ForEach-Object { $_.name }
1029 $instrNames | Should -Not -Contain 'workflows-instructions'
1030 $result.Skipped | Where-Object { $_.Reason -match 'repo-specific' } | Should -Not -BeNullOrEmpty
1031 }
1032
1033 It 'Still discovers instructions in subdirectories' {
1034 $otherDir = Join-Path $script:instrDir 'csharp'
1035 New-Item -ItemType Directory -Path $otherDir -Force | Out-Null
1036 @'
1037---
1038description: "Repo-specific"
1039applyTo: "**/.github/workflows/*.yml"
1040---
1041'@ | Set-Content -Path (Join-Path $script:instrDir 'workflows.instructions.md')
1042 @'
1043---
1044description: "C# instruction"
1045applyTo: "**/*.cs"
1046---
1047'@ | Set-Content -Path (Join-Path $otherDir 'csharp.instructions.md')
1048
1049 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
1050 $instrNames = $result.Instructions | ForEach-Object { $_.name }
1051 $instrNames | Should -Contain 'csharp-instructions'
1052 $instrNames | Should -Not -Contain 'workflows-instructions'
1053 }
1054}
1055
1056Describe 'Get-DiscoveredSkills' {
1057 BeforeAll {
1058 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1059 $script:skillsDir = Join-Path $script:tempDir 'skills'
1060 New-Item -ItemType Directory -Path $script:skillsDir -Force | Out-Null
1061
1062 # Create test skill under a collection-id directory
1063 $skillDir = Join-Path $script:skillsDir 'test-collection/test-skill'
1064 New-Item -ItemType Directory -Path $skillDir -Force | Out-Null
1065 @'
1066---
1067name: test-skill
1068description: "Test skill"
1069---
1070# Skill
1071'@ | Set-Content -Path (Join-Path $skillDir 'SKILL.md')
1072
1073 # Create nested skill under same collection-id directory
1074 $nestedSkillDir = Join-Path $script:skillsDir 'test-collection/nested-skill'
1075 New-Item -ItemType Directory -Path $nestedSkillDir -Force | Out-Null
1076 @'
1077---
1078name: nested-skill
1079description: "Nested skill in collection"
1080---
1081# Nested Skill
1082'@ | Set-Content -Path (Join-Path $nestedSkillDir 'SKILL.md')
1083
1084 # Create root-level skill (repo-specific, should be skipped)
1085 $rootSkillDir = Join-Path $script:skillsDir 'root-skill'
1086 New-Item -ItemType Directory -Path $rootSkillDir -Force | Out-Null
1087 @'
1088---
1089name: root-skill
1090description: "Root-level skill"
1091---
1092# Root Skill
1093'@ | Set-Content -Path (Join-Path $rootSkillDir 'SKILL.md')
1094
1095 }
1096
1097 AfterAll {
1098 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1099 }
1100
1101 It 'Discovers skills in directory' {
1102 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
1103 $result.DirectoryExists | Should -BeTrue
1104 $result.Skills.Count | Should -Be 2
1105 $skillNames = $result.Skills | ForEach-Object { $_.name }
1106 $skillNames | Should -Contain 'test-skill'
1107 $skillNames | Should -Contain 'nested-skill'
1108 }
1109
1110 It 'Returns empty when directory does not exist' {
1111 $nonexistent = Join-Path $script:tempDir 'nonexistent-skills'
1112 $result = Get-DiscoveredSkills -SkillsDir $nonexistent -AllowedMaturities @('stable')
1113 $result.DirectoryExists | Should -BeFalse
1114 $result.Skills | Should -BeNullOrEmpty
1115 }
1116
1117 It 'Filters skills when stable is not an allowed maturity' {
1118 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('preview')
1119 $result.Skills.Count | Should -Be 0
1120 $result.Skipped.Count | Should -BeGreaterThan 0
1121 }
1122
1123 It 'Discovers nested skills with correct path' {
1124 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
1125 $nestedSkill = $result.Skills | Where-Object { $_.name -eq 'nested-skill' }
1126 $nestedSkill | Should -Not -BeNullOrEmpty
1127 $nestedSkill.path | Should -Be './.github/skills/test-collection/nested-skill/SKILL.md'
1128 }
1129
1130 It 'Skips root-level repo-specific skills with correct skip reason' {
1131 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
1132 $skillNames = $result.Skills | ForEach-Object { $_.name }
1133 $skillNames | Should -Not -Contain 'root-skill'
1134 $skipped = $result.Skipped | Where-Object { $_.Name -eq 'root-skill' }
1135 $skipped | Should -Not -BeNullOrEmpty
1136 $skipped.Reason | Should -Match 'repo-specific'
1137 }
1138}
1139
1140Describe 'Get-CollectionManifest' {
1141 BeforeAll {
1142 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1143 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
1144 }
1145
1146 AfterAll {
1147 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1148 }
1149
1150 It 'Loads collection manifest from valid YAML path' {
1151 $manifestFile = Join-Path $script:tempDir 'test.collection.yml'
1152 @"
1153id: test
1154name: test-ext
1155displayName: Test Extension
1156description: Test
1157items:
1158 - hve-core-all
1159"@ | Set-Content -Path $manifestFile
1160
1161 $result = Get-CollectionManifest -CollectionPath $manifestFile
1162 $result | Should -Not -BeNullOrEmpty
1163 $result.id | Should -Be 'test'
1164 }
1165
1166 It 'Loads collection manifest from valid JSON path' {
1167 $manifestFile = Join-Path $script:tempDir 'test.collection.json'
1168 @{
1169 '\$schema' = '../schemas/collection-manifest.schema.json'
1170 id = 'test'
1171 name = 'test-ext'
1172 displayName = 'Test Extension'
1173 description = 'Test'
1174 items = @('hve-core-all')
1175 } | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestFile
1176
1177 $result = Get-CollectionManifest -CollectionPath $manifestFile
1178 $result | Should -Not -BeNullOrEmpty
1179 $result.id | Should -Be 'test'
1180 }
1181
1182 It 'Throws when path does not exist' {
1183 $nonexistent = Join-Path $script:tempDir 'nonexistent.json'
1184 { Get-CollectionManifest -CollectionPath $nonexistent } | Should -Throw '*not found*'
1185 }
1186
1187 It 'Returns hashtable with expected keys' {
1188 $manifestFile = Join-Path $script:tempDir 'keys.collection.yml'
1189 @"
1190id: keys
1191name: keys-ext
1192displayName: Keys
1193description: Keys test
1194items:
1195 - developer
1196"@ | Set-Content -Path $manifestFile
1197
1198 $result = Get-CollectionManifest -CollectionPath $manifestFile
1199 $result.Keys | Should -Contain 'id'
1200 $result.Keys | Should -Contain 'name'
1201 $result.Keys | Should -Contain 'items'
1202 }
1203}
1204
1205Describe 'Test-GlobMatch' {
1206 It 'Returns true for matching wildcard pattern' {
1207 $result = Test-GlobMatch -Name 'rpi-agent' -Patterns @('rpi-*')
1208 $result | Should -BeTrue
1209 }
1210
1211 It 'Returns false for non-matching pattern' {
1212 $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*')
1213 $result | Should -BeFalse
1214 }
1215
1216 It 'Matches against multiple patterns' {
1217 $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*', 'mem*')
1218 $result | Should -BeTrue
1219 }
1220
1221 It 'Handles exact name match' {
1222 $result = Test-GlobMatch -Name 'memory' -Patterns @('memory')
1223 $result | Should -BeTrue
1224 }
1225}
1226
1227Describe 'Get-CollectionArtifacts' {
1228 It 'Returns artifacts from collection items across supported kinds' {
1229 $collection = @{
1230 items = @(
1231 @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' },
1232 @{ kind = 'prompt'; path = '.github/prompts/dev-prompt.prompt.md' },
1233 @{ kind = 'instruction'; path = '.github/instructions/dev/dev.instructions.md' },
1234 @{ kind = 'skill'; path = '.github/skills/video-to-gif/' }
1235 )
1236 }
1237
1238 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable', 'preview')
1239 $result.Agents | Should -Contain 'dev-agent'
1240 $result.Prompts | Should -Contain 'dev-prompt'
1241 $result.Instructions | Should -Contain 'dev/dev'
1242 $result.Skills | Should -Contain 'video-to-gif'
1243 }
1244
1245 It 'Uses item maturity when provided' {
1246 $collection = @{
1247 items = @(
1248 @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md'; maturity = 'stable' },
1249 @{ kind = 'agent'; path = '.github/agents/preview-dev.agent.md'; maturity = 'preview' }
1250 )
1251 }
1252
1253 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable')
1254 $result.Agents | Should -Contain 'dev-agent'
1255 $result.Agents | Should -Not -Contain 'preview-dev'
1256 }
1257
1258 It 'Defaults to stable maturity when item maturity is omitted' {
1259 $collection = @{
1260 items = @(
1261 @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' },
1262 @{ kind = 'agent'; path = '.github/agents/preview-dev.agent.md' }
1263 )
1264 }
1265
1266 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable')
1267 $result.Agents | Should -Contain 'dev-agent'
1268 $result.Agents | Should -Contain 'preview-dev'
1269 }
1270
1271 It 'Returns empty when collection has no items' {
1272 $collection = @{ id = 'empty' }
1273 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable')
1274 $result.Agents.Count | Should -Be 0
1275 $result.Prompts.Count | Should -Be 0
1276 $result.Instructions.Count | Should -Be 0
1277 $result.Skills.Count | Should -Be 0
1278 }
1279}
1280
1281Describe 'Resolve-HandoffDependencies' {
1282 BeforeAll {
1283 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1284 $script:agentsDir = Join-Path $script:tempDir 'agents'
1285 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
1286
1287 # Agent with no handoffs
1288 @'
1289---
1290description: "Solo agent"
1291---
1292'@ | Set-Content -Path (Join-Path $script:agentsDir 'solo.agent.md')
1293
1294 # Agent with single handoff (object format matching real agents)
1295 @'
1296---
1297description: "Parent agent"
1298handoffs:
1299 - label: "Go to child"
1300 agent: child
1301 prompt: Continue
1302---
1303'@ | Set-Content -Path (Join-Path $script:agentsDir 'parent.agent.md')
1304
1305 @'
1306---
1307description: "Child agent"
1308---
1309'@ | Set-Content -Path (Join-Path $script:agentsDir 'child.agent.md')
1310
1311 # Self-referential agent (object format)
1312 @'
1313---
1314description: "Self agent"
1315handoffs:
1316 - label: "Self"
1317 agent: self-ref
1318---
1319'@ | Set-Content -Path (Join-Path $script:agentsDir 'self-ref.agent.md')
1320
1321 # Circular chain (object format)
1322 @'
1323---
1324description: "Chain A"
1325handoffs:
1326 - label: "To B"
1327 agent: chain-b
1328---
1329'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-a.agent.md')
1330
1331 @'
1332---
1333description: "Chain B"
1334handoffs:
1335 - label: "To A"
1336 agent: chain-a
1337---
1338'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-b.agent.md')
1339
1340 }
1341
1342 AfterAll {
1343 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1344 }
1345
1346 It 'Returns seed agents when no handoffs' {
1347 $result = Resolve-HandoffDependencies -SeedAgents @('solo') -AgentsDir $script:agentsDir
1348 $result | Should -Contain 'solo'
1349 $result.Count | Should -Be 1
1350 }
1351
1352 It 'Resolves single-level handoff' {
1353 $result = Resolve-HandoffDependencies -SeedAgents @('parent') -AgentsDir $script:agentsDir
1354 $result | Should -Contain 'parent'
1355 $result | Should -Contain 'child'
1356 }
1357
1358 It 'Handles self-referential handoffs' {
1359 $result = Resolve-HandoffDependencies -SeedAgents @('self-ref') -AgentsDir $script:agentsDir
1360 $result | Should -Contain 'self-ref'
1361 $result.Count | Should -Be 1
1362 }
1363
1364 It 'Handles circular handoff chains' {
1365 $result = Resolve-HandoffDependencies -SeedAgents @('chain-a') -AgentsDir $script:agentsDir
1366 $result | Should -Contain 'chain-a'
1367 $result | Should -Contain 'chain-b'
1368 $result.Count | Should -Be 2
1369 }
1370}
1371
1372Describe 'Resolve-RequiresDependencies' {
1373 It 'Resolves agent requires to include dependent prompts' {
1374 $result = Resolve-RequiresDependencies `
1375 -ArtifactNames @{ agents = @('main') } `
1376 -AllowedMaturities @('stable') `
1377 -CollectionRequires @{ agents = @{ 'main' = @{ prompts = @('dep-prompt') } } } `
1378 -CollectionMaturities @{ prompts = @{ 'dep-prompt' = 'stable' } }
1379 $result.Prompts | Should -Contain 'dep-prompt'
1380 }
1381
1382 It 'Resolves transitive agent dependencies' {
1383 $result = Resolve-RequiresDependencies `
1384 -ArtifactNames @{ agents = @('top') } `
1385 -AllowedMaturities @('stable') `
1386 -CollectionRequires @{ agents = @{ 'top' = @{ agents = @('mid') }; 'mid' = @{ prompts = @('leaf-prompt') } } } `
1387 -CollectionMaturities @{ agents = @{ 'mid' = 'stable' }; prompts = @{ 'leaf-prompt' = 'stable' } }
1388 $result.Agents | Should -Contain 'mid'
1389 $result.Prompts | Should -Contain 'leaf-prompt'
1390 }
1391
1392 It 'Respects maturity filter on dependencies' {
1393 $result = Resolve-RequiresDependencies `
1394 -ArtifactNames @{ agents = @('main') } `
1395 -AllowedMaturities @('stable') `
1396 -CollectionRequires @{ agents = @{ 'main' = @{ prompts = @('exp-prompt') } } } `
1397 -CollectionMaturities @{ prompts = @{ 'exp-prompt' = 'experimental' } }
1398 $result.Prompts | Should -Not -Contain 'exp-prompt'
1399 }
1400}
1401
1402Describe 'Update-PackageJsonContributes' {
1403 It 'Updates contributes section with chat participants' {
1404 $packageJson = [PSCustomObject]@{
1405 name = 'test-extension'
1406 contributes = [PSCustomObject]@{}
1407 }
1408 $agents = @(
1409 @{ name = 'agent1'; description = 'Desc 1' }
1410 )
1411 $prompts = @(
1412 @{ name = 'prompt1'; description = 'Prompt desc' }
1413 )
1414 $instructions = @(
1415 @{ name = 'instr1'; description = 'Instr desc' }
1416 )
1417
1418 $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents $agents -ChatPromptFiles $prompts -ChatInstructions $instructions -ChatSkills @()
1419 $result.contributes | Should -Not -BeNullOrEmpty
1420 }
1421
1422 It 'Handles empty arrays' {
1423 $packageJson = [PSCustomObject]@{
1424 name = 'test-extension'
1425 contributes = [PSCustomObject]@{}
1426 }
1427
1428 $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents @() -ChatPromptFiles @() -ChatInstructions @() -ChatSkills @()
1429 $result | Should -Not -BeNullOrEmpty
1430 }
1431}
1432
1433Describe 'New-PrepareResult' {
1434 It 'Creates success result with counts' {
1435 $result = New-PrepareResult -Success $true -AgentCount 5 -PromptCount 10 -InstructionCount 15 -SkillCount 3 -Version '1.0.0'
1436 $result.Success | Should -BeTrue
1437 $result.AgentCount | Should -Be 5
1438 $result.PromptCount | Should -Be 10
1439 $result.InstructionCount | Should -Be 15
1440 $result.SkillCount | Should -Be 3
1441 $result.Version | Should -Be '1.0.0'
1442 $result.ErrorMessage | Should -BeNullOrEmpty
1443 }
1444
1445 It 'Creates failure result with error message' {
1446 $result = New-PrepareResult -Success $false -ErrorMessage 'Something went wrong'
1447 $result.Success | Should -BeFalse
1448 $result.ErrorMessage | Should -Be 'Something went wrong'
1449 $result.AgentCount | Should -Be 0
1450 $result.PromptCount | Should -Be 0
1451 $result.InstructionCount | Should -Be 0
1452 }
1453
1454 It 'Returns hashtable with all expected keys' {
1455 $result = New-PrepareResult -Success $true
1456 $result.Keys | Should -Contain 'Success'
1457 $result.Keys | Should -Contain 'AgentCount'
1458 $result.Keys | Should -Contain 'PromptCount'
1459 $result.Keys | Should -Contain 'InstructionCount'
1460 $result.Keys | Should -Contain 'SkillCount'
1461 $result.Keys | Should -Contain 'Version'
1462 $result.Keys | Should -Contain 'ErrorMessage'
1463 }
1464}
1465
1466Describe 'Invoke-PrepareExtension' {
1467 BeforeAll {
1468 $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString())
1469 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
1470
1471 # Create extension directory with package.json
1472 $script:extDir = Join-Path $script:tempDir 'extension'
1473 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
1474 @'
1475{
1476 "name": "test-extension",
1477 "version": "1.2.3",
1478 "contributes": {}
1479}
1480'@ | Set-Content -Path (Join-Path $script:extDir 'package.json')
1481
1482 # Create package template for generation
1483 $script:templatesDir = Join-Path $script:extDir 'templates'
1484 New-Item -ItemType Directory -Path $script:templatesDir -Force | Out-Null
1485 @'
1486{
1487 "name": "hve-core",
1488 "displayName": "HVE Core",
1489 "version": "1.2.3",
1490 "description": "Test extension",
1491 "publisher": "test-pub",
1492 "engines": { "vscode": "^1.80.0" },
1493 "contributes": {}
1494}
1495'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json')
1496
1497 # Create collections directory with a minimal hve-core collection (flagship)
1498 $script:collectionsDir = Join-Path $script:tempDir 'collections'
1499 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
1500 @"
1501id: hve-core
1502name: HVE Core
1503displayName: HVE Core
1504description: Test extension
1505"@ | Set-Content -Path (Join-Path $script:collectionsDir 'hve-core.collection.yml')
1506
1507 # Create .github structure with subdirectories (root-level files are repo-specific)
1508 $script:ghDir = Join-Path $script:tempDir '.github'
1509 $script:agentsDir = Join-Path $script:ghDir 'agents'
1510 $script:agentsSubDir = Join-Path $script:agentsDir 'test-collection'
1511 $script:promptsDir = Join-Path $script:ghDir 'prompts'
1512 $script:promptsSubDir = Join-Path $script:promptsDir 'test-collection'
1513 $script:instrDir = Join-Path $script:ghDir 'instructions'
1514 $script:instrSubDir = Join-Path $script:instrDir 'test-collection'
1515 New-Item -ItemType Directory -Path $script:agentsSubDir -Force | Out-Null
1516 New-Item -ItemType Directory -Path $script:promptsSubDir -Force | Out-Null
1517 New-Item -ItemType Directory -Path $script:instrSubDir -Force | Out-Null
1518
1519 # Create test agent in subdirectory
1520 @'
1521---
1522description: "Test agent"
1523---
1524# Agent
1525'@ | Set-Content -Path (Join-Path $script:agentsSubDir 'test.agent.md')
1526
1527 # Create test prompt in subdirectory
1528 @'
1529---
1530description: "Test prompt"
1531---
1532# Prompt
1533'@ | Set-Content -Path (Join-Path $script:promptsSubDir 'test.prompt.md')
1534
1535 # Create test instruction in subdirectory
1536 @'
1537---
1538description: "Test instruction"
1539applyTo: "**/*.ps1"
1540---
1541# Instruction
1542'@ | Set-Content -Path (Join-Path $script:instrSubDir 'test.instructions.md')
1543
1544 }
1545
1546 AfterAll {
1547 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1548 }
1549
1550 It 'Returns success result with correct counts' {
1551 $result = Invoke-PrepareExtension `
1552 -ExtensionDirectory $script:extDir `
1553 -RepoRoot $script:tempDir `
1554 -Channel 'Stable' `
1555 -DryRun
1556
1557 $result.Success | Should -BeTrue
1558 $result.AgentCount | Should -Be 1
1559 $result.PromptCount | Should -Be 1
1560 $result.InstructionCount | Should -Be 1
1561 $result.Version | Should -Be '1.2.3'
1562 }
1563
1564 It 'Fails when extension directory missing' {
1565 $nonexistentPath = Join-Path $TestDrive 'nonexistent-ext-dir-12345'
1566 $result = Invoke-PrepareExtension `
1567 -ExtensionDirectory $nonexistentPath `
1568 -RepoRoot $script:tempDir `
1569 -Channel 'Stable'
1570
1571 $result.Success | Should -BeFalse
1572 $result.ErrorMessage | Should -Not -BeNullOrEmpty
1573 }
1574
1575 It 'Respects channel filtering' {
1576 # Add preview agent in subdirectory
1577 @'
1578---
1579description: "Preview agent"
1580---
1581'@ | Set-Content -Path (Join-Path $script:agentsSubDir 'preview.agent.md')
1582
1583 $collectionPath = Join-Path $script:tempDir 'channel-filter.collection.yml'
1584 @"
1585id: hve-core
1586name: HVE Core
1587displayName: HVE Core
1588description: Channel filtering test
1589items:
1590 - kind: agent
1591 path: .github/agents/test-collection/test.agent.md
1592 maturity: stable
1593 - kind: agent
1594 path: .github/agents/test-collection/preview.agent.md
1595 maturity: preview
1596"@ | Set-Content -Path $collectionPath
1597
1598 $stableResult = Invoke-PrepareExtension `
1599 -ExtensionDirectory $script:extDir `
1600 -RepoRoot $script:tempDir `
1601 -Channel 'Stable' `
1602 -Collection $collectionPath `
1603 -DryRun
1604
1605 $preReleaseResult = Invoke-PrepareExtension `
1606 -ExtensionDirectory $script:extDir `
1607 -RepoRoot $script:tempDir `
1608 -Channel 'PreRelease' `
1609 -Collection $collectionPath `
1610 -DryRun
1611
1612 $preReleaseResult.AgentCount | Should -BeGreaterThan $stableResult.AgentCount
1613 }
1614
1615 It 'Filters prompts and instructions by maturity' {
1616 # Add experimental prompt in subdirectory
1617 @'
1618---
1619description: "Experimental prompt"
1620---
1621'@ | Set-Content -Path (Join-Path $script:promptsSubDir 'experimental.prompt.md')
1622
1623 # Add preview instruction in subdirectory
1624 @'
1625---
1626description: "Preview instruction"
1627applyTo: "**/*.js"
1628---
1629'@ | Set-Content -Path (Join-Path $script:instrSubDir 'preview.instructions.md')
1630
1631 $collectionPath = Join-Path $script:tempDir 'prompt-instruction-filter.collection.yml'
1632 @"
1633id: hve-core
1634name: HVE Core
1635displayName: HVE Core
1636description: Prompt/instruction filtering test
1637items:
1638 - kind: agent
1639 path: .github/agents/test-collection/test.agent.md
1640 maturity: stable
1641 - kind: prompt
1642 path: .github/prompts/test-collection/test.prompt.md
1643 maturity: stable
1644 - kind: prompt
1645 path: .github/prompts/test-collection/experimental.prompt.md
1646 maturity: experimental
1647 - kind: instruction
1648 path: .github/instructions/test-collection/test.instructions.md
1649 maturity: stable
1650 - kind: instruction
1651 path: .github/instructions/test-collection/preview.instructions.md
1652 maturity: preview
1653"@ | Set-Content -Path $collectionPath
1654
1655 $stableResult = Invoke-PrepareExtension `
1656 -ExtensionDirectory $script:extDir `
1657 -RepoRoot $script:tempDir `
1658 -Channel 'Stable' `
1659 -Collection $collectionPath `
1660 -DryRun
1661
1662 $preReleaseResult = Invoke-PrepareExtension `
1663 -ExtensionDirectory $script:extDir `
1664 -RepoRoot $script:tempDir `
1665 -Channel 'PreRelease' `
1666 -Collection $collectionPath `
1667 -DryRun
1668
1669 $preReleaseResult.PromptCount | Should -BeGreaterThan $stableResult.PromptCount
1670 $preReleaseResult.InstructionCount | Should -BeGreaterThan $stableResult.InstructionCount
1671 }
1672
1673 It 'Updates package.json when not DryRun' {
1674 $result = Invoke-PrepareExtension `
1675 -ExtensionDirectory $script:extDir `
1676 -RepoRoot $script:tempDir `
1677 -Channel 'Stable' `
1678 -DryRun:$false
1679
1680 $result.Success | Should -BeTrue
1681
1682 $pkgJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1683 $pkgJson.contributes.chatAgents | Should -Not -BeNullOrEmpty
1684 }
1685
1686 It 'Copies changelog when path provided' {
1687 $changelogPath = Join-Path $script:tempDir 'CHANGELOG.md'
1688 '# Changelog' | Set-Content -Path $changelogPath
1689
1690 $result = Invoke-PrepareExtension `
1691 -ExtensionDirectory $script:extDir `
1692 -RepoRoot $script:tempDir `
1693 -Channel 'Stable' `
1694 -ChangelogPath $changelogPath `
1695 -DryRun:$false
1696
1697 $result.Success | Should -BeTrue
1698 Test-Path (Join-Path $script:extDir 'CHANGELOG.md') | Should -BeTrue
1699 }
1700
1701 It 'Fails when package template is missing' {
1702 $badRoot = Join-Path $TestDrive 'bad-template-root'
1703 $badExtDir = Join-Path $badRoot 'extension'
1704 New-Item -ItemType Directory -Path $badExtDir -Force | Out-Null
1705 New-Item -ItemType Directory -Path (Join-Path $badRoot 'collections') -Force | Out-Null
1706 New-Item -ItemType Directory -Path (Join-Path $badRoot '.github/agents') -Force | Out-Null
1707 @"
1708id: test
1709"@ | Set-Content -Path (Join-Path $badRoot 'collections/test.collection.yml')
1710
1711 $result = Invoke-PrepareExtension `
1712 -ExtensionDirectory $badExtDir `
1713 -RepoRoot $badRoot `
1714 -Channel 'Stable'
1715
1716 $result.Success | Should -BeFalse
1717 $result.ErrorMessage | Should -Match 'Package generation failed'
1718 }
1719
1720 It 'Fails when no collection YAML files exist' {
1721 $emptyRoot = Join-Path $TestDrive 'empty-collections-root'
1722 $emptyExtDir = Join-Path $emptyRoot 'extension'
1723 New-Item -ItemType Directory -Path $emptyExtDir -Force | Out-Null
1724 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'collections') -Force | Out-Null
1725 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'extension/templates') -Force | Out-Null
1726 New-Item -ItemType Directory -Path (Join-Path $emptyRoot '.github/agents') -Force | Out-Null
1727 @{ name = 'test'; version = '1.0.0' } | ConvertTo-Json | Set-Content -Path (Join-Path $emptyRoot 'extension/templates/package.template.json')
1728
1729 $result = Invoke-PrepareExtension `
1730 -ExtensionDirectory $emptyExtDir `
1731 -RepoRoot $emptyRoot `
1732 -Channel 'Stable'
1733
1734 $result.Success | Should -BeFalse
1735 $result.ErrorMessage | Should -Match 'Package generation failed'
1736 }
1737
1738 Context 'Collection template copy' {
1739 BeforeAll {
1740 # Developer collection manifest (in collections/ for generation)
1741 $script:devCollectionYaml = Join-Path $script:collectionsDir 'developer.collection.yml'
1742 @"
1743id: developer
1744name: hve-developer
1745displayName: HVE Core - Developer Edition
1746description: Developer edition
1747"@ | Set-Content -Path $script:devCollectionYaml
1748 $script:devCollectionPath = $script:devCollectionYaml
1749
1750 # hve-core collection manifest (flagship, skips template copy)
1751 $script:coreCollectionPath = Join-Path $script:tempDir 'hve-core.collection.yml'
1752 @"
1753id: hve-core
1754name: HVE Core
1755displayName: HVE Core
1756description: Flagship collection
1757"@ | Set-Content -Path $script:coreCollectionPath
1758
1759 # Collection manifest referencing a missing template
1760 $script:missingCollectionPath = Join-Path $script:tempDir 'nonexistent.collection.yml'
1761 @"
1762id: nonexistent
1763name: nonexistent
1764displayName: Nonexistent
1765description: Missing template
1766"@ | Set-Content -Path $script:missingCollectionPath
1767
1768 }
1769
1770 AfterEach {
1771 # Clean up backup files left by collection template copy
1772 $bakPath = Join-Path $script:extDir 'package.json.bak'
1773 if (Test-Path $bakPath) {
1774 Remove-Item -Path $bakPath -Force
1775 }
1776 }
1777
1778 It 'Skips template copy when no collection specified' {
1779 $result = Invoke-PrepareExtension `
1780 -ExtensionDirectory $script:extDir `
1781 -RepoRoot $script:tempDir `
1782 -Channel 'Stable' `
1783 -DryRun
1784
1785 $result.Success | Should -BeTrue
1786 # package.json should contain the generated hve-core content (not a collection template)
1787 $currentJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1788 $currentJson.name | Should -Be 'hve-core'
1789 Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse
1790 }
1791
1792 It 'Skips template copy for hve-core collection' {
1793 $result = Invoke-PrepareExtension `
1794 -ExtensionDirectory $script:extDir `
1795 -RepoRoot $script:tempDir `
1796 -Channel 'Stable' `
1797 -Collection $script:coreCollectionPath `
1798 -DryRun
1799
1800 $result.Success | Should -BeTrue
1801 Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse
1802 }
1803
1804 It 'Returns error when collection template file missing' {
1805 $result = Invoke-PrepareExtension `
1806 -ExtensionDirectory $script:extDir `
1807 -RepoRoot $script:tempDir `
1808 -Channel 'Stable' `
1809 -Collection $script:missingCollectionPath `
1810 -DryRun
1811
1812 $result.Success | Should -BeFalse
1813 $result.ErrorMessage | Should -Match 'Collection template not found'
1814 }
1815
1816 It 'Copies template to package.json for non-default collection' {
1817 $result = Invoke-PrepareExtension `
1818 -ExtensionDirectory $script:extDir `
1819 -RepoRoot $script:tempDir `
1820 -Channel 'Stable' `
1821 -Collection $script:devCollectionPath `
1822 -DryRun
1823
1824 $result.Success | Should -BeTrue
1825 $updatedJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1826 $updatedJson.name | Should -Be 'hve-developer'
1827 }
1828
1829 It 'Creates package.json.bak backup before template copy' {
1830 $result = Invoke-PrepareExtension `
1831 -ExtensionDirectory $script:extDir `
1832 -RepoRoot $script:tempDir `
1833 -Channel 'Stable' `
1834 -Collection $script:devCollectionPath `
1835 -DryRun
1836
1837 $result.Success | Should -BeTrue
1838 $bakPath = Join-Path $script:extDir 'package.json.bak'
1839 Test-Path $bakPath | Should -BeTrue
1840 # Backup should contain the hve-core (flagship) generated content
1841 $bakJson = Get-Content -Path $bakPath -Raw | ConvertFrom-Json
1842 $bakJson.name | Should -Be 'hve-core'
1843 }
1844 }
1845
1846 Context 'Collection maturity gating' {
1847 BeforeAll {
1848 # Deprecated collection in collections/ directory for generation
1849 $script:deprecatedCollectionPath = Join-Path $script:collectionsDir 'deprecated-coll.collection.yml'
1850 @"
1851id: deprecated-coll
1852name: deprecated-ext
1853displayName: Deprecated Collection
1854description: Deprecated collection for testing
1855maturity: deprecated
1856"@ | Set-Content -Path $script:deprecatedCollectionPath
1857
1858 # Experimental collection in collections/ directory for generation
1859 $script:experimentalCollectionPath = Join-Path $script:collectionsDir 'experimental-coll.collection.yml'
1860 @"
1861id: experimental-coll
1862name: experimental-ext
1863displayName: Experimental Collection
1864description: Experimental collection for testing
1865maturity: experimental
1866"@ | Set-Content -Path $script:experimentalCollectionPath
1867 }
1868
1869 It 'Returns early success for deprecated collection on Stable channel' {
1870 $result = Invoke-PrepareExtension `
1871 -ExtensionDirectory $script:extDir `
1872 -RepoRoot $script:tempDir `
1873 -Channel 'Stable' `
1874 -Collection $script:deprecatedCollectionPath `
1875 -DryRun
1876
1877 $result.Success | Should -BeTrue
1878 $result.AgentCount | Should -Be 0
1879 }
1880
1881 It 'Returns early success for deprecated collection on PreRelease channel' {
1882 $result = Invoke-PrepareExtension `
1883 -ExtensionDirectory $script:extDir `
1884 -RepoRoot $script:tempDir `
1885 -Channel 'PreRelease' `
1886 -Collection $script:deprecatedCollectionPath `
1887 -DryRun
1888
1889 $result.Success | Should -BeTrue
1890 $result.AgentCount | Should -Be 0
1891 }
1892
1893 It 'Returns early success for experimental collection on Stable channel' {
1894 $result = Invoke-PrepareExtension `
1895 -ExtensionDirectory $script:extDir `
1896 -RepoRoot $script:tempDir `
1897 -Channel 'Stable' `
1898 -Collection $script:experimentalCollectionPath `
1899 -DryRun
1900
1901 $result.Success | Should -BeTrue
1902 $result.AgentCount | Should -Be 0
1903 }
1904
1905 It 'Processes experimental collection on PreRelease channel' {
1906 $result = Invoke-PrepareExtension `
1907 -ExtensionDirectory $script:extDir `
1908 -RepoRoot $script:tempDir `
1909 -Channel 'PreRelease' `
1910 -Collection $script:experimentalCollectionPath `
1911 -DryRun
1912
1913 $result.Success | Should -BeTrue
1914 $result.ErrorMessage | Should -Be ''
1915 }
1916 }
1917
1918 Context 'Exclusion reporting and skill filtering' {
1919 BeforeAll {
1920 # Add root-level repo-specific files to trigger exclusion messages
1921 @'
1922---
1923description: "Root-level agent"
1924---
1925'@ | Set-Content -Path (Join-Path $script:agentsDir 'root-agent.agent.md')
1926
1927 @'
1928---
1929description: "Root-level prompt"
1930---
1931'@ | Set-Content -Path (Join-Path $script:promptsDir 'root-prompt.prompt.md')
1932
1933 @'
1934---
1935description: "Root-level instruction"
1936applyTo: "**/*.ps1"
1937---
1938'@ | Set-Content -Path (Join-Path $script:instrDir 'root-instr.instructions.md')
1939
1940 # Add skills directory with skill in subdirectory
1941 $script:skillsDir = Join-Path $script:ghDir 'skills'
1942 $script:skillSubDir = Join-Path $script:skillsDir 'test-collection/test-skill'
1943 New-Item -ItemType Directory -Path $script:skillSubDir -Force | Out-Null
1944 @'
1945---
1946name: test-skill
1947description: "Test skill"
1948---
1949# Skill
1950'@ | Set-Content -Path (Join-Path $script:skillSubDir 'SKILL.md')
1951
1952 # Add root-level skill
1953 $rootSkillDir = Join-Path $script:skillsDir 'root-skill'
1954 New-Item -ItemType Directory -Path $rootSkillDir -Force | Out-Null
1955 @'
1956---
1957name: root-skill
1958description: "Root-level skill"
1959---
1960# Root Skill
1961'@ | Set-Content -Path (Join-Path $rootSkillDir 'SKILL.md')
1962
1963 # Restore valid package.json and template
1964 @'
1965{
1966 "name": "hve-core",
1967 "displayName": "HVE Core",
1968 "version": "1.2.3",
1969 "description": "Test extension",
1970 "publisher": "test-pub",
1971 "engines": { "vscode": "^1.80.0" },
1972 "contributes": {}
1973}
1974'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json')
1975 }
1976
1977 It 'Reports skipped items when root-level repo-specific files exist' {
1978 $result = Invoke-PrepareExtension `
1979 -ExtensionDirectory $script:extDir `
1980 -RepoRoot $script:tempDir `
1981 -Channel 'Stable' `
1982 -DryRun
1983
1984 $result.Success | Should -BeTrue
1985 $result.AgentCount | Should -BeGreaterOrEqual 1
1986 $result.SkillCount | Should -BeGreaterOrEqual 1
1987 }
1988
1989 It 'Filters skills by collection membership' {
1990 $collectionPath = Join-Path $script:tempDir 'skill-filter.collection.yml'
1991 @"
1992id: hve-core
1993name: HVE Core
1994displayName: HVE Core
1995description: Skill filtering test
1996items:
1997 - kind: agent
1998 path: .github/agents/test-collection/test.agent.md
1999 maturity: stable
2000 - kind: skill
2001 path: .github/skills/test-collection/test-skill/
2002 maturity: stable
2003"@ | Set-Content -Path $collectionPath
2004
2005 $result = Invoke-PrepareExtension `
2006 -ExtensionDirectory $script:extDir `
2007 -RepoRoot $script:tempDir `
2008 -Channel 'Stable' `
2009 -Collection $collectionPath `
2010 -DryRun
2011
2012 $result.Success | Should -BeTrue
2013 $result.SkillCount | Should -Be 1
2014 }
2015
2016 It 'Shows DryRun message when changelog provided with DryRun' {
2017 $changelogPath = Join-Path $script:tempDir 'CHANGELOG-DRYRUN.md'
2018 '# DryRun Changelog' | Set-Content -Path $changelogPath
2019
2020 $result = Invoke-PrepareExtension `
2021 -ExtensionDirectory $script:extDir `
2022 -RepoRoot $script:tempDir `
2023 -Channel 'Stable' `
2024 -ChangelogPath $changelogPath `
2025 -DryRun
2026
2027 $result.Success | Should -BeTrue
2028 }
2029 }
2030}
2031
2032#region Additional Coverage Tests
2033
2034Describe 'Get-ArtifactDescription' {
2035 BeforeAll {
2036 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2037 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
2038 }
2039
2040 AfterAll {
2041 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2042 }
2043
2044 It 'Returns empty string when file does not exist' {
2045 $result = Get-ArtifactDescription -FilePath (Join-Path $script:tempDir 'nonexistent.md')
2046 $result | Should -Be ''
2047 }
2048
2049 It 'Returns empty string when file has no frontmatter' {
2050 $path = Join-Path $script:tempDir 'no-frontmatter.md'
2051 '# Just a heading' | Set-Content -Path $path
2052 $result = Get-ArtifactDescription -FilePath $path
2053 $result | Should -Be ''
2054 }
2055
2056 It 'Returns empty string when frontmatter has no description' {
2057 $path = Join-Path $script:tempDir 'no-desc.md'
2058 @"
2059---
2060applyTo: "**/*.ps1"
2061---
2062# No description
2063"@ | Set-Content -Path $path
2064 $result = Get-ArtifactDescription -FilePath $path
2065 $result | Should -Be ''
2066 }
2067
2068 It 'Returns description from valid frontmatter' {
2069 $path = Join-Path $script:tempDir 'valid.md'
2070 @"
2071---
2072description: "My artifact description"
2073---
2074# Valid
2075"@ | Set-Content -Path $path
2076 $result = Get-ArtifactDescription -FilePath $path
2077 $result | Should -Be 'My artifact description'
2078 }
2079
2080 It 'Strips branding suffix from description' {
2081 $path = Join-Path $script:tempDir 'branded.md'
2082 @"
2083---
2084description: "Some tool - Brought to you by microsoft/hve-core"
2085---
2086# Branded
2087"@ | Set-Content -Path $path
2088 $result = Get-ArtifactDescription -FilePath $path
2089 $result | Should -Be 'Some tool'
2090 }
2091
2092 It 'Returns empty string when frontmatter YAML is invalid' {
2093 $path = Join-Path $script:tempDir 'bad-yaml.md'
2094 @"
2095---
2096description: [invalid: yaml: :
2097---
2098# Bad
2099"@ | Set-Content -Path $path
2100 $result = Get-ArtifactDescription -FilePath $path
2101 $result | Should -Be ''
2102 }
2103}
2104
2105Describe 'Get-CollectionArtifactKey - default branch' {
2106 It 'Handles unknown kind with matching suffix' {
2107 $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/my-file.custom.md'
2108 $result | Should -Be 'my-file'
2109 }
2110
2111 It 'Handles unknown kind with .md extension but no matching suffix' {
2112 $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/readme.md'
2113 $result | Should -Be 'readme'
2114 }
2115
2116 It 'Handles unknown kind with non-md file' {
2117 $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/config.json'
2118 $result | Should -Be 'config.json'
2119 }
2120}
2121
2122Describe 'Test-TemplateConsistency' {
2123 BeforeAll {
2124 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2125 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
2126 }
2127
2128 AfterAll {
2129 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2130 }
2131
2132 It 'Returns inconsistent when template file not found' {
2133 $manifest = @{ name = 'test'; displayName = 'Test'; description = 'Desc' }
2134 $result = Test-TemplateConsistency -TemplatePath (Join-Path $script:tempDir 'nonexistent.json') -CollectionManifest $manifest
2135 $result.IsConsistent | Should -BeFalse
2136 $result.Mismatches.Count | Should -Be 1
2137 $result.Mismatches[0].Field | Should -Be 'file'
2138 $result.Mismatches[0].Message | Should -Match 'not found'
2139 }
2140
2141 It 'Returns inconsistent when template is invalid JSON' {
2142 $badPath = Join-Path $script:tempDir 'bad-template.json'
2143 'not valid json {{{' | Set-Content -Path $badPath
2144 $manifest = @{ name = 'test' }
2145 $result = Test-TemplateConsistency -TemplatePath $badPath -CollectionManifest $manifest
2146 $result.IsConsistent | Should -BeFalse
2147 $result.Mismatches[0].Message | Should -Match 'Failed to parse'
2148 }
2149
2150 It 'Returns consistent when fields match' {
2151 $path = Join-Path $script:tempDir 'matching.json'
2152 @{ name = 'hve-rpi'; displayName = 'HVE RPI'; description = 'RPI tools' } | ConvertTo-Json | Set-Content -Path $path
2153 $manifest = @{ name = 'hve-rpi'; displayName = 'HVE RPI'; description = 'RPI tools' }
2154 $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest
2155 $result.IsConsistent | Should -BeTrue
2156 $result.Mismatches.Count | Should -Be 0
2157 }
2158
2159 It 'Reports mismatches for diverging fields' {
2160 $path = Join-Path $script:tempDir 'diverging.json'
2161 @{ name = 'old-name'; displayName = 'Old Name'; description = 'Old desc' } | ConvertTo-Json | Set-Content -Path $path
2162 $manifest = @{ name = 'new-name'; displayName = 'New Name'; description = 'New desc' }
2163 $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest
2164 $result.IsConsistent | Should -BeFalse
2165 $result.Mismatches.Count | Should -Be 3
2166 }
2167
2168 It 'Skips comparison when field missing in either side' {
2169 $path = Join-Path $script:tempDir 'partial.json'
2170 @{ name = 'test' } | ConvertTo-Json | Set-Content -Path $path
2171 $manifest = @{ displayName = 'Test Display' }
2172 $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest
2173 $result.IsConsistent | Should -BeTrue
2174 }
2175}
2176
2177Describe 'Update-PackageJsonContributes - existing contributes fields' {
2178 It 'Updates existing chatAgents field via else branch' {
2179 $packageJson = [PSCustomObject]@{
2180 name = 'test-extension'
2181 contributes = [PSCustomObject]@{
2182 chatAgents = @(@{ path = './old.agent.md' })
2183 chatPromptFiles = @(@{ path = './old.prompt.md' })
2184 chatInstructions = @(@{ path = './old.instr.md' })
2185 chatSkills = @(@{ path = './old.skill' })
2186 }
2187 }
2188 $agents = @(@{ name = 'new-agent'; path = './.github/agents/new.agent.md' })
2189 $prompts = @(@{ name = 'new-prompt'; path = './.github/prompts/new.prompt.md' })
2190 $instructions = @(@{ name = 'new-instr'; path = './.github/instructions/new.instructions.md' })
2191 $skills = @(@{ name = 'new-skill'; path = './.github/skills/new-skill' })
2192
2193 $result = Update-PackageJsonContributes -PackageJson $packageJson `
2194 -ChatAgents $agents `
2195 -ChatPromptFiles $prompts `
2196 -ChatInstructions $instructions `
2197 -ChatSkills $skills
2198
2199 $result.contributes.chatAgents[0].path | Should -Be './.github/agents/new.agent.md'
2200 $result.contributes.chatPromptFiles[0].path | Should -Be './.github/prompts/new.prompt.md'
2201 $result.contributes.chatInstructions[0].path | Should -Be './.github/instructions/new.instructions.md'
2202 $result.contributes.chatSkills[0].path | Should -Be './.github/skills/new-skill'
2203 }
2204
2205 It 'Adds contributes section when missing' {
2206 $packageJson = [PSCustomObject]@{
2207 name = 'bare-extension'
2208 }
2209
2210 $result = Update-PackageJsonContributes -PackageJson $packageJson `
2211 -ChatAgents @() `
2212 -ChatPromptFiles @() `
2213 -ChatInstructions @() `
2214 -ChatSkills @()
2215
2216 $result.contributes | Should -Not -BeNullOrEmpty
2217 }
2218}
2219
2220Describe 'Resolve-HandoffDependencies - additional cases' {
2221 BeforeAll {
2222 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2223 $script:agentsDir = Join-Path $script:tempDir 'agents'
2224 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
2225
2226 # Agent with string-format handoffs
2227 @'
2228---
2229description: "String handoff agent"
2230handoffs:
2231 - string-target
2232---
2233'@ | Set-Content -Path (Join-Path $script:agentsDir 'string-handoff.agent.md')
2234
2235 @'
2236---
2237description: "String target"
2238---
2239'@ | Set-Content -Path (Join-Path $script:agentsDir 'string-target.agent.md')
2240
2241 # Agent with broken YAML in handoffs section
2242 @'
2243---
2244description: "Broken YAML agent"
2245handoffs:
2246 - label: [invalid: yaml: :
2247---
2248'@ | Set-Content -Path (Join-Path $script:agentsDir 'broken-yaml.agent.md')
2249 }
2250
2251 AfterAll {
2252 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2253 }
2254
2255 It 'Resolves string-format handoffs' {
2256 $result = Resolve-HandoffDependencies -SeedAgents @('string-handoff') -AgentsDir $script:agentsDir
2257 $result | Should -Contain 'string-handoff'
2258 $result | Should -Contain 'string-target'
2259 }
2260
2261 It 'Warns but continues when handoff target file is missing' {
2262 $result = Resolve-HandoffDependencies -SeedAgents @('missing-agent') -AgentsDir $script:agentsDir 3>&1
2263 # The function emits a warning and returns the seed agent
2264 $agentNames = @($result | Where-Object { $_ -is [string] })
2265 $agentNames | Should -Contain 'missing-agent'
2266 }
2267
2268 It 'Warns and continues when handoff YAML is malformed' {
2269 $result = Resolve-HandoffDependencies -SeedAgents @('broken-yaml') -AgentsDir $script:agentsDir 3>&1
2270 $warnings = @($result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] })
2271 $warnings.Count | Should -BeGreaterOrEqual 1
2272 $agentNames = @($result | Where-Object { $_ -is [string] })
2273 $agentNames | Should -Contain 'broken-yaml'
2274 }
2275}
2276
2277Describe 'Resolve-HandoffDependencies - display name resolution' {
2278 BeforeAll {
2279 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2280 $script:agentsDir = Join-Path $script:tempDir 'agents'
2281 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
2282
2283 # Agent whose handoffs use display names instead of file stems
2284 @'
2285---
2286name: Parent Agent
2287description: "Agent with display-name handoffs"
2288handoffs:
2289 - label: "Go to child"
2290 agent: Child Agent
2291 prompt: Continue
2292---
2293'@ | Set-Content -Path (Join-Path $script:agentsDir 'parent-agent.agent.md')
2294
2295 @'
2296---
2297name: Child Agent
2298description: "Child with display name"
2299---
2300'@ | Set-Content -Path (Join-Path $script:agentsDir 'child-agent.agent.md')
2301
2302 # Chain using display names: Planner -> Implementor (mimics real hve-core agents)
2303 @'
2304---
2305name: Task Planner
2306description: "Planner agent"
2307handoffs:
2308 - label: "Implement"
2309 agent: Task Implementor
2310---
2311'@ | Set-Content -Path (Join-Path $script:agentsDir 'task-planner.agent.md')
2312
2313 @'
2314---
2315name: Task Implementor
2316description: "Implementor agent"
2317handoffs:
2318 - label: "Review"
2319 agent: Task Planner
2320---
2321'@ | Set-Content -Path (Join-Path $script:agentsDir 'task-implementor.agent.md')
2322 }
2323
2324 AfterAll {
2325 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2326 }
2327
2328 It 'Resolves handoff targets specified by display name' {
2329 $result = Resolve-HandoffDependencies -SeedAgents @('parent-agent') -AgentsDir $script:agentsDir
2330 $result | Should -Contain 'parent-agent'
2331 $result | Should -Contain 'child-agent'
2332 }
2333
2334 It 'Resolves circular display-name handoff chains' {
2335 $result = Resolve-HandoffDependencies -SeedAgents @('task-planner') -AgentsDir $script:agentsDir
2336 $result | Should -Contain 'task-planner'
2337 $result | Should -Contain 'task-implementor'
2338 }
2339}
2340
2341Describe 'Get-DiscoveredPrompts - maturity filtering' {
2342 BeforeAll {
2343 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2344 $script:promptsDir = Join-Path $script:tempDir 'prompts'
2345 $script:promptsSubDir = Join-Path $script:promptsDir 'test-collection'
2346 $script:ghDir = Join-Path $script:tempDir '.github'
2347 New-Item -ItemType Directory -Path $script:promptsSubDir -Force | Out-Null
2348 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
2349
2350 @'
2351---
2352description: "Stable prompt"
2353---
2354'@ | Set-Content -Path (Join-Path $script:promptsSubDir 'stable.prompt.md')
2355 }
2356
2357 AfterAll {
2358 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2359 }
2360
2361 It 'Skips prompts when none match allowed maturities' {
2362 $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('experimental')
2363 $result.Prompts.Count | Should -Be 0
2364 $result.Skipped.Count | Should -Be 1
2365 $result.Skipped[0].Reason | Should -Match 'maturity'
2366 }
2367}
2368
2369Describe 'Get-DiscoveredInstructions - maturity filtering' {
2370 BeforeAll {
2371 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2372 $script:instrDir = Join-Path $script:tempDir 'instructions'
2373 $script:instrSubDir = Join-Path $script:instrDir 'test-collection'
2374 $script:ghDir = Join-Path $script:tempDir '.github'
2375 New-Item -ItemType Directory -Path $script:instrSubDir -Force | Out-Null
2376 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
2377
2378 @'
2379---
2380description: "Test instruction"
2381applyTo: "**/*.ps1"
2382---
2383'@ | Set-Content -Path (Join-Path $script:instrSubDir 'test.instructions.md')
2384 }
2385
2386 AfterAll {
2387 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2388 }
2389
2390 It 'Skips instructions when none match allowed maturities' {
2391 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('experimental')
2392 $result.Instructions.Count | Should -Be 0
2393 $result.Skipped.Count | Should -Be 1
2394 $result.Skipped[0].Reason | Should -Match 'maturity'
2395 }
2396}
2397
2398Describe 'Invoke-PrepareExtension - error cases' {
2399 BeforeAll {
2400 $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString())
2401 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
2402
2403 $script:extDir = Join-Path $script:tempDir 'extension'
2404 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
2405
2406 $script:templatesDir = Join-Path $script:extDir 'templates'
2407 New-Item -ItemType Directory -Path $script:templatesDir -Force | Out-Null
2408 @'
2409{
2410 "name": "hve-core",
2411 "displayName": "HVE Core",
2412 "version": "1.0.0",
2413 "description": "Test extension",
2414 "publisher": "test-pub",
2415 "engines": { "vscode": "^1.80.0" },
2416 "contributes": {}
2417}
2418'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json')
2419
2420 $script:collectionsDir = Join-Path $script:tempDir 'collections'
2421 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
2422 @"
2423id: hve-core
2424name: HVE Core
2425displayName: HVE Core
2426description: Test
2427"@ | Set-Content -Path (Join-Path $script:collectionsDir 'hve-core.collection.yml')
2428
2429 $script:ghDir = Join-Path $script:tempDir '.github'
2430 New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'agents') -Force | Out-Null
2431 New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'prompts') -Force | Out-Null
2432 New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'instructions') -Force | Out-Null
2433 }
2434
2435 AfterAll {
2436 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2437 }
2438
2439 It 'Fails when package.json has invalid JSON' {
2440 # Write invalid JSON and mock generation to preserve it
2441 $badPkgPath = Join-Path $script:extDir 'package.json'
2442 'NOT VALID JSON' | Set-Content -Path $badPkgPath
2443
2444 Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) }
2445
2446 $result = Invoke-PrepareExtension `
2447 -ExtensionDirectory $script:extDir `
2448 -RepoRoot $script:tempDir `
2449 -Channel 'Stable'
2450
2451 $result.Success | Should -BeFalse
2452 $result.ErrorMessage | Should -Match 'Failed to parse package.json'
2453 }
2454
2455 It 'Fails when package.json lacks version field' {
2456 $badPkgPath = Join-Path $script:extDir 'package.json'
2457 @{ name = 'test-no-version' } | ConvertTo-Json | Set-Content -Path $badPkgPath
2458
2459 Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) }
2460
2461 $result = Invoke-PrepareExtension `
2462 -ExtensionDirectory $script:extDir `
2463 -RepoRoot $script:tempDir `
2464 -Channel 'Stable'
2465
2466 $result.Success | Should -BeFalse
2467 $result.ErrorMessage | Should -Match "does not contain a 'version' field"
2468 }
2469
2470 It 'Fails when version format is invalid' {
2471 $badPkgPath = Join-Path $script:extDir 'package.json'
2472 @{ name = 'test'; version = 'not-semver' } | ConvertTo-Json | Set-Content -Path $badPkgPath
2473
2474 Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) }
2475
2476 $result = Invoke-PrepareExtension `
2477 -ExtensionDirectory $script:extDir `
2478 -RepoRoot $script:tempDir `
2479 -Channel 'Stable'
2480
2481 $result.Success | Should -BeFalse
2482 $result.ErrorMessage | Should -Match 'Invalid version format'
2483 }
2484
2485 It 'Warns when changelog path specified but file not found' {
2486 $validPkgPath = Join-Path $script:extDir 'package.json'
2487 @{ name = 'test'; version = '1.0.0'; contributes = @{} } | ConvertTo-Json -Depth 5 | Set-Content -Path $validPkgPath
2488
2489 $result = Invoke-PrepareExtension `
2490 -ExtensionDirectory $script:extDir `
2491 -RepoRoot $script:tempDir `
2492 -Channel 'Stable' `
2493 -ChangelogPath (Join-Path $script:tempDir 'NONEXISTENT-CHANGELOG.md') 3>&1
2494
2495 # Filter out the result hashtable from warnings
2496 $hashtableResult = $result | Where-Object { $_ -is [hashtable] }
2497 if ($hashtableResult) {
2498 $hashtableResult.Success | Should -BeTrue
2499 }
2500 }
2501
2502 Context 'Collection with requires dependencies' {
2503 BeforeAll {
2504 $script:reqCollectionPath = Join-Path $script:tempDir 'requires-test.collection.yml'
2505 @"
2506id: hve-core
2507name: HVE Core
2508displayName: HVE Core
2509description: Requires test
2510items:
2511 - kind: agent
2512 path: .github/agents/test-collection/main.agent.md
2513 maturity: stable
2514 requires:
2515 prompts:
2516 - dep-prompt
2517 - kind: prompt
2518 path: .github/prompts/test-collection/dep-prompt.prompt.md
2519 maturity: stable
2520"@ | Set-Content -Path $script:reqCollectionPath
2521
2522 # Create required agent and prompt files in subdirectories
2523 $reqAgentDir = Join-Path $script:ghDir 'agents/test-collection'
2524 $reqPromptDir = Join-Path $script:ghDir 'prompts/test-collection'
2525 New-Item -ItemType Directory -Path $reqAgentDir -Force | Out-Null
2526 New-Item -ItemType Directory -Path $reqPromptDir -Force | Out-Null
2527 @'
2528---
2529description: "Main agent"
2530---
2531'@ | Set-Content -Path (Join-Path $reqAgentDir 'main.agent.md')
2532
2533 @'
2534---
2535description: "Dependent prompt"
2536---
2537'@ | Set-Content -Path (Join-Path $reqPromptDir 'dep-prompt.prompt.md')
2538
2539 # Restore valid package.json
2540 $validPkgPath = Join-Path $script:extDir 'package.json'
2541 @{ name = 'hve-core'; version = '1.0.0'; contributes = @{} } | ConvertTo-Json -Depth 5 | Set-Content -Path $validPkgPath
2542 }
2543
2544 It 'Resolves requires dependencies in collection' {
2545 $result = Invoke-PrepareExtension `
2546 -ExtensionDirectory $script:extDir `
2547 -RepoRoot $script:tempDir `
2548 -Channel 'Stable' `
2549 -Collection $script:reqCollectionPath `
2550 -DryRun
2551
2552 $result.Success | Should -BeTrue
2553 $result.AgentCount | Should -BeGreaterOrEqual 1
2554 $result.PromptCount | Should -BeGreaterOrEqual 1
2555 }
2556 }
2557}
2558
2559Describe 'Invoke-ExtensionCollectionsGeneration - collection manifest errors' {
2560 BeforeAll {
2561 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2562
2563 $collectionsDir = Join-Path $script:tempDir 'collections'
2564 $templatesDir = Join-Path $script:tempDir 'extension/templates'
2565 New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null
2566 New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null
2567
2568 @{
2569 name = 'hve-core'
2570 displayName = 'HVE Core'
2571 version = '1.0.0'
2572 description = 'default'
2573 publisher = 'test-pub'
2574 engines = @{ vscode = '^1.80.0' }
2575 contributes = @{}
2576 } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json')
2577 }
2578
2579 AfterAll {
2580 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2581 }
2582
2583 It 'Throws when collection id is empty' {
2584 $collectionsDir = Join-Path $script:tempDir 'collections'
2585 Remove-Item -Path "$collectionsDir/*" -Force -ErrorAction SilentlyContinue
2586 @"
2587id:
2588name: empty-id
2589"@ | Set-Content -Path (Join-Path $collectionsDir 'empty.collection.yml')
2590
2591 { Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir } | Should -Throw '*Collection id is required*'
2592 }
2593
2594 It 'Throws when collection manifest is not a hashtable' {
2595 $collectionsDir = Join-Path $script:tempDir 'collections'
2596 Remove-Item -Path "$collectionsDir/*" -Force -ErrorAction SilentlyContinue
2597 # YAML that parses as a scalar string
2598 'just a string' | Set-Content -Path (Join-Path $collectionsDir 'bad.collection.yml')
2599
2600 { Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir } | Should -Throw '*must be a hashtable*'
2601 }
2602}
2603
2604Describe 'Invoke-ExtensionCollectionsGeneration - README generation' {
2605 BeforeAll {
2606 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2607
2608 $collectionsDir = Join-Path $script:tempDir 'collections'
2609 $templatesDir = Join-Path $script:tempDir 'extension/templates'
2610 New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null
2611 New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null
2612
2613 # Package template
2614 @{
2615 name = 'hve-core'
2616 displayName = 'HVE Core'
2617 version = '1.0.0'
2618 description = 'default'
2619 publisher = 'test-pub'
2620 engines = @{ vscode = '^1.80.0' }
2621 contributes = @{}
2622 } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json')
2623
2624 # README template
2625 $repoRoot = (Get-Item "$PSScriptRoot/../../..").FullName
2626 $realTemplatePath = Join-Path $repoRoot 'extension/templates/README.template.md'
2627 if (Test-Path $realTemplatePath) {
2628 Copy-Item -Path $realTemplatePath -Destination (Join-Path $templatesDir 'README.template.md')
2629 }
2630 else {
2631 @"
2632# {{DISPLAY_NAME}}
2633
2634> {{DESCRIPTION}}
2635
2636{{BODY}}
2637
2638{{ARTIFACTS}}
2639
2640{{FULL_EDITION}}
2641"@ | Set-Content -Path (Join-Path $templatesDir 'README.template.md')
2642 }
2643
2644 # Collection with a .collection.md body file
2645 @"
2646id: readme-test
2647name: README Test
2648displayName: HVE Core - README Test
2649description: Test readme generation
2650"@ | Set-Content -Path (Join-Path $collectionsDir 'readme-test.collection.yml')
2651
2652 'Body content for readme test.' | Set-Content -Path (Join-Path $collectionsDir 'readme-test.collection.md')
2653
2654 # hve-core needed for the defaults
2655 @"
2656id: hve-core
2657name: HVE Core
2658displayName: HVE Core
2659description: All artifacts
2660"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core.collection.yml')
2661
2662 'HVE Core body content.' | Set-Content -Path (Join-Path $collectionsDir 'hve-core.collection.md')
2663
2664 # hve-core-all collection with body
2665 @"
2666id: hve-core-all
2667name: All
2668displayName: HVE Core - All
2669description: All combined
2670"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.yml')
2671
2672 'HVE Core All body content.' | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.md')
2673
2674 # Collection without .collection.md body
2675 @"
2676id: no-readme
2677name: No README
2678displayName: HVE Core - No README
2679description: Collection without body
2680"@ | Set-Content -Path (Join-Path $collectionsDir 'no-readme.collection.yml')
2681 }
2682
2683 AfterAll {
2684 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2685 }
2686
2687 It 'Generates README files for collections with .collection.md' {
2688 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2689 $readmePath = Join-Path $script:tempDir 'extension/README.readme-test.md'
2690 Test-Path $readmePath | Should -BeTrue
2691 $content = Get-Content -Path $readmePath -Raw
2692 $content | Should -Match 'Body content for readme test'
2693 }
2694
2695 It 'Generates README.md for hve-core collection' {
2696 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2697 $readmePath = Join-Path $script:tempDir 'extension/README.md'
2698 Test-Path $readmePath | Should -BeTrue
2699 $content = Get-Content -Path $readmePath -Raw
2700 $content | Should -Match 'HVE Core body content'
2701 }
2702
2703 It 'Generates README for hve-core-all collection' {
2704 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2705 $readmePath = Join-Path $script:tempDir 'extension/README.hve-core-all.md'
2706 Test-Path $readmePath | Should -BeTrue
2707 $content = Get-Content -Path $readmePath -Raw
2708 $content | Should -Match 'HVE Core All body content'
2709 }
2710
2711 It 'Skips README generation when .collection.md is missing' {
2712 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2713 $readmePath = Join-Path $script:tempDir 'extension/README.no-readme.md'
2714 Test-Path $readmePath | Should -BeFalse
2715 }
2716}
2717
2718#region Deprecated Path Exclusion Tests
2719
2720Describe 'Get-DiscoveredAgents - deprecated path exclusion' {
2721 BeforeAll {
2722 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2723 $script:agentsDir = Join-Path $script:tempDir 'agents'
2724 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
2725
2726 # Create active agent
2727 $activeDir = Join-Path $script:agentsDir 'rpi'
2728 New-Item -ItemType Directory -Path $activeDir -Force | Out-Null
2729 @'
2730---
2731description: "Active agent"
2732---
2733'@ | Set-Content -Path (Join-Path $activeDir 'active.agent.md')
2734
2735 # Create deprecated agent
2736 $deprecatedDir = Join-Path $script:agentsDir 'deprecated'
2737 New-Item -ItemType Directory -Path $deprecatedDir -Force | Out-Null
2738 @'
2739---
2740description: "Deprecated agent"
2741---
2742'@ | Set-Content -Path (Join-Path $deprecatedDir 'old.agent.md')
2743 }
2744
2745 AfterAll {
2746 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2747 }
2748
2749 It 'Excludes agents in deprecated directory' {
2750 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -ExcludedAgents @()
2751 $agentNames = $result.Agents | ForEach-Object { $_.name }
2752 $agentNames | Should -Contain 'active'
2753 $agentNames | Should -Not -Contain 'old'
2754 }
2755}
2756
2757Describe 'Get-DiscoveredPrompts - deprecated path exclusion' {
2758 BeforeAll {
2759 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2760 $script:promptsDir = Join-Path $script:tempDir 'prompts'
2761 $script:ghDir = Join-Path $script:tempDir '.github'
2762 New-Item -ItemType Directory -Path $script:promptsDir -Force | Out-Null
2763 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
2764
2765 # Create active prompt
2766 $activeDir = Join-Path $script:promptsDir 'rpi'
2767 New-Item -ItemType Directory -Path $activeDir -Force | Out-Null
2768 @'
2769---
2770description: "Active prompt"
2771---
2772'@ | Set-Content -Path (Join-Path $activeDir 'active.prompt.md')
2773
2774 # Create deprecated prompt
2775 $deprecatedDir = Join-Path $script:promptsDir 'deprecated'
2776 New-Item -ItemType Directory -Path $deprecatedDir -Force | Out-Null
2777 @'
2778---
2779description: "Deprecated prompt"
2780---
2781'@ | Set-Content -Path (Join-Path $deprecatedDir 'old.prompt.md')
2782 }
2783
2784 AfterAll {
2785 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2786 }
2787
2788 It 'Excludes prompts in deprecated directory' {
2789 $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
2790 $promptNames = $result.Prompts | ForEach-Object { $_.name }
2791 $promptNames | Should -Contain 'active'
2792 $promptNames | Should -Not -Contain 'old'
2793 }
2794}
2795
2796Describe 'Get-DiscoveredInstructions - deprecated path exclusion' {
2797 BeforeAll {
2798 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2799 $script:instrDir = Join-Path $script:tempDir 'instructions'
2800 $script:ghDir = Join-Path $script:tempDir '.github'
2801 New-Item -ItemType Directory -Path $script:instrDir -Force | Out-Null
2802 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
2803
2804 # Create active instruction
2805 $activeDir = Join-Path $script:instrDir 'rpi'
2806 New-Item -ItemType Directory -Path $activeDir -Force | Out-Null
2807 @'
2808---
2809description: "Active instruction"
2810applyTo: "**/*.ps1"
2811---
2812'@ | Set-Content -Path (Join-Path $activeDir 'active.instructions.md')
2813
2814 # Create deprecated instruction
2815 $deprecatedDir = Join-Path $script:instrDir 'deprecated'
2816 New-Item -ItemType Directory -Path $deprecatedDir -Force | Out-Null
2817 @'
2818---
2819description: "Deprecated instruction"
2820applyTo: "**/*.ps1"
2821---
2822'@ | Set-Content -Path (Join-Path $deprecatedDir 'old.instructions.md')
2823 }
2824
2825 AfterAll {
2826 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2827 }
2828
2829 It 'Excludes instructions in deprecated directory' {
2830 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
2831 $instrNames = $result.Instructions | ForEach-Object { $_.name }
2832 $instrNames | Should -Contain 'active-instructions'
2833 $instrNames | Should -Not -Contain 'old-instructions'
2834 }
2835}
2836
2837Describe 'Get-DiscoveredSkills - deprecated path exclusion' {
2838 BeforeAll {
2839 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2840 $script:skillsDir = Join-Path $script:tempDir 'skills'
2841 New-Item -ItemType Directory -Path $script:skillsDir -Force | Out-Null
2842
2843 # Create active skill
2844 $activeSkillDir = Join-Path $script:skillsDir 'experimental/good-skill'
2845 New-Item -ItemType Directory -Path $activeSkillDir -Force | Out-Null
2846 @'
2847---
2848name: good-skill
2849description: "Active skill"
2850---
2851'@ | Set-Content -Path (Join-Path $activeSkillDir 'SKILL.md')
2852
2853 # Create deprecated skill
2854 $deprecatedSkillDir = Join-Path $script:skillsDir 'deprecated/old-skill'
2855 New-Item -ItemType Directory -Path $deprecatedSkillDir -Force | Out-Null
2856 @'
2857---
2858name: old-skill
2859description: "Deprecated skill"
2860---
2861'@ | Set-Content -Path (Join-Path $deprecatedSkillDir 'SKILL.md')
2862 }
2863
2864 AfterAll {
2865 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2866 }
2867
2868 It 'Excludes skills in deprecated directory' {
2869 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
2870 $skillNames = $result.Skills | ForEach-Object { $_.name }
2871 $skillNames | Should -Contain 'good-skill'
2872 $skillNames | Should -Not -Contain 'old-skill'
2873 }
2874}
2875
2876#endregion Deprecated Path Exclusion Tests
2877
2878#region Maturity Notice Tests
2879
2880Describe 'New-CollectionReadme - maturity notice' {
2881 BeforeAll {
2882 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2883 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
2884
2885 # Create minimal README template with all tokens including MATURITY_NOTICE
2886 $templateContent = @"
2887# {{DISPLAY_NAME}}
2888
2889> {{DESCRIPTION}}
2890
2891{{MATURITY_NOTICE}}
2892
2893{{BODY}}
2894
2895## Included Artifacts
2896
2897{{ARTIFACTS}}
2898
2899{{FULL_EDITION}}
2900"@
2901 $script:templatePath = Join-Path $script:tempDir 'README.template.md'
2902 Set-Content -Path $script:templatePath -Value $templateContent
2903
2904 # Create collection body markdown
2905 $script:bodyPath = Join-Path $script:tempDir 'test.collection.md'
2906 Set-Content -Path $script:bodyPath -Value 'Collection body content.'
2907 }
2908
2909 AfterAll {
2910 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2911 }
2912
2913 It 'Includes experimental notice for experimental collection' {
2914 $collection = @{
2915 id = 'test-exp'
2916 name = 'Test Experimental'
2917 description = 'An experimental collection'
2918 maturity = 'experimental'
2919 items = @()
2920 }
2921 $outputPath = Join-Path $script:tempDir 'README-exp.md'
2922 New-CollectionReadme -Collection $collection -CollectionMdPath $script:bodyPath `
2923 -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outputPath
2924
2925 $content = Get-Content -Path $outputPath -Raw
2926 $content | Should -Match '\u26A0' # warning sign emoji
2927 $content | Should -Match 'Pre-Release channel'
2928 }
2929
2930 It 'Has no notice for collection without maturity field' {
2931 $collection = @{
2932 id = 'test-default'
2933 name = 'Test Default'
2934 description = 'A default collection'
2935 items = @()
2936 }
2937 $outputPath = Join-Path $script:tempDir 'README-default.md'
2938 New-CollectionReadme -Collection $collection -CollectionMdPath $script:bodyPath `
2939 -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outputPath
2940
2941 $content = Get-Content -Path $outputPath -Raw
2942 $content | Should -Not -Match '\u26A0'
2943 }
2944
2945 It 'Has no notice for explicit stable maturity' {
2946 $collection = @{
2947 id = 'test-stable'
2948 name = 'Test Stable'
2949 description = 'A stable collection'
2950 maturity = 'stable'
2951 items = @()
2952 }
2953 $outputPath = Join-Path $script:tempDir 'README-stable.md'
2954 New-CollectionReadme -Collection $collection -CollectionMdPath $script:bodyPath `
2955 -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outputPath
2956
2957 $content = Get-Content -Path $outputPath -Raw
2958 $content | Should -Not -Match '\u26A0'
2959 }
2960}
2961
2962#endregion Maturity Notice Tests
2963
2964#region Split-CollectionMdByMarkers Tests
2965
2966Describe 'Split-CollectionMdByMarkers' {
2967 It 'Returns HasMarkers false for content without markers' {
2968 $result = Split-CollectionMdByMarkers -Content 'Hello world'
2969 $result.HasMarkers | Should -BeFalse
2970 $result.Intro | Should -Be 'Hello world'
2971 $result.Footer | Should -Be ''
2972 }
2973
2974 It 'Throws for empty string input' {
2975 { Split-CollectionMdByMarkers -Content '' } | Should -Throw
2976 }
2977
2978 It 'Parses intro and footer around markers' {
2979 $content = "Intro text`n`n<!-- BEGIN AUTO-GENERATED ARTIFACTS -->`n`nGenerated`n`n<!-- END AUTO-GENERATED ARTIFACTS -->`n`nFooter text"
2980 $result = Split-CollectionMdByMarkers -Content $content
2981 $result.HasMarkers | Should -BeTrue
2982 $result.Intro | Should -Be 'Intro text'
2983 $result.Footer | Should -Be 'Footer text'
2984 }
2985
2986 It 'Returns HasMarkers false when only BEGIN marker is present' {
2987 $content = "Intro`n<!-- BEGIN AUTO-GENERATED ARTIFACTS -->`nSome content"
2988 $result = Split-CollectionMdByMarkers -Content $content
2989 $result.HasMarkers | Should -BeFalse
2990 }
2991
2992 It 'Returns HasMarkers false when END marker appears before BEGIN' {
2993 $content = "<!-- END AUTO-GENERATED ARTIFACTS -->`n<!-- BEGIN AUTO-GENERATED ARTIFACTS -->"
2994 $result = Split-CollectionMdByMarkers -Content $content
2995 $result.HasMarkers | Should -BeFalse
2996 }
2997
2998 It 'Returns HasMarkers false for duplicate BEGIN markers without END' {
2999 $content = "<!-- BEGIN AUTO-GENERATED ARTIFACTS -->`n<!-- BEGIN AUTO-GENERATED ARTIFACTS -->`nContent"
3000 $result = Split-CollectionMdByMarkers -Content $content
3001 $result.HasMarkers | Should -BeFalse
3002 }
3003
3004 It 'Does not include an Existing key in the result' {
3005 $noMarkers = Split-CollectionMdByMarkers -Content 'plain'
3006 $noMarkers.Keys | Should -Not -Contain 'Existing'
3007
3008 $withMarkers = Split-CollectionMdByMarkers -Content "Intro`n<!-- BEGIN AUTO-GENERATED ARTIFACTS -->`n`n<!-- END AUTO-GENERATED ARTIFACTS -->"
3009 $withMarkers.Keys | Should -Not -Contain 'Existing'
3010 }
3011}
3012
3013#endregion Split-CollectionMdByMarkers Tests
3014
3015#endregion Additional Coverage Tests
3016