microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/fix-hardcoded-paths-in-artifacts

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

2622lines · 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
472#endregion Package Generation Function Tests
473
474Describe 'Get-AllowedMaturities' {
475 It 'Returns only stable for Stable channel' {
476 $result = Get-AllowedMaturities -Channel 'Stable'
477 $result | Should -Be @('stable')
478 }
479
480 It 'Returns all maturities for PreRelease channel' {
481 $result = Get-AllowedMaturities -Channel 'PreRelease'
482 $result | Should -Contain 'stable'
483 $result | Should -Contain 'preview'
484 $result | Should -Contain 'experimental'
485 }
486
487}
488
489Describe 'Test-CollectionMaturityEligible' {
490 It 'Returns eligible for stable collection on Stable channel' {
491 $manifest = @{ id = 'test'; maturity = 'stable' }
492 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
493 $result.IsEligible | Should -BeTrue
494 $result.Reason | Should -BeNullOrEmpty
495 }
496
497 It 'Returns eligible for stable collection on PreRelease channel' {
498 $manifest = @{ id = 'test'; maturity = 'stable' }
499 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
500 $result.IsEligible | Should -BeTrue
501 }
502
503 It 'Returns eligible for preview collection on Stable channel' {
504 $manifest = @{ id = 'test'; maturity = 'preview' }
505 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
506 $result.IsEligible | Should -BeTrue
507 }
508
509 It 'Returns eligible for preview collection on PreRelease channel' {
510 $manifest = @{ id = 'test'; maturity = 'preview' }
511 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
512 $result.IsEligible | Should -BeTrue
513 }
514
515 It 'Returns ineligible for experimental collection on Stable channel' {
516 $manifest = @{ id = 'exp-coll'; maturity = 'experimental' }
517 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
518 $result.IsEligible | Should -BeFalse
519 $result.Reason | Should -Match 'experimental.*excluded from Stable'
520 }
521
522 It 'Returns eligible for experimental collection on PreRelease channel' {
523 $manifest = @{ id = 'exp-coll'; maturity = 'experimental' }
524 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
525 $result.IsEligible | Should -BeTrue
526 }
527
528 It 'Returns ineligible for deprecated collection on Stable channel' {
529 $manifest = @{ id = 'old-coll'; maturity = 'deprecated' }
530 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
531 $result.IsEligible | Should -BeFalse
532 $result.Reason | Should -Match 'deprecated.*excluded from all channels'
533 }
534
535 It 'Returns ineligible for deprecated collection on PreRelease channel' {
536 $manifest = @{ id = 'old-coll'; maturity = 'deprecated' }
537 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
538 $result.IsEligible | Should -BeFalse
539 $result.Reason | Should -Match 'deprecated.*excluded from all channels'
540 }
541
542 It 'Defaults to stable when maturity key is absent' {
543 $manifest = @{ id = 'no-maturity' }
544 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
545 $result.IsEligible | Should -BeTrue
546 }
547
548 It 'Defaults to stable when maturity value is empty string' {
549 $manifest = @{ id = 'empty-maturity'; maturity = '' }
550 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
551 $result.IsEligible | Should -BeTrue
552 }
553
554 It 'Returns ineligible for unknown maturity value' {
555 $manifest = @{ id = 'bad-coll'; maturity = 'alpha' }
556 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
557 $result.IsEligible | Should -BeFalse
558 $result.Reason | Should -Match 'invalid maturity value'
559 }
560
561 It 'Returns hashtable with expected keys' {
562 $manifest = @{ id = 'test'; maturity = 'stable' }
563 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
564 $result.Keys | Should -Contain 'IsEligible'
565 $result.Keys | Should -Contain 'Reason'
566 }
567}
568
569Describe 'Test-PathsExist' {
570 BeforeAll {
571 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
572 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
573 $script:extDir = Join-Path $script:tempDir 'extension'
574 $script:ghDir = Join-Path $script:tempDir '.github'
575 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
576 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
577 $script:pkgJson = Join-Path $script:extDir 'package.json'
578 '{}' | Set-Content -Path $script:pkgJson
579 }
580
581 AfterAll {
582 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
583 }
584
585 It 'Returns valid when all paths exist' {
586 $result = Test-PathsExist -ExtensionDir $script:extDir -PackageJsonPath $script:pkgJson -GitHubDir $script:ghDir
587 $result.IsValid | Should -BeTrue
588 $result.MissingPaths | Should -BeNullOrEmpty
589 }
590
591 It 'Returns invalid when extension dir missing' {
592 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-ext-dir-12345')
593 $result = Test-PathsExist -ExtensionDir $nonexistentPath -PackageJsonPath $script:pkgJson -GitHubDir $script:ghDir
594 $result.IsValid | Should -BeFalse
595 $result.MissingPaths | Should -Contain $nonexistentPath
596 }
597
598 It 'Collects multiple missing paths' {
599 $missing1 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-1')
600 $missing2 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-2')
601 $missing3 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-3')
602 $result = Test-PathsExist -ExtensionDir $missing1 -PackageJsonPath $missing2 -GitHubDir $missing3
603 $result.IsValid | Should -BeFalse
604 $result.MissingPaths.Count | Should -Be 3
605 }
606}
607
608Describe 'Get-DiscoveredAgents' {
609 BeforeAll {
610 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
611 $script:agentsDir = Join-Path $script:tempDir 'agents'
612 $script:agentsSubDir = Join-Path $script:agentsDir 'test-collection'
613 New-Item -ItemType Directory -Path $script:agentsSubDir -Force | Out-Null
614
615 # Create test agent files in subdirectory (distributable)
616 @'
617---
618description: "Stable agent"
619---
620'@ | Set-Content -Path (Join-Path $script:agentsSubDir 'stable.agent.md')
621
622 @'
623---
624description: "Preview agent"
625---
626'@ | Set-Content -Path (Join-Path $script:agentsSubDir 'preview.agent.md')
627
628 # Create root-level agent (repo-specific, should be skipped)
629 @'
630---
631description: "Root-level agent"
632---
633'@ | Set-Content -Path (Join-Path $script:agentsDir 'root-agent.agent.md')
634
635 }
636
637 AfterAll {
638 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
639 }
640
641 It 'Discovers agents matching allowed maturities' {
642 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @()
643 $result.DirectoryExists | Should -BeTrue
644 $result.Agents.Count | Should -Be 2
645 }
646
647 It 'Filters agents by maturity' {
648 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('preview') -ExcludedAgents @()
649 $result.Agents.Count | Should -Be 0
650 $result.Skipped.Count | Should -Be 3
651 }
652
653 It 'Excludes specified agents' {
654 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @('stable')
655 $result.Agents.Count | Should -Be 1
656 }
657
658 It 'Returns empty when directory does not exist' {
659 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-agents-dir-12345')
660 $result = Get-DiscoveredAgents -AgentsDir $nonexistentPath -AllowedMaturities @('stable') -ExcludedAgents @()
661 $result.DirectoryExists | Should -BeFalse
662 $result.Agents | Should -BeNullOrEmpty
663 }
664
665 It 'Skips root-level repo-specific agents with correct skip reason' {
666 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @()
667 $agentNames = $result.Agents | ForEach-Object { $_.name }
668 $agentNames | Should -Not -Contain 'root-agent'
669 $skipped = $result.Skipped | Where-Object { $_.Name -eq 'root-agent' }
670 $skipped | Should -Not -BeNullOrEmpty
671 $skipped.Reason | Should -Match 'repo-specific'
672 }
673}
674
675Describe 'Get-DiscoveredPrompts' {
676 BeforeAll {
677 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
678 $script:promptsDir = Join-Path $script:tempDir 'prompts'
679 $script:promptsSubDir = Join-Path $script:promptsDir 'test-collection'
680 $script:ghDir = Join-Path $script:tempDir '.github'
681 New-Item -ItemType Directory -Path $script:promptsSubDir -Force | Out-Null
682 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
683
684 @'
685---
686description: "Test prompt"
687---
688'@ | Set-Content -Path (Join-Path $script:promptsSubDir 'test.prompt.md')
689 }
690
691 AfterAll {
692 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
693 }
694
695 It 'Discovers prompts in directory' {
696 $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
697 $result.DirectoryExists | Should -BeTrue
698 $result.Prompts.Count | Should -BeGreaterThan 0
699 }
700
701 It 'Returns empty when directory does not exist' {
702 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-prompts-dir-12345')
703 $result = Get-DiscoveredPrompts -PromptsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable')
704 $result.DirectoryExists | Should -BeFalse
705 }
706}
707
708Describe 'Get-DiscoveredInstructions' {
709 BeforeAll {
710 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
711 $script:instrDir = Join-Path $script:tempDir 'instructions'
712 $script:instrSubDir = Join-Path $script:instrDir 'test-collection'
713 $script:ghDir = Join-Path $script:tempDir '.github'
714 New-Item -ItemType Directory -Path $script:instrSubDir -Force | Out-Null
715 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
716
717 @'
718---
719description: "Test instruction"
720applyTo: "**/*.ps1"
721---
722'@ | Set-Content -Path (Join-Path $script:instrSubDir 'test.instructions.md')
723 }
724
725 AfterAll {
726 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
727 }
728
729 It 'Discovers instructions in directory' {
730 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
731 $result.DirectoryExists | Should -BeTrue
732 $result.Instructions.Count | Should -BeGreaterThan 0
733 }
734
735 It 'Returns empty when directory does not exist' {
736 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-instr-dir-12345')
737 $result = Get-DiscoveredInstructions -InstructionsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable')
738 $result.DirectoryExists | Should -BeFalse
739 }
740
741 It 'Skips root-level repo-specific instructions' {
742 @'
743---
744description: "Repo-specific workflow instruction"
745applyTo: "**/.github/workflows/*.yml"
746---
747'@ | Set-Content -Path (Join-Path $script:instrDir 'workflows.instructions.md')
748
749 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
750 $instrNames = $result.Instructions | ForEach-Object { $_.name }
751 $instrNames | Should -Not -Contain 'workflows-instructions'
752 $result.Skipped | Where-Object { $_.Reason -match 'repo-specific' } | Should -Not -BeNullOrEmpty
753 }
754
755 It 'Still discovers instructions in subdirectories' {
756 $otherDir = Join-Path $script:instrDir 'csharp'
757 New-Item -ItemType Directory -Path $otherDir -Force | Out-Null
758 @'
759---
760description: "Repo-specific"
761applyTo: "**/.github/workflows/*.yml"
762---
763'@ | Set-Content -Path (Join-Path $script:instrDir 'workflows.instructions.md')
764 @'
765---
766description: "C# instruction"
767applyTo: "**/*.cs"
768---
769'@ | Set-Content -Path (Join-Path $otherDir 'csharp.instructions.md')
770
771 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
772 $instrNames = $result.Instructions | ForEach-Object { $_.name }
773 $instrNames | Should -Contain 'csharp-instructions'
774 $instrNames | Should -Not -Contain 'workflows-instructions'
775 }
776}
777
778Describe 'Get-DiscoveredSkills' {
779 BeforeAll {
780 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
781 $script:skillsDir = Join-Path $script:tempDir 'skills'
782 New-Item -ItemType Directory -Path $script:skillsDir -Force | Out-Null
783
784 # Create test skill under a collection-id directory
785 $skillDir = Join-Path $script:skillsDir 'test-collection/test-skill'
786 New-Item -ItemType Directory -Path $skillDir -Force | Out-Null
787 @'
788---
789name: test-skill
790description: "Test skill"
791---
792# Skill
793'@ | Set-Content -Path (Join-Path $skillDir 'SKILL.md')
794
795 # Create nested skill under same collection-id directory
796 $nestedSkillDir = Join-Path $script:skillsDir 'test-collection/nested-skill'
797 New-Item -ItemType Directory -Path $nestedSkillDir -Force | Out-Null
798 @'
799---
800name: nested-skill
801description: "Nested skill in collection"
802---
803# Nested Skill
804'@ | Set-Content -Path (Join-Path $nestedSkillDir 'SKILL.md')
805
806 # Create root-level skill (repo-specific, should be skipped)
807 $rootSkillDir = Join-Path $script:skillsDir 'root-skill'
808 New-Item -ItemType Directory -Path $rootSkillDir -Force | Out-Null
809 @'
810---
811name: root-skill
812description: "Root-level skill"
813---
814# Root Skill
815'@ | Set-Content -Path (Join-Path $rootSkillDir 'SKILL.md')
816
817 }
818
819 AfterAll {
820 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
821 }
822
823 It 'Discovers skills in directory' {
824 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
825 $result.DirectoryExists | Should -BeTrue
826 $result.Skills.Count | Should -Be 2
827 $skillNames = $result.Skills | ForEach-Object { $_.name }
828 $skillNames | Should -Contain 'test-skill'
829 $skillNames | Should -Contain 'nested-skill'
830 }
831
832 It 'Returns empty when directory does not exist' {
833 $nonexistent = Join-Path $script:tempDir 'nonexistent-skills'
834 $result = Get-DiscoveredSkills -SkillsDir $nonexistent -AllowedMaturities @('stable')
835 $result.DirectoryExists | Should -BeFalse
836 $result.Skills | Should -BeNullOrEmpty
837 }
838
839 It 'Filters skills when stable is not an allowed maturity' {
840 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('preview')
841 $result.Skills.Count | Should -Be 0
842 $result.Skipped.Count | Should -BeGreaterThan 0
843 }
844
845 It 'Discovers nested skills with correct path' {
846 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
847 $nestedSkill = $result.Skills | Where-Object { $_.name -eq 'nested-skill' }
848 $nestedSkill | Should -Not -BeNullOrEmpty
849 $nestedSkill.path | Should -Be './.github/skills/test-collection/nested-skill/SKILL.md'
850 }
851
852 It 'Skips root-level repo-specific skills with correct skip reason' {
853 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
854 $skillNames = $result.Skills | ForEach-Object { $_.name }
855 $skillNames | Should -Not -Contain 'root-skill'
856 $skipped = $result.Skipped | Where-Object { $_.Name -eq 'root-skill' }
857 $skipped | Should -Not -BeNullOrEmpty
858 $skipped.Reason | Should -Match 'repo-specific'
859 }
860}
861
862Describe 'Get-CollectionManifest' {
863 BeforeAll {
864 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
865 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
866 }
867
868 AfterAll {
869 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
870 }
871
872 It 'Loads collection manifest from valid YAML path' {
873 $manifestFile = Join-Path $script:tempDir 'test.collection.yml'
874 @"
875id: test
876name: test-ext
877displayName: Test Extension
878description: Test
879items:
880 - hve-core-all
881"@ | Set-Content -Path $manifestFile
882
883 $result = Get-CollectionManifest -CollectionPath $manifestFile
884 $result | Should -Not -BeNullOrEmpty
885 $result.id | Should -Be 'test'
886 }
887
888 It 'Loads collection manifest from valid JSON path' {
889 $manifestFile = Join-Path $script:tempDir 'test.collection.json'
890 @{
891 '\$schema' = '../schemas/collection-manifest.schema.json'
892 id = 'test'
893 name = 'test-ext'
894 displayName = 'Test Extension'
895 description = 'Test'
896 items = @('hve-core-all')
897 } | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestFile
898
899 $result = Get-CollectionManifest -CollectionPath $manifestFile
900 $result | Should -Not -BeNullOrEmpty
901 $result.id | Should -Be 'test'
902 }
903
904 It 'Throws when path does not exist' {
905 $nonexistent = Join-Path $script:tempDir 'nonexistent.json'
906 { Get-CollectionManifest -CollectionPath $nonexistent } | Should -Throw '*not found*'
907 }
908
909 It 'Returns hashtable with expected keys' {
910 $manifestFile = Join-Path $script:tempDir 'keys.collection.yml'
911 @"
912id: keys
913name: keys-ext
914displayName: Keys
915description: Keys test
916items:
917 - developer
918"@ | Set-Content -Path $manifestFile
919
920 $result = Get-CollectionManifest -CollectionPath $manifestFile
921 $result.Keys | Should -Contain 'id'
922 $result.Keys | Should -Contain 'name'
923 $result.Keys | Should -Contain 'items'
924 }
925}
926
927Describe 'Test-GlobMatch' {
928 It 'Returns true for matching wildcard pattern' {
929 $result = Test-GlobMatch -Name 'rpi-agent' -Patterns @('rpi-*')
930 $result | Should -BeTrue
931 }
932
933 It 'Returns false for non-matching pattern' {
934 $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*')
935 $result | Should -BeFalse
936 }
937
938 It 'Matches against multiple patterns' {
939 $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*', 'mem*')
940 $result | Should -BeTrue
941 }
942
943 It 'Handles exact name match' {
944 $result = Test-GlobMatch -Name 'memory' -Patterns @('memory')
945 $result | Should -BeTrue
946 }
947}
948
949Describe 'Get-CollectionArtifacts' {
950 It 'Returns artifacts from collection items across supported kinds' {
951 $collection = @{
952 items = @(
953 @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' },
954 @{ kind = 'prompt'; path = '.github/prompts/dev-prompt.prompt.md' },
955 @{ kind = 'instruction'; path = '.github/instructions/dev/dev.instructions.md' },
956 @{ kind = 'skill'; path = '.github/skills/video-to-gif/' }
957 )
958 }
959
960 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable', 'preview')
961 $result.Agents | Should -Contain 'dev-agent'
962 $result.Prompts | Should -Contain 'dev-prompt'
963 $result.Instructions | Should -Contain 'dev/dev'
964 $result.Skills | Should -Contain 'video-to-gif'
965 }
966
967 It 'Uses item maturity when provided' {
968 $collection = @{
969 items = @(
970 @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md'; maturity = 'stable' },
971 @{ kind = 'agent'; path = '.github/agents/preview-dev.agent.md'; maturity = 'preview' }
972 )
973 }
974
975 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable')
976 $result.Agents | Should -Contain 'dev-agent'
977 $result.Agents | Should -Not -Contain 'preview-dev'
978 }
979
980 It 'Defaults to stable maturity when item maturity is omitted' {
981 $collection = @{
982 items = @(
983 @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' },
984 @{ kind = 'agent'; path = '.github/agents/preview-dev.agent.md' }
985 )
986 }
987
988 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable')
989 $result.Agents | Should -Contain 'dev-agent'
990 $result.Agents | Should -Contain 'preview-dev'
991 }
992
993 It 'Returns empty when collection has no items' {
994 $collection = @{ id = 'empty' }
995 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable')
996 $result.Agents.Count | Should -Be 0
997 $result.Prompts.Count | Should -Be 0
998 $result.Instructions.Count | Should -Be 0
999 $result.Skills.Count | Should -Be 0
1000 }
1001}
1002
1003Describe 'Resolve-HandoffDependencies' {
1004 BeforeAll {
1005 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1006 $script:agentsDir = Join-Path $script:tempDir 'agents'
1007 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
1008
1009 # Agent with no handoffs
1010 @'
1011---
1012description: "Solo agent"
1013---
1014'@ | Set-Content -Path (Join-Path $script:agentsDir 'solo.agent.md')
1015
1016 # Agent with single handoff (object format matching real agents)
1017 @'
1018---
1019description: "Parent agent"
1020handoffs:
1021 - label: "Go to child"
1022 agent: child
1023 prompt: Continue
1024---
1025'@ | Set-Content -Path (Join-Path $script:agentsDir 'parent.agent.md')
1026
1027 @'
1028---
1029description: "Child agent"
1030---
1031'@ | Set-Content -Path (Join-Path $script:agentsDir 'child.agent.md')
1032
1033 # Self-referential agent (object format)
1034 @'
1035---
1036description: "Self agent"
1037handoffs:
1038 - label: "Self"
1039 agent: self-ref
1040---
1041'@ | Set-Content -Path (Join-Path $script:agentsDir 'self-ref.agent.md')
1042
1043 # Circular chain (object format)
1044 @'
1045---
1046description: "Chain A"
1047handoffs:
1048 - label: "To B"
1049 agent: chain-b
1050---
1051'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-a.agent.md')
1052
1053 @'
1054---
1055description: "Chain B"
1056handoffs:
1057 - label: "To A"
1058 agent: chain-a
1059---
1060'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-b.agent.md')
1061
1062 }
1063
1064 AfterAll {
1065 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1066 }
1067
1068 It 'Returns seed agents when no handoffs' {
1069 $result = Resolve-HandoffDependencies -SeedAgents @('solo') -AgentsDir $script:agentsDir
1070 $result | Should -Contain 'solo'
1071 $result.Count | Should -Be 1
1072 }
1073
1074 It 'Resolves single-level handoff' {
1075 $result = Resolve-HandoffDependencies -SeedAgents @('parent') -AgentsDir $script:agentsDir
1076 $result | Should -Contain 'parent'
1077 $result | Should -Contain 'child'
1078 }
1079
1080 It 'Handles self-referential handoffs' {
1081 $result = Resolve-HandoffDependencies -SeedAgents @('self-ref') -AgentsDir $script:agentsDir
1082 $result | Should -Contain 'self-ref'
1083 $result.Count | Should -Be 1
1084 }
1085
1086 It 'Handles circular handoff chains' {
1087 $result = Resolve-HandoffDependencies -SeedAgents @('chain-a') -AgentsDir $script:agentsDir
1088 $result | Should -Contain 'chain-a'
1089 $result | Should -Contain 'chain-b'
1090 $result.Count | Should -Be 2
1091 }
1092}
1093
1094Describe 'Resolve-RequiresDependencies' {
1095 It 'Resolves agent requires to include dependent prompts' {
1096 $result = Resolve-RequiresDependencies `
1097 -ArtifactNames @{ agents = @('main') } `
1098 -AllowedMaturities @('stable') `
1099 -CollectionRequires @{ agents = @{ 'main' = @{ prompts = @('dep-prompt') } } } `
1100 -CollectionMaturities @{ prompts = @{ 'dep-prompt' = 'stable' } }
1101 $result.Prompts | Should -Contain 'dep-prompt'
1102 }
1103
1104 It 'Resolves transitive agent dependencies' {
1105 $result = Resolve-RequiresDependencies `
1106 -ArtifactNames @{ agents = @('top') } `
1107 -AllowedMaturities @('stable') `
1108 -CollectionRequires @{ agents = @{ 'top' = @{ agents = @('mid') }; 'mid' = @{ prompts = @('leaf-prompt') } } } `
1109 -CollectionMaturities @{ agents = @{ 'mid' = 'stable' }; prompts = @{ 'leaf-prompt' = 'stable' } }
1110 $result.Agents | Should -Contain 'mid'
1111 $result.Prompts | Should -Contain 'leaf-prompt'
1112 }
1113
1114 It 'Respects maturity filter on dependencies' {
1115 $result = Resolve-RequiresDependencies `
1116 -ArtifactNames @{ agents = @('main') } `
1117 -AllowedMaturities @('stable') `
1118 -CollectionRequires @{ agents = @{ 'main' = @{ prompts = @('exp-prompt') } } } `
1119 -CollectionMaturities @{ prompts = @{ 'exp-prompt' = 'experimental' } }
1120 $result.Prompts | Should -Not -Contain 'exp-prompt'
1121 }
1122}
1123
1124Describe 'Update-PackageJsonContributes' {
1125 It 'Updates contributes section with chat participants' {
1126 $packageJson = [PSCustomObject]@{
1127 name = 'test-extension'
1128 contributes = [PSCustomObject]@{}
1129 }
1130 $agents = @(
1131 @{ name = 'agent1'; description = 'Desc 1' }
1132 )
1133 $prompts = @(
1134 @{ name = 'prompt1'; description = 'Prompt desc' }
1135 )
1136 $instructions = @(
1137 @{ name = 'instr1'; description = 'Instr desc' }
1138 )
1139
1140 $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents $agents -ChatPromptFiles $prompts -ChatInstructions $instructions -ChatSkills @()
1141 $result.contributes | Should -Not -BeNullOrEmpty
1142 }
1143
1144 It 'Handles empty arrays' {
1145 $packageJson = [PSCustomObject]@{
1146 name = 'test-extension'
1147 contributes = [PSCustomObject]@{}
1148 }
1149
1150 $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents @() -ChatPromptFiles @() -ChatInstructions @() -ChatSkills @()
1151 $result | Should -Not -BeNullOrEmpty
1152 }
1153}
1154
1155Describe 'New-PrepareResult' {
1156 It 'Creates success result with counts' {
1157 $result = New-PrepareResult -Success $true -AgentCount 5 -PromptCount 10 -InstructionCount 15 -SkillCount 3 -Version '1.0.0'
1158 $result.Success | Should -BeTrue
1159 $result.AgentCount | Should -Be 5
1160 $result.PromptCount | Should -Be 10
1161 $result.InstructionCount | Should -Be 15
1162 $result.SkillCount | Should -Be 3
1163 $result.Version | Should -Be '1.0.0'
1164 $result.ErrorMessage | Should -BeNullOrEmpty
1165 }
1166
1167 It 'Creates failure result with error message' {
1168 $result = New-PrepareResult -Success $false -ErrorMessage 'Something went wrong'
1169 $result.Success | Should -BeFalse
1170 $result.ErrorMessage | Should -Be 'Something went wrong'
1171 $result.AgentCount | Should -Be 0
1172 $result.PromptCount | Should -Be 0
1173 $result.InstructionCount | Should -Be 0
1174 }
1175
1176 It 'Returns hashtable with all expected keys' {
1177 $result = New-PrepareResult -Success $true
1178 $result.Keys | Should -Contain 'Success'
1179 $result.Keys | Should -Contain 'AgentCount'
1180 $result.Keys | Should -Contain 'PromptCount'
1181 $result.Keys | Should -Contain 'InstructionCount'
1182 $result.Keys | Should -Contain 'SkillCount'
1183 $result.Keys | Should -Contain 'Version'
1184 $result.Keys | Should -Contain 'ErrorMessage'
1185 }
1186}
1187
1188Describe 'Invoke-PrepareExtension' {
1189 BeforeAll {
1190 $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString())
1191 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
1192
1193 # Create extension directory with package.json
1194 $script:extDir = Join-Path $script:tempDir 'extension'
1195 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
1196 @'
1197{
1198 "name": "test-extension",
1199 "version": "1.2.3",
1200 "contributes": {}
1201}
1202'@ | Set-Content -Path (Join-Path $script:extDir 'package.json')
1203
1204 # Create package template for generation
1205 $script:templatesDir = Join-Path $script:extDir 'templates'
1206 New-Item -ItemType Directory -Path $script:templatesDir -Force | Out-Null
1207 @'
1208{
1209 "name": "hve-core",
1210 "displayName": "HVE Core",
1211 "version": "1.2.3",
1212 "description": "Test extension",
1213 "publisher": "test-pub",
1214 "engines": { "vscode": "^1.80.0" },
1215 "contributes": {}
1216}
1217'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json')
1218
1219 # Create collections directory with a minimal hve-core collection (flagship)
1220 $script:collectionsDir = Join-Path $script:tempDir 'collections'
1221 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
1222 @"
1223id: hve-core
1224name: HVE Core
1225displayName: HVE Core
1226description: Test extension
1227"@ | Set-Content -Path (Join-Path $script:collectionsDir 'hve-core.collection.yml')
1228
1229 # Create .github structure with subdirectories (root-level files are repo-specific)
1230 $script:ghDir = Join-Path $script:tempDir '.github'
1231 $script:agentsDir = Join-Path $script:ghDir 'agents'
1232 $script:agentsSubDir = Join-Path $script:agentsDir 'test-collection'
1233 $script:promptsDir = Join-Path $script:ghDir 'prompts'
1234 $script:promptsSubDir = Join-Path $script:promptsDir 'test-collection'
1235 $script:instrDir = Join-Path $script:ghDir 'instructions'
1236 $script:instrSubDir = Join-Path $script:instrDir 'test-collection'
1237 New-Item -ItemType Directory -Path $script:agentsSubDir -Force | Out-Null
1238 New-Item -ItemType Directory -Path $script:promptsSubDir -Force | Out-Null
1239 New-Item -ItemType Directory -Path $script:instrSubDir -Force | Out-Null
1240
1241 # Create test agent in subdirectory
1242 @'
1243---
1244description: "Test agent"
1245---
1246# Agent
1247'@ | Set-Content -Path (Join-Path $script:agentsSubDir 'test.agent.md')
1248
1249 # Create test prompt in subdirectory
1250 @'
1251---
1252description: "Test prompt"
1253---
1254# Prompt
1255'@ | Set-Content -Path (Join-Path $script:promptsSubDir 'test.prompt.md')
1256
1257 # Create test instruction in subdirectory
1258 @'
1259---
1260description: "Test instruction"
1261applyTo: "**/*.ps1"
1262---
1263# Instruction
1264'@ | Set-Content -Path (Join-Path $script:instrSubDir 'test.instructions.md')
1265
1266 }
1267
1268 AfterAll {
1269 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1270 }
1271
1272 It 'Returns success result with correct counts' {
1273 $result = Invoke-PrepareExtension `
1274 -ExtensionDirectory $script:extDir `
1275 -RepoRoot $script:tempDir `
1276 -Channel 'Stable' `
1277 -DryRun
1278
1279 $result.Success | Should -BeTrue
1280 $result.AgentCount | Should -Be 1
1281 $result.PromptCount | Should -Be 1
1282 $result.InstructionCount | Should -Be 1
1283 $result.Version | Should -Be '1.2.3'
1284 }
1285
1286 It 'Fails when extension directory missing' {
1287 $nonexistentPath = Join-Path $TestDrive 'nonexistent-ext-dir-12345'
1288 $result = Invoke-PrepareExtension `
1289 -ExtensionDirectory $nonexistentPath `
1290 -RepoRoot $script:tempDir `
1291 -Channel 'Stable'
1292
1293 $result.Success | Should -BeFalse
1294 $result.ErrorMessage | Should -Not -BeNullOrEmpty
1295 }
1296
1297 It 'Respects channel filtering' {
1298 # Add preview agent in subdirectory
1299 @'
1300---
1301description: "Preview agent"
1302---
1303'@ | Set-Content -Path (Join-Path $script:agentsSubDir 'preview.agent.md')
1304
1305 $collectionPath = Join-Path $script:tempDir 'channel-filter.collection.yml'
1306 @"
1307id: hve-core
1308name: HVE Core
1309displayName: HVE Core
1310description: Channel filtering test
1311items:
1312 - kind: agent
1313 path: .github/agents/test-collection/test.agent.md
1314 maturity: stable
1315 - kind: agent
1316 path: .github/agents/test-collection/preview.agent.md
1317 maturity: preview
1318"@ | Set-Content -Path $collectionPath
1319
1320 $stableResult = Invoke-PrepareExtension `
1321 -ExtensionDirectory $script:extDir `
1322 -RepoRoot $script:tempDir `
1323 -Channel 'Stable' `
1324 -Collection $collectionPath `
1325 -DryRun
1326
1327 $preReleaseResult = Invoke-PrepareExtension `
1328 -ExtensionDirectory $script:extDir `
1329 -RepoRoot $script:tempDir `
1330 -Channel 'PreRelease' `
1331 -Collection $collectionPath `
1332 -DryRun
1333
1334 $preReleaseResult.AgentCount | Should -BeGreaterThan $stableResult.AgentCount
1335 }
1336
1337 It 'Filters prompts and instructions by maturity' {
1338 # Add experimental prompt in subdirectory
1339 @'
1340---
1341description: "Experimental prompt"
1342---
1343'@ | Set-Content -Path (Join-Path $script:promptsSubDir 'experimental.prompt.md')
1344
1345 # Add preview instruction in subdirectory
1346 @'
1347---
1348description: "Preview instruction"
1349applyTo: "**/*.js"
1350---
1351'@ | Set-Content -Path (Join-Path $script:instrSubDir 'preview.instructions.md')
1352
1353 $collectionPath = Join-Path $script:tempDir 'prompt-instruction-filter.collection.yml'
1354 @"
1355id: hve-core
1356name: HVE Core
1357displayName: HVE Core
1358description: Prompt/instruction filtering test
1359items:
1360 - kind: agent
1361 path: .github/agents/test-collection/test.agent.md
1362 maturity: stable
1363 - kind: prompt
1364 path: .github/prompts/test-collection/test.prompt.md
1365 maturity: stable
1366 - kind: prompt
1367 path: .github/prompts/test-collection/experimental.prompt.md
1368 maturity: experimental
1369 - kind: instruction
1370 path: .github/instructions/test-collection/test.instructions.md
1371 maturity: stable
1372 - kind: instruction
1373 path: .github/instructions/test-collection/preview.instructions.md
1374 maturity: preview
1375"@ | Set-Content -Path $collectionPath
1376
1377 $stableResult = Invoke-PrepareExtension `
1378 -ExtensionDirectory $script:extDir `
1379 -RepoRoot $script:tempDir `
1380 -Channel 'Stable' `
1381 -Collection $collectionPath `
1382 -DryRun
1383
1384 $preReleaseResult = Invoke-PrepareExtension `
1385 -ExtensionDirectory $script:extDir `
1386 -RepoRoot $script:tempDir `
1387 -Channel 'PreRelease' `
1388 -Collection $collectionPath `
1389 -DryRun
1390
1391 $preReleaseResult.PromptCount | Should -BeGreaterThan $stableResult.PromptCount
1392 $preReleaseResult.InstructionCount | Should -BeGreaterThan $stableResult.InstructionCount
1393 }
1394
1395 It 'Updates package.json when not DryRun' {
1396 $result = Invoke-PrepareExtension `
1397 -ExtensionDirectory $script:extDir `
1398 -RepoRoot $script:tempDir `
1399 -Channel 'Stable' `
1400 -DryRun:$false
1401
1402 $result.Success | Should -BeTrue
1403
1404 $pkgJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1405 $pkgJson.contributes.chatAgents | Should -Not -BeNullOrEmpty
1406 }
1407
1408 It 'Copies changelog when path provided' {
1409 $changelogPath = Join-Path $script:tempDir 'CHANGELOG.md'
1410 '# Changelog' | Set-Content -Path $changelogPath
1411
1412 $result = Invoke-PrepareExtension `
1413 -ExtensionDirectory $script:extDir `
1414 -RepoRoot $script:tempDir `
1415 -Channel 'Stable' `
1416 -ChangelogPath $changelogPath `
1417 -DryRun:$false
1418
1419 $result.Success | Should -BeTrue
1420 Test-Path (Join-Path $script:extDir 'CHANGELOG.md') | Should -BeTrue
1421 }
1422
1423 It 'Fails when package template is missing' {
1424 $badRoot = Join-Path $TestDrive 'bad-template-root'
1425 $badExtDir = Join-Path $badRoot 'extension'
1426 New-Item -ItemType Directory -Path $badExtDir -Force | Out-Null
1427 New-Item -ItemType Directory -Path (Join-Path $badRoot 'collections') -Force | Out-Null
1428 New-Item -ItemType Directory -Path (Join-Path $badRoot '.github/agents') -Force | Out-Null
1429 @"
1430id: test
1431"@ | Set-Content -Path (Join-Path $badRoot 'collections/test.collection.yml')
1432
1433 $result = Invoke-PrepareExtension `
1434 -ExtensionDirectory $badExtDir `
1435 -RepoRoot $badRoot `
1436 -Channel 'Stable'
1437
1438 $result.Success | Should -BeFalse
1439 $result.ErrorMessage | Should -Match 'Package generation failed'
1440 }
1441
1442 It 'Fails when no collection YAML files exist' {
1443 $emptyRoot = Join-Path $TestDrive 'empty-collections-root'
1444 $emptyExtDir = Join-Path $emptyRoot 'extension'
1445 New-Item -ItemType Directory -Path $emptyExtDir -Force | Out-Null
1446 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'collections') -Force | Out-Null
1447 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'extension/templates') -Force | Out-Null
1448 New-Item -ItemType Directory -Path (Join-Path $emptyRoot '.github/agents') -Force | Out-Null
1449 @{ name = 'test'; version = '1.0.0' } | ConvertTo-Json | Set-Content -Path (Join-Path $emptyRoot 'extension/templates/package.template.json')
1450
1451 $result = Invoke-PrepareExtension `
1452 -ExtensionDirectory $emptyExtDir `
1453 -RepoRoot $emptyRoot `
1454 -Channel 'Stable'
1455
1456 $result.Success | Should -BeFalse
1457 $result.ErrorMessage | Should -Match 'Package generation failed'
1458 }
1459
1460 Context 'Collection template copy' {
1461 BeforeAll {
1462 # Developer collection manifest (in collections/ for generation)
1463 $script:devCollectionYaml = Join-Path $script:collectionsDir 'developer.collection.yml'
1464 @"
1465id: developer
1466name: hve-developer
1467displayName: HVE Core - Developer Edition
1468description: Developer edition
1469"@ | Set-Content -Path $script:devCollectionYaml
1470 $script:devCollectionPath = $script:devCollectionYaml
1471
1472 # hve-core collection manifest (flagship, skips template copy)
1473 $script:coreCollectionPath = Join-Path $script:tempDir 'hve-core.collection.yml'
1474 @"
1475id: hve-core
1476name: HVE Core
1477displayName: HVE Core
1478description: Flagship collection
1479"@ | Set-Content -Path $script:coreCollectionPath
1480
1481 # Collection manifest referencing a missing template
1482 $script:missingCollectionPath = Join-Path $script:tempDir 'nonexistent.collection.yml'
1483 @"
1484id: nonexistent
1485name: nonexistent
1486displayName: Nonexistent
1487description: Missing template
1488"@ | Set-Content -Path $script:missingCollectionPath
1489
1490 }
1491
1492 AfterEach {
1493 # Clean up backup files left by collection template copy
1494 $bakPath = Join-Path $script:extDir 'package.json.bak'
1495 if (Test-Path $bakPath) {
1496 Remove-Item -Path $bakPath -Force
1497 }
1498 }
1499
1500 It 'Skips template copy when no collection specified' {
1501 $result = Invoke-PrepareExtension `
1502 -ExtensionDirectory $script:extDir `
1503 -RepoRoot $script:tempDir `
1504 -Channel 'Stable' `
1505 -DryRun
1506
1507 $result.Success | Should -BeTrue
1508 # package.json should contain the generated hve-core content (not a collection template)
1509 $currentJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1510 $currentJson.name | Should -Be 'hve-core'
1511 Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse
1512 }
1513
1514 It 'Skips template copy for hve-core collection' {
1515 $result = Invoke-PrepareExtension `
1516 -ExtensionDirectory $script:extDir `
1517 -RepoRoot $script:tempDir `
1518 -Channel 'Stable' `
1519 -Collection $script:coreCollectionPath `
1520 -DryRun
1521
1522 $result.Success | Should -BeTrue
1523 Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse
1524 }
1525
1526 It 'Returns error when collection template file missing' {
1527 $result = Invoke-PrepareExtension `
1528 -ExtensionDirectory $script:extDir `
1529 -RepoRoot $script:tempDir `
1530 -Channel 'Stable' `
1531 -Collection $script:missingCollectionPath `
1532 -DryRun
1533
1534 $result.Success | Should -BeFalse
1535 $result.ErrorMessage | Should -Match 'Collection template not found'
1536 }
1537
1538 It 'Copies template to package.json for non-default collection' {
1539 $result = Invoke-PrepareExtension `
1540 -ExtensionDirectory $script:extDir `
1541 -RepoRoot $script:tempDir `
1542 -Channel 'Stable' `
1543 -Collection $script:devCollectionPath `
1544 -DryRun
1545
1546 $result.Success | Should -BeTrue
1547 $updatedJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1548 $updatedJson.name | Should -Be 'hve-developer'
1549 }
1550
1551 It 'Creates package.json.bak backup before template copy' {
1552 $result = Invoke-PrepareExtension `
1553 -ExtensionDirectory $script:extDir `
1554 -RepoRoot $script:tempDir `
1555 -Channel 'Stable' `
1556 -Collection $script:devCollectionPath `
1557 -DryRun
1558
1559 $result.Success | Should -BeTrue
1560 $bakPath = Join-Path $script:extDir 'package.json.bak'
1561 Test-Path $bakPath | Should -BeTrue
1562 # Backup should contain the hve-core (flagship) generated content
1563 $bakJson = Get-Content -Path $bakPath -Raw | ConvertFrom-Json
1564 $bakJson.name | Should -Be 'hve-core'
1565 }
1566 }
1567
1568 Context 'Collection maturity gating' {
1569 BeforeAll {
1570 # Deprecated collection in collections/ directory for generation
1571 $script:deprecatedCollectionPath = Join-Path $script:collectionsDir 'deprecated-coll.collection.yml'
1572 @"
1573id: deprecated-coll
1574name: deprecated-ext
1575displayName: Deprecated Collection
1576description: Deprecated collection for testing
1577maturity: deprecated
1578"@ | Set-Content -Path $script:deprecatedCollectionPath
1579
1580 # Experimental collection in collections/ directory for generation
1581 $script:experimentalCollectionPath = Join-Path $script:collectionsDir 'experimental-coll.collection.yml'
1582 @"
1583id: experimental-coll
1584name: experimental-ext
1585displayName: Experimental Collection
1586description: Experimental collection for testing
1587maturity: experimental
1588"@ | Set-Content -Path $script:experimentalCollectionPath
1589 }
1590
1591 It 'Returns early success for deprecated collection on Stable channel' {
1592 $result = Invoke-PrepareExtension `
1593 -ExtensionDirectory $script:extDir `
1594 -RepoRoot $script:tempDir `
1595 -Channel 'Stable' `
1596 -Collection $script:deprecatedCollectionPath `
1597 -DryRun
1598
1599 $result.Success | Should -BeTrue
1600 $result.AgentCount | Should -Be 0
1601 }
1602
1603 It 'Returns early success for deprecated collection on PreRelease channel' {
1604 $result = Invoke-PrepareExtension `
1605 -ExtensionDirectory $script:extDir `
1606 -RepoRoot $script:tempDir `
1607 -Channel 'PreRelease' `
1608 -Collection $script:deprecatedCollectionPath `
1609 -DryRun
1610
1611 $result.Success | Should -BeTrue
1612 $result.AgentCount | Should -Be 0
1613 }
1614
1615 It 'Returns early success for experimental collection on Stable channel' {
1616 $result = Invoke-PrepareExtension `
1617 -ExtensionDirectory $script:extDir `
1618 -RepoRoot $script:tempDir `
1619 -Channel 'Stable' `
1620 -Collection $script:experimentalCollectionPath `
1621 -DryRun
1622
1623 $result.Success | Should -BeTrue
1624 $result.AgentCount | Should -Be 0
1625 }
1626
1627 It 'Processes experimental collection on PreRelease channel' {
1628 $result = Invoke-PrepareExtension `
1629 -ExtensionDirectory $script:extDir `
1630 -RepoRoot $script:tempDir `
1631 -Channel 'PreRelease' `
1632 -Collection $script:experimentalCollectionPath `
1633 -DryRun
1634
1635 $result.Success | Should -BeTrue
1636 $result.ErrorMessage | Should -Be ''
1637 }
1638 }
1639
1640 Context 'Exclusion reporting and skill filtering' {
1641 BeforeAll {
1642 # Add root-level repo-specific files to trigger exclusion messages
1643 @'
1644---
1645description: "Root-level agent"
1646---
1647'@ | Set-Content -Path (Join-Path $script:agentsDir 'root-agent.agent.md')
1648
1649 @'
1650---
1651description: "Root-level prompt"
1652---
1653'@ | Set-Content -Path (Join-Path $script:promptsDir 'root-prompt.prompt.md')
1654
1655 @'
1656---
1657description: "Root-level instruction"
1658applyTo: "**/*.ps1"
1659---
1660'@ | Set-Content -Path (Join-Path $script:instrDir 'root-instr.instructions.md')
1661
1662 # Add skills directory with skill in subdirectory
1663 $script:skillsDir = Join-Path $script:ghDir 'skills'
1664 $script:skillSubDir = Join-Path $script:skillsDir 'test-collection/test-skill'
1665 New-Item -ItemType Directory -Path $script:skillSubDir -Force | Out-Null
1666 @'
1667---
1668name: test-skill
1669description: "Test skill"
1670---
1671# Skill
1672'@ | Set-Content -Path (Join-Path $script:skillSubDir 'SKILL.md')
1673
1674 # Add root-level skill
1675 $rootSkillDir = Join-Path $script:skillsDir 'root-skill'
1676 New-Item -ItemType Directory -Path $rootSkillDir -Force | Out-Null
1677 @'
1678---
1679name: root-skill
1680description: "Root-level skill"
1681---
1682# Root Skill
1683'@ | Set-Content -Path (Join-Path $rootSkillDir 'SKILL.md')
1684
1685 # Restore valid package.json and template
1686 @'
1687{
1688 "name": "hve-core",
1689 "displayName": "HVE Core",
1690 "version": "1.2.3",
1691 "description": "Test extension",
1692 "publisher": "test-pub",
1693 "engines": { "vscode": "^1.80.0" },
1694 "contributes": {}
1695}
1696'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json')
1697 }
1698
1699 It 'Reports skipped items when root-level repo-specific files exist' {
1700 $result = Invoke-PrepareExtension `
1701 -ExtensionDirectory $script:extDir `
1702 -RepoRoot $script:tempDir `
1703 -Channel 'Stable' `
1704 -DryRun
1705
1706 $result.Success | Should -BeTrue
1707 $result.AgentCount | Should -BeGreaterOrEqual 1
1708 $result.SkillCount | Should -BeGreaterOrEqual 1
1709 }
1710
1711 It 'Filters skills by collection membership' {
1712 $collectionPath = Join-Path $script:tempDir 'skill-filter.collection.yml'
1713 @"
1714id: hve-core
1715name: HVE Core
1716displayName: HVE Core
1717description: Skill filtering test
1718items:
1719 - kind: agent
1720 path: .github/agents/test-collection/test.agent.md
1721 maturity: stable
1722 - kind: skill
1723 path: .github/skills/test-collection/test-skill/
1724 maturity: stable
1725"@ | Set-Content -Path $collectionPath
1726
1727 $result = Invoke-PrepareExtension `
1728 -ExtensionDirectory $script:extDir `
1729 -RepoRoot $script:tempDir `
1730 -Channel 'Stable' `
1731 -Collection $collectionPath `
1732 -DryRun
1733
1734 $result.Success | Should -BeTrue
1735 $result.SkillCount | Should -Be 1
1736 }
1737
1738 It 'Shows DryRun message when changelog provided with DryRun' {
1739 $changelogPath = Join-Path $script:tempDir 'CHANGELOG-DRYRUN.md'
1740 '# DryRun Changelog' | Set-Content -Path $changelogPath
1741
1742 $result = Invoke-PrepareExtension `
1743 -ExtensionDirectory $script:extDir `
1744 -RepoRoot $script:tempDir `
1745 -Channel 'Stable' `
1746 -ChangelogPath $changelogPath `
1747 -DryRun
1748
1749 $result.Success | Should -BeTrue
1750 }
1751 }
1752}
1753
1754#region Additional Coverage Tests
1755
1756Describe 'Get-ArtifactDescription' {
1757 BeforeAll {
1758 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1759 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
1760 }
1761
1762 AfterAll {
1763 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1764 }
1765
1766 It 'Returns empty string when file does not exist' {
1767 $result = Get-ArtifactDescription -FilePath (Join-Path $script:tempDir 'nonexistent.md')
1768 $result | Should -Be ''
1769 }
1770
1771 It 'Returns empty string when file has no frontmatter' {
1772 $path = Join-Path $script:tempDir 'no-frontmatter.md'
1773 '# Just a heading' | Set-Content -Path $path
1774 $result = Get-ArtifactDescription -FilePath $path
1775 $result | Should -Be ''
1776 }
1777
1778 It 'Returns empty string when frontmatter has no description' {
1779 $path = Join-Path $script:tempDir 'no-desc.md'
1780 @"
1781---
1782applyTo: "**/*.ps1"
1783---
1784# No description
1785"@ | Set-Content -Path $path
1786 $result = Get-ArtifactDescription -FilePath $path
1787 $result | Should -Be ''
1788 }
1789
1790 It 'Returns description from valid frontmatter' {
1791 $path = Join-Path $script:tempDir 'valid.md'
1792 @"
1793---
1794description: "My artifact description"
1795---
1796# Valid
1797"@ | Set-Content -Path $path
1798 $result = Get-ArtifactDescription -FilePath $path
1799 $result | Should -Be 'My artifact description'
1800 }
1801
1802 It 'Strips branding suffix from description' {
1803 $path = Join-Path $script:tempDir 'branded.md'
1804 @"
1805---
1806description: "Some tool - Brought to you by microsoft/hve-core"
1807---
1808# Branded
1809"@ | Set-Content -Path $path
1810 $result = Get-ArtifactDescription -FilePath $path
1811 $result | Should -Be 'Some tool'
1812 }
1813
1814 It 'Returns empty string when frontmatter YAML is invalid' {
1815 $path = Join-Path $script:tempDir 'bad-yaml.md'
1816 @"
1817---
1818description: [invalid: yaml: :
1819---
1820# Bad
1821"@ | Set-Content -Path $path
1822 $result = Get-ArtifactDescription -FilePath $path
1823 $result | Should -Be ''
1824 }
1825}
1826
1827Describe 'Get-CollectionArtifactKey - default branch' {
1828 It 'Handles unknown kind with matching suffix' {
1829 $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/my-file.custom.md'
1830 $result | Should -Be 'my-file'
1831 }
1832
1833 It 'Handles unknown kind with .md extension but no matching suffix' {
1834 $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/readme.md'
1835 $result | Should -Be 'readme'
1836 }
1837
1838 It 'Handles unknown kind with non-md file' {
1839 $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/config.json'
1840 $result | Should -Be 'config.json'
1841 }
1842}
1843
1844Describe 'Test-TemplateConsistency' {
1845 BeforeAll {
1846 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1847 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
1848 }
1849
1850 AfterAll {
1851 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1852 }
1853
1854 It 'Returns inconsistent when template file not found' {
1855 $manifest = @{ name = 'test'; displayName = 'Test'; description = 'Desc' }
1856 $result = Test-TemplateConsistency -TemplatePath (Join-Path $script:tempDir 'nonexistent.json') -CollectionManifest $manifest
1857 $result.IsConsistent | Should -BeFalse
1858 $result.Mismatches.Count | Should -Be 1
1859 $result.Mismatches[0].Field | Should -Be 'file'
1860 $result.Mismatches[0].Message | Should -Match 'not found'
1861 }
1862
1863 It 'Returns inconsistent when template is invalid JSON' {
1864 $badPath = Join-Path $script:tempDir 'bad-template.json'
1865 'not valid json {{{' | Set-Content -Path $badPath
1866 $manifest = @{ name = 'test' }
1867 $result = Test-TemplateConsistency -TemplatePath $badPath -CollectionManifest $manifest
1868 $result.IsConsistent | Should -BeFalse
1869 $result.Mismatches[0].Message | Should -Match 'Failed to parse'
1870 }
1871
1872 It 'Returns consistent when fields match' {
1873 $path = Join-Path $script:tempDir 'matching.json'
1874 @{ name = 'hve-rpi'; displayName = 'HVE RPI'; description = 'RPI tools' } | ConvertTo-Json | Set-Content -Path $path
1875 $manifest = @{ name = 'hve-rpi'; displayName = 'HVE RPI'; description = 'RPI tools' }
1876 $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest
1877 $result.IsConsistent | Should -BeTrue
1878 $result.Mismatches.Count | Should -Be 0
1879 }
1880
1881 It 'Reports mismatches for diverging fields' {
1882 $path = Join-Path $script:tempDir 'diverging.json'
1883 @{ name = 'old-name'; displayName = 'Old Name'; description = 'Old desc' } | ConvertTo-Json | Set-Content -Path $path
1884 $manifest = @{ name = 'new-name'; displayName = 'New Name'; description = 'New desc' }
1885 $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest
1886 $result.IsConsistent | Should -BeFalse
1887 $result.Mismatches.Count | Should -Be 3
1888 }
1889
1890 It 'Skips comparison when field missing in either side' {
1891 $path = Join-Path $script:tempDir 'partial.json'
1892 @{ name = 'test' } | ConvertTo-Json | Set-Content -Path $path
1893 $manifest = @{ displayName = 'Test Display' }
1894 $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest
1895 $result.IsConsistent | Should -BeTrue
1896 }
1897}
1898
1899Describe 'Update-PackageJsonContributes - existing contributes fields' {
1900 It 'Updates existing chatAgents field via else branch' {
1901 $packageJson = [PSCustomObject]@{
1902 name = 'test-extension'
1903 contributes = [PSCustomObject]@{
1904 chatAgents = @(@{ path = './old.agent.md' })
1905 chatPromptFiles = @(@{ path = './old.prompt.md' })
1906 chatInstructions = @(@{ path = './old.instr.md' })
1907 chatSkills = @(@{ path = './old.skill' })
1908 }
1909 }
1910 $agents = @(@{ name = 'new-agent'; path = './.github/agents/new.agent.md' })
1911 $prompts = @(@{ name = 'new-prompt'; path = './.github/prompts/new.prompt.md' })
1912 $instructions = @(@{ name = 'new-instr'; path = './.github/instructions/new.instructions.md' })
1913 $skills = @(@{ name = 'new-skill'; path = './.github/skills/new-skill' })
1914
1915 $result = Update-PackageJsonContributes -PackageJson $packageJson `
1916 -ChatAgents $agents `
1917 -ChatPromptFiles $prompts `
1918 -ChatInstructions $instructions `
1919 -ChatSkills $skills
1920
1921 $result.contributes.chatAgents[0].path | Should -Be './.github/agents/new.agent.md'
1922 $result.contributes.chatPromptFiles[0].path | Should -Be './.github/prompts/new.prompt.md'
1923 $result.contributes.chatInstructions[0].path | Should -Be './.github/instructions/new.instructions.md'
1924 $result.contributes.chatSkills[0].path | Should -Be './.github/skills/new-skill'
1925 }
1926
1927 It 'Adds contributes section when missing' {
1928 $packageJson = [PSCustomObject]@{
1929 name = 'bare-extension'
1930 }
1931
1932 $result = Update-PackageJsonContributes -PackageJson $packageJson `
1933 -ChatAgents @() `
1934 -ChatPromptFiles @() `
1935 -ChatInstructions @() `
1936 -ChatSkills @()
1937
1938 $result.contributes | Should -Not -BeNullOrEmpty
1939 }
1940}
1941
1942Describe 'Resolve-HandoffDependencies - additional cases' {
1943 BeforeAll {
1944 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1945 $script:agentsDir = Join-Path $script:tempDir 'agents'
1946 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
1947
1948 # Agent with string-format handoffs
1949 @'
1950---
1951description: "String handoff agent"
1952handoffs:
1953 - string-target
1954---
1955'@ | Set-Content -Path (Join-Path $script:agentsDir 'string-handoff.agent.md')
1956
1957 @'
1958---
1959description: "String target"
1960---
1961'@ | Set-Content -Path (Join-Path $script:agentsDir 'string-target.agent.md')
1962
1963 # Agent with broken YAML in handoffs section
1964 @'
1965---
1966description: "Broken YAML agent"
1967handoffs:
1968 - label: [invalid: yaml: :
1969---
1970'@ | Set-Content -Path (Join-Path $script:agentsDir 'broken-yaml.agent.md')
1971 }
1972
1973 AfterAll {
1974 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1975 }
1976
1977 It 'Resolves string-format handoffs' {
1978 $result = Resolve-HandoffDependencies -SeedAgents @('string-handoff') -AgentsDir $script:agentsDir
1979 $result | Should -Contain 'string-handoff'
1980 $result | Should -Contain 'string-target'
1981 }
1982
1983 It 'Warns but continues when handoff target file is missing' {
1984 $result = Resolve-HandoffDependencies -SeedAgents @('missing-agent') -AgentsDir $script:agentsDir 3>&1
1985 # The function emits a warning and returns the seed agent
1986 $agentNames = @($result | Where-Object { $_ -is [string] })
1987 $agentNames | Should -Contain 'missing-agent'
1988 }
1989
1990 It 'Warns and continues when handoff YAML is malformed' {
1991 $result = Resolve-HandoffDependencies -SeedAgents @('broken-yaml') -AgentsDir $script:agentsDir 3>&1
1992 $warnings = @($result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] })
1993 $warnings.Count | Should -BeGreaterOrEqual 1
1994 $agentNames = @($result | Where-Object { $_ -is [string] })
1995 $agentNames | Should -Contain 'broken-yaml'
1996 }
1997}
1998
1999Describe 'Get-DiscoveredPrompts - maturity filtering' {
2000 BeforeAll {
2001 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2002 $script:promptsDir = Join-Path $script:tempDir 'prompts'
2003 $script:promptsSubDir = Join-Path $script:promptsDir 'test-collection'
2004 $script:ghDir = Join-Path $script:tempDir '.github'
2005 New-Item -ItemType Directory -Path $script:promptsSubDir -Force | Out-Null
2006 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
2007
2008 @'
2009---
2010description: "Stable prompt"
2011---
2012'@ | Set-Content -Path (Join-Path $script:promptsSubDir 'stable.prompt.md')
2013 }
2014
2015 AfterAll {
2016 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2017 }
2018
2019 It 'Skips prompts when none match allowed maturities' {
2020 $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('experimental')
2021 $result.Prompts.Count | Should -Be 0
2022 $result.Skipped.Count | Should -Be 1
2023 $result.Skipped[0].Reason | Should -Match 'maturity'
2024 }
2025}
2026
2027Describe 'Get-DiscoveredInstructions - maturity filtering' {
2028 BeforeAll {
2029 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2030 $script:instrDir = Join-Path $script:tempDir 'instructions'
2031 $script:instrSubDir = Join-Path $script:instrDir 'test-collection'
2032 $script:ghDir = Join-Path $script:tempDir '.github'
2033 New-Item -ItemType Directory -Path $script:instrSubDir -Force | Out-Null
2034 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
2035
2036 @'
2037---
2038description: "Test instruction"
2039applyTo: "**/*.ps1"
2040---
2041'@ | Set-Content -Path (Join-Path $script:instrSubDir 'test.instructions.md')
2042 }
2043
2044 AfterAll {
2045 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2046 }
2047
2048 It 'Skips instructions when none match allowed maturities' {
2049 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('experimental')
2050 $result.Instructions.Count | Should -Be 0
2051 $result.Skipped.Count | Should -Be 1
2052 $result.Skipped[0].Reason | Should -Match 'maturity'
2053 }
2054}
2055
2056Describe 'Invoke-PrepareExtension - error cases' {
2057 BeforeAll {
2058 $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString())
2059 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
2060
2061 $script:extDir = Join-Path $script:tempDir 'extension'
2062 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
2063
2064 $script:templatesDir = Join-Path $script:extDir 'templates'
2065 New-Item -ItemType Directory -Path $script:templatesDir -Force | Out-Null
2066 @'
2067{
2068 "name": "hve-core",
2069 "displayName": "HVE Core",
2070 "version": "1.0.0",
2071 "description": "Test extension",
2072 "publisher": "test-pub",
2073 "engines": { "vscode": "^1.80.0" },
2074 "contributes": {}
2075}
2076'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json')
2077
2078 $script:collectionsDir = Join-Path $script:tempDir 'collections'
2079 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
2080 @"
2081id: hve-core
2082name: HVE Core
2083displayName: HVE Core
2084description: Test
2085"@ | Set-Content -Path (Join-Path $script:collectionsDir 'hve-core.collection.yml')
2086
2087 $script:ghDir = Join-Path $script:tempDir '.github'
2088 New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'agents') -Force | Out-Null
2089 New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'prompts') -Force | Out-Null
2090 New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'instructions') -Force | Out-Null
2091 }
2092
2093 AfterAll {
2094 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2095 }
2096
2097 It 'Fails when package.json has invalid JSON' {
2098 # Write invalid JSON and mock generation to preserve it
2099 $badPkgPath = Join-Path $script:extDir 'package.json'
2100 'NOT VALID JSON' | Set-Content -Path $badPkgPath
2101
2102 Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) }
2103
2104 $result = Invoke-PrepareExtension `
2105 -ExtensionDirectory $script:extDir `
2106 -RepoRoot $script:tempDir `
2107 -Channel 'Stable'
2108
2109 $result.Success | Should -BeFalse
2110 $result.ErrorMessage | Should -Match 'Failed to parse package.json'
2111 }
2112
2113 It 'Fails when package.json lacks version field' {
2114 $badPkgPath = Join-Path $script:extDir 'package.json'
2115 @{ name = 'test-no-version' } | ConvertTo-Json | Set-Content -Path $badPkgPath
2116
2117 Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) }
2118
2119 $result = Invoke-PrepareExtension `
2120 -ExtensionDirectory $script:extDir `
2121 -RepoRoot $script:tempDir `
2122 -Channel 'Stable'
2123
2124 $result.Success | Should -BeFalse
2125 $result.ErrorMessage | Should -Match "does not contain a 'version' field"
2126 }
2127
2128 It 'Fails when version format is invalid' {
2129 $badPkgPath = Join-Path $script:extDir 'package.json'
2130 @{ name = 'test'; version = 'not-semver' } | ConvertTo-Json | Set-Content -Path $badPkgPath
2131
2132 Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) }
2133
2134 $result = Invoke-PrepareExtension `
2135 -ExtensionDirectory $script:extDir `
2136 -RepoRoot $script:tempDir `
2137 -Channel 'Stable'
2138
2139 $result.Success | Should -BeFalse
2140 $result.ErrorMessage | Should -Match 'Invalid version format'
2141 }
2142
2143 It 'Warns when changelog path specified but file not found' {
2144 $validPkgPath = Join-Path $script:extDir 'package.json'
2145 @{ name = 'test'; version = '1.0.0'; contributes = @{} } | ConvertTo-Json -Depth 5 | Set-Content -Path $validPkgPath
2146
2147 $result = Invoke-PrepareExtension `
2148 -ExtensionDirectory $script:extDir `
2149 -RepoRoot $script:tempDir `
2150 -Channel 'Stable' `
2151 -ChangelogPath (Join-Path $script:tempDir 'NONEXISTENT-CHANGELOG.md') 3>&1
2152
2153 # Filter out the result hashtable from warnings
2154 $hashtableResult = $result | Where-Object { $_ -is [hashtable] }
2155 if ($hashtableResult) {
2156 $hashtableResult.Success | Should -BeTrue
2157 }
2158 }
2159
2160 Context 'Collection with requires dependencies' {
2161 BeforeAll {
2162 $script:reqCollectionPath = Join-Path $script:tempDir 'requires-test.collection.yml'
2163 @"
2164id: hve-core
2165name: HVE Core
2166displayName: HVE Core
2167description: Requires test
2168items:
2169 - kind: agent
2170 path: .github/agents/test-collection/main.agent.md
2171 maturity: stable
2172 requires:
2173 prompts:
2174 - dep-prompt
2175 - kind: prompt
2176 path: .github/prompts/test-collection/dep-prompt.prompt.md
2177 maturity: stable
2178"@ | Set-Content -Path $script:reqCollectionPath
2179
2180 # Create required agent and prompt files in subdirectories
2181 $reqAgentDir = Join-Path $script:ghDir 'agents/test-collection'
2182 $reqPromptDir = Join-Path $script:ghDir 'prompts/test-collection'
2183 New-Item -ItemType Directory -Path $reqAgentDir -Force | Out-Null
2184 New-Item -ItemType Directory -Path $reqPromptDir -Force | Out-Null
2185 @'
2186---
2187description: "Main agent"
2188---
2189'@ | Set-Content -Path (Join-Path $reqAgentDir 'main.agent.md')
2190
2191 @'
2192---
2193description: "Dependent prompt"
2194---
2195'@ | Set-Content -Path (Join-Path $reqPromptDir 'dep-prompt.prompt.md')
2196
2197 # Restore valid package.json
2198 $validPkgPath = Join-Path $script:extDir 'package.json'
2199 @{ name = 'hve-core'; version = '1.0.0'; contributes = @{} } | ConvertTo-Json -Depth 5 | Set-Content -Path $validPkgPath
2200 }
2201
2202 It 'Resolves requires dependencies in collection' {
2203 $result = Invoke-PrepareExtension `
2204 -ExtensionDirectory $script:extDir `
2205 -RepoRoot $script:tempDir `
2206 -Channel 'Stable' `
2207 -Collection $script:reqCollectionPath `
2208 -DryRun
2209
2210 $result.Success | Should -BeTrue
2211 $result.AgentCount | Should -BeGreaterOrEqual 1
2212 $result.PromptCount | Should -BeGreaterOrEqual 1
2213 }
2214 }
2215}
2216
2217Describe 'Invoke-ExtensionCollectionsGeneration - collection manifest errors' {
2218 BeforeAll {
2219 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2220
2221 $collectionsDir = Join-Path $script:tempDir 'collections'
2222 $templatesDir = Join-Path $script:tempDir 'extension/templates'
2223 New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null
2224 New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null
2225
2226 @{
2227 name = 'hve-core'
2228 displayName = 'HVE Core'
2229 version = '1.0.0'
2230 description = 'default'
2231 publisher = 'test-pub'
2232 engines = @{ vscode = '^1.80.0' }
2233 contributes = @{}
2234 } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json')
2235 }
2236
2237 AfterAll {
2238 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2239 }
2240
2241 It 'Throws when collection id is empty' {
2242 $collectionsDir = Join-Path $script:tempDir 'collections'
2243 Remove-Item -Path "$collectionsDir/*" -Force -ErrorAction SilentlyContinue
2244 @"
2245id:
2246name: empty-id
2247"@ | Set-Content -Path (Join-Path $collectionsDir 'empty.collection.yml')
2248
2249 { Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir } | Should -Throw '*Collection id is required*'
2250 }
2251
2252 It 'Throws when collection manifest is not a hashtable' {
2253 $collectionsDir = Join-Path $script:tempDir 'collections'
2254 Remove-Item -Path "$collectionsDir/*" -Force -ErrorAction SilentlyContinue
2255 # YAML that parses as a scalar string
2256 'just a string' | Set-Content -Path (Join-Path $collectionsDir 'bad.collection.yml')
2257
2258 { Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir } | Should -Throw '*must be a hashtable*'
2259 }
2260}
2261
2262Describe 'Invoke-ExtensionCollectionsGeneration - README generation' {
2263 BeforeAll {
2264 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2265
2266 $collectionsDir = Join-Path $script:tempDir 'collections'
2267 $templatesDir = Join-Path $script:tempDir 'extension/templates'
2268 New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null
2269 New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null
2270
2271 # Package template
2272 @{
2273 name = 'hve-core'
2274 displayName = 'HVE Core'
2275 version = '1.0.0'
2276 description = 'default'
2277 publisher = 'test-pub'
2278 engines = @{ vscode = '^1.80.0' }
2279 contributes = @{}
2280 } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json')
2281
2282 # README template
2283 $repoRoot = (Get-Item "$PSScriptRoot/../../..").FullName
2284 $realTemplatePath = Join-Path $repoRoot 'extension/templates/README.template.md'
2285 if (Test-Path $realTemplatePath) {
2286 Copy-Item -Path $realTemplatePath -Destination (Join-Path $templatesDir 'README.template.md')
2287 }
2288 else {
2289 @"
2290# {{DISPLAY_NAME}}
2291
2292> {{DESCRIPTION}}
2293
2294{{BODY}}
2295
2296{{ARTIFACTS}}
2297
2298{{FULL_EDITION}}
2299"@ | Set-Content -Path (Join-Path $templatesDir 'README.template.md')
2300 }
2301
2302 # Collection with a .collection.md body file
2303 @"
2304id: readme-test
2305name: README Test
2306displayName: HVE Core - README Test
2307description: Test readme generation
2308"@ | Set-Content -Path (Join-Path $collectionsDir 'readme-test.collection.yml')
2309
2310 'Body content for readme test.' | Set-Content -Path (Join-Path $collectionsDir 'readme-test.collection.md')
2311
2312 # hve-core needed for the defaults
2313 @"
2314id: hve-core
2315name: HVE Core
2316displayName: HVE Core
2317description: All artifacts
2318"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core.collection.yml')
2319
2320 'HVE Core body content.' | Set-Content -Path (Join-Path $collectionsDir 'hve-core.collection.md')
2321
2322 # hve-core-all collection with body
2323 @"
2324id: hve-core-all
2325name: All
2326displayName: HVE Core - All
2327description: All combined
2328"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.yml')
2329
2330 'HVE Core All body content.' | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.md')
2331
2332 # Collection without .collection.md body
2333 @"
2334id: no-readme
2335name: No README
2336displayName: HVE Core - No README
2337description: Collection without body
2338"@ | Set-Content -Path (Join-Path $collectionsDir 'no-readme.collection.yml')
2339 }
2340
2341 AfterAll {
2342 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2343 }
2344
2345 It 'Generates README files for collections with .collection.md' {
2346 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2347 $readmePath = Join-Path $script:tempDir 'extension/README.readme-test.md'
2348 Test-Path $readmePath | Should -BeTrue
2349 $content = Get-Content -Path $readmePath -Raw
2350 $content | Should -Match 'Body content for readme test'
2351 }
2352
2353 It 'Generates README.md for hve-core collection' {
2354 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2355 $readmePath = Join-Path $script:tempDir 'extension/README.md'
2356 Test-Path $readmePath | Should -BeTrue
2357 $content = Get-Content -Path $readmePath -Raw
2358 $content | Should -Match 'HVE Core body content'
2359 }
2360
2361 It 'Generates README for hve-core-all collection' {
2362 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2363 $readmePath = Join-Path $script:tempDir 'extension/README.hve-core-all.md'
2364 Test-Path $readmePath | Should -BeTrue
2365 $content = Get-Content -Path $readmePath -Raw
2366 $content | Should -Match 'HVE Core All body content'
2367 }
2368
2369 It 'Skips README generation when .collection.md is missing' {
2370 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2371 $readmePath = Join-Path $script:tempDir 'extension/README.no-readme.md'
2372 Test-Path $readmePath | Should -BeFalse
2373 }
2374}
2375
2376#region Deprecated Path Exclusion Tests
2377
2378Describe 'Get-DiscoveredAgents - deprecated path exclusion' {
2379 BeforeAll {
2380 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2381 $script:agentsDir = Join-Path $script:tempDir 'agents'
2382 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
2383
2384 # Create active agent
2385 $activeDir = Join-Path $script:agentsDir 'rpi'
2386 New-Item -ItemType Directory -Path $activeDir -Force | Out-Null
2387 @'
2388---
2389description: "Active agent"
2390---
2391'@ | Set-Content -Path (Join-Path $activeDir 'active.agent.md')
2392
2393 # Create deprecated agent
2394 $deprecatedDir = Join-Path $script:agentsDir 'deprecated'
2395 New-Item -ItemType Directory -Path $deprecatedDir -Force | Out-Null
2396 @'
2397---
2398description: "Deprecated agent"
2399---
2400'@ | Set-Content -Path (Join-Path $deprecatedDir 'old.agent.md')
2401 }
2402
2403 AfterAll {
2404 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2405 }
2406
2407 It 'Excludes agents in deprecated directory' {
2408 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -ExcludedAgents @()
2409 $agentNames = $result.Agents | ForEach-Object { $_.name }
2410 $agentNames | Should -Contain 'active'
2411 $agentNames | Should -Not -Contain 'old'
2412 }
2413}
2414
2415Describe 'Get-DiscoveredPrompts - deprecated path exclusion' {
2416 BeforeAll {
2417 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2418 $script:promptsDir = Join-Path $script:tempDir 'prompts'
2419 $script:ghDir = Join-Path $script:tempDir '.github'
2420 New-Item -ItemType Directory -Path $script:promptsDir -Force | Out-Null
2421 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
2422
2423 # Create active prompt
2424 $activeDir = Join-Path $script:promptsDir 'rpi'
2425 New-Item -ItemType Directory -Path $activeDir -Force | Out-Null
2426 @'
2427---
2428description: "Active prompt"
2429---
2430'@ | Set-Content -Path (Join-Path $activeDir 'active.prompt.md')
2431
2432 # Create deprecated prompt
2433 $deprecatedDir = Join-Path $script:promptsDir 'deprecated'
2434 New-Item -ItemType Directory -Path $deprecatedDir -Force | Out-Null
2435 @'
2436---
2437description: "Deprecated prompt"
2438---
2439'@ | Set-Content -Path (Join-Path $deprecatedDir 'old.prompt.md')
2440 }
2441
2442 AfterAll {
2443 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2444 }
2445
2446 It 'Excludes prompts in deprecated directory' {
2447 $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
2448 $promptNames = $result.Prompts | ForEach-Object { $_.name }
2449 $promptNames | Should -Contain 'active'
2450 $promptNames | Should -Not -Contain 'old'
2451 }
2452}
2453
2454Describe 'Get-DiscoveredInstructions - deprecated path exclusion' {
2455 BeforeAll {
2456 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2457 $script:instrDir = Join-Path $script:tempDir 'instructions'
2458 $script:ghDir = Join-Path $script:tempDir '.github'
2459 New-Item -ItemType Directory -Path $script:instrDir -Force | Out-Null
2460 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
2461
2462 # Create active instruction
2463 $activeDir = Join-Path $script:instrDir 'rpi'
2464 New-Item -ItemType Directory -Path $activeDir -Force | Out-Null
2465 @'
2466---
2467description: "Active instruction"
2468applyTo: "**/*.ps1"
2469---
2470'@ | Set-Content -Path (Join-Path $activeDir 'active.instructions.md')
2471
2472 # Create deprecated instruction
2473 $deprecatedDir = Join-Path $script:instrDir 'deprecated'
2474 New-Item -ItemType Directory -Path $deprecatedDir -Force | Out-Null
2475 @'
2476---
2477description: "Deprecated instruction"
2478applyTo: "**/*.ps1"
2479---
2480'@ | Set-Content -Path (Join-Path $deprecatedDir 'old.instructions.md')
2481 }
2482
2483 AfterAll {
2484 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2485 }
2486
2487 It 'Excludes instructions in deprecated directory' {
2488 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
2489 $instrNames = $result.Instructions | ForEach-Object { $_.name }
2490 $instrNames | Should -Contain 'active-instructions'
2491 $instrNames | Should -Not -Contain 'old-instructions'
2492 }
2493}
2494
2495Describe 'Get-DiscoveredSkills - deprecated path exclusion' {
2496 BeforeAll {
2497 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2498 $script:skillsDir = Join-Path $script:tempDir 'skills'
2499 New-Item -ItemType Directory -Path $script:skillsDir -Force | Out-Null
2500
2501 # Create active skill
2502 $activeSkillDir = Join-Path $script:skillsDir 'experimental/good-skill'
2503 New-Item -ItemType Directory -Path $activeSkillDir -Force | Out-Null
2504 @'
2505---
2506name: good-skill
2507description: "Active skill"
2508---
2509'@ | Set-Content -Path (Join-Path $activeSkillDir 'SKILL.md')
2510
2511 # Create deprecated skill
2512 $deprecatedSkillDir = Join-Path $script:skillsDir 'deprecated/old-skill'
2513 New-Item -ItemType Directory -Path $deprecatedSkillDir -Force | Out-Null
2514 @'
2515---
2516name: old-skill
2517description: "Deprecated skill"
2518---
2519'@ | Set-Content -Path (Join-Path $deprecatedSkillDir 'SKILL.md')
2520 }
2521
2522 AfterAll {
2523 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2524 }
2525
2526 It 'Excludes skills in deprecated directory' {
2527 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
2528 $skillNames = $result.Skills | ForEach-Object { $_.name }
2529 $skillNames | Should -Contain 'good-skill'
2530 $skillNames | Should -Not -Contain 'old-skill'
2531 }
2532}
2533
2534#endregion Deprecated Path Exclusion Tests
2535
2536#region Maturity Notice Tests
2537
2538Describe 'New-CollectionReadme - maturity notice' {
2539 BeforeAll {
2540 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2541 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
2542
2543 # Create minimal README template with all tokens including MATURITY_NOTICE
2544 $templateContent = @"
2545# {{DISPLAY_NAME}}
2546
2547> {{DESCRIPTION}}
2548
2549{{MATURITY_NOTICE}}
2550
2551{{BODY}}
2552
2553## Included Artifacts
2554
2555{{ARTIFACTS}}
2556
2557{{FULL_EDITION}}
2558"@
2559 $script:templatePath = Join-Path $script:tempDir 'README.template.md'
2560 Set-Content -Path $script:templatePath -Value $templateContent
2561
2562 # Create collection body markdown
2563 $script:bodyPath = Join-Path $script:tempDir 'test.collection.md'
2564 Set-Content -Path $script:bodyPath -Value 'Collection body content.'
2565 }
2566
2567 AfterAll {
2568 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2569 }
2570
2571 It 'Includes experimental notice for experimental collection' {
2572 $collection = @{
2573 id = 'test-exp'
2574 name = 'Test Experimental'
2575 description = 'An experimental collection'
2576 maturity = 'experimental'
2577 items = @()
2578 }
2579 $outputPath = Join-Path $script:tempDir 'README-exp.md'
2580 New-CollectionReadme -Collection $collection -CollectionMdPath $script:bodyPath `
2581 -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outputPath
2582
2583 $content = Get-Content -Path $outputPath -Raw
2584 $content | Should -Match '\u26A0' # warning sign emoji
2585 $content | Should -Match 'Pre-Release channel'
2586 }
2587
2588 It 'Has no notice for collection without maturity field' {
2589 $collection = @{
2590 id = 'test-default'
2591 name = 'Test Default'
2592 description = 'A default collection'
2593 items = @()
2594 }
2595 $outputPath = Join-Path $script:tempDir 'README-default.md'
2596 New-CollectionReadme -Collection $collection -CollectionMdPath $script:bodyPath `
2597 -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outputPath
2598
2599 $content = Get-Content -Path $outputPath -Raw
2600 $content | Should -Not -Match '\u26A0'
2601 }
2602
2603 It 'Has no notice for explicit stable maturity' {
2604 $collection = @{
2605 id = 'test-stable'
2606 name = 'Test Stable'
2607 description = 'A stable collection'
2608 maturity = 'stable'
2609 items = @()
2610 }
2611 $outputPath = Join-Path $script:tempDir 'README-stable.md'
2612 New-CollectionReadme -Collection $collection -CollectionMdPath $script:bodyPath `
2613 -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outputPath
2614
2615 $content = Get-Content -Path $outputPath -Raw
2616 $content | Should -Not -Match '\u26A0'
2617 }
2618}
2619
2620#endregion Maturity Notice Tests
2621
2622#endregion Additional Coverage Tests
2623