microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
docs/621-ai-artifacts

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

2103lines · 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-all collection
147 @"
148id: hve-core-all
149name: hve-core
150displayName: HVE Core
151description: All artifacts
152"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.yml')
153
154 # rpi collection
155 @"
156id: rpi
157name: RPI Workflow
158displayName: HVE Core - RPI Workflow
159description: RPI workflow agents
160"@ | Set-Content -Path (Join-Path $collectionsDir 'rpi.collection.yml')
161 }
162
163 AfterAll {
164 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
165 }
166
167 It 'Generates package.json for hve-core-all' {
168 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
169 $pkgPath = Join-Path $script:tempDir 'extension/package.json'
170 Test-Path $pkgPath | Should -BeTrue
171 $pkg = Get-Content $pkgPath -Raw | ConvertFrom-Json
172 $pkg.name | Should -Be 'hve-core'
173 $pkg.version | Should -Be '2.0.0'
174 }
175
176 It 'Generates collection package file for non-default collection' {
177 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
178 $pkgPath = Join-Path $script:tempDir 'extension/package.rpi.json'
179 Test-Path $pkgPath | Should -BeTrue
180 $pkg = Get-Content $pkgPath -Raw | ConvertFrom-Json
181 $pkg.name | Should -Be 'hve-rpi'
182 $pkg.displayName | Should -Be 'HVE Core - RPI Workflow'
183 }
184
185 It 'Returns array of generated file paths' {
186 $result = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
187 $result.Count | Should -Be 2
188 }
189
190 It 'Propagates version from template to all generated files' {
191 $result = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
192 foreach ($file in $result) {
193 $pkg = Get-Content $file -Raw | ConvertFrom-Json
194 $pkg.version | Should -Be '2.0.0'
195 }
196 }
197
198 It 'Removes stale collection files not matching current collections' {
199 $staleFile = Join-Path $script:tempDir 'extension/package.obsolete.json'
200 '{}' | Set-Content -Path $staleFile
201
202 Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
203
204 Test-Path $staleFile | Should -BeFalse
205 }
206
207 It 'Throws when package template is missing' {
208 $badRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
209 New-Item -ItemType Directory -Path (Join-Path $badRoot 'collections') -Force | Out-Null
210 New-Item -ItemType Directory -Path (Join-Path $badRoot 'extension/templates') -Force | Out-Null
211 @"
212id: test
213"@ | Set-Content -Path (Join-Path $badRoot 'collections/test.collection.yml')
214
215 { Invoke-ExtensionCollectionsGeneration -RepoRoot $badRoot } | Should -Throw '*Package template not found*'
216
217 Remove-Item -Path $badRoot -Recurse -Force -ErrorAction SilentlyContinue
218 }
219
220 It 'Throws when no collection files exist' {
221 $emptyRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
222 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'collections') -Force | Out-Null
223 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'extension/templates') -Force | Out-Null
224 @{ name = 'test'; version = '1.0.0' } | ConvertTo-Json | Set-Content -Path (Join-Path $emptyRoot 'extension/templates/package.template.json')
225
226 { Invoke-ExtensionCollectionsGeneration -RepoRoot $emptyRoot } | Should -Throw '*No root collection files found*'
227
228 Remove-Item -Path $emptyRoot -Recurse -Force -ErrorAction SilentlyContinue
229 }
230}
231
232Describe 'New-CollectionReadme' {
233 BeforeAll {
234 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
235 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
236
237 # Resolve the real template from the repo
238 $script:repoRoot = (Get-Item "$PSScriptRoot/../../..").FullName
239 $script:templatePath = Join-Path $script:repoRoot 'extension/templates/README.template.md'
240
241 # Create mock artifact files with frontmatter descriptions
242 $agentsDir = Join-Path $script:tempDir '.github/agents'
243 $promptsDir = Join-Path $script:tempDir '.github/prompts'
244 $instrDir = Join-Path $script:tempDir '.github/instructions'
245 $skillsDir = Join-Path $script:tempDir '.github/skills/my-skill'
246 New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null
247 New-Item -ItemType Directory -Path $promptsDir -Force | Out-Null
248 New-Item -ItemType Directory -Path $instrDir -Force | Out-Null
249 New-Item -ItemType Directory -Path $skillsDir -Force | Out-Null
250
251 @"
252---
253description: "Alpha agent description"
254---
255# Alpha
256"@ | Set-Content -Path (Join-Path $agentsDir 'alpha.agent.md')
257
258 @"
259---
260description: "Zebra agent description"
261---
262# Zebra
263"@ | Set-Content -Path (Join-Path $agentsDir 'zebra.agent.md')
264
265 @"
266---
267description: "My prompt description"
268---
269# Prompt
270"@ | Set-Content -Path (Join-Path $promptsDir 'my-prompt.prompt.md')
271
272 @"
273---
274description: "My instruction description"
275applyTo: "**/*.ps1"
276---
277# Instruction
278"@ | Set-Content -Path (Join-Path $instrDir 'my-instr.instructions.md')
279
280 @"
281---
282name: my-skill
283description: "My skill description"
284---
285# Skill
286"@ | Set-Content -Path (Join-Path $skillsDir 'SKILL.md')
287 }
288
289 AfterAll {
290 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
291 }
292
293 It 'Generates README with title and description from collection manifest' {
294 $collection = @{
295 id = 'test-coll'
296 name = 'Test Collection'
297 description = 'A test collection for unit testing'
298 items = @()
299 }
300 $mdPath = Join-Path $script:tempDir 'test.collection.md'
301 'Body content goes here.' | Set-Content -Path $mdPath
302 $outPath = Join-Path $script:tempDir 'README.test-coll.md'
303
304 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
305
306 $content = Get-Content -Path $outPath -Raw
307 $content | Should -Match '# HVE Core - Test Collection'
308 $content | Should -Match '> A test collection for unit testing'
309 $content | Should -Match 'Body content goes here'
310 }
311
312 It 'Uses HVE Core as title for hve-core-all collection' {
313 $collection = @{
314 id = 'hve-core-all'
315 name = 'HVE Core All'
316 description = 'Full bundle'
317 items = @()
318 }
319 $mdPath = Join-Path $script:tempDir 'all.collection.md'
320 'All artifacts.' | Set-Content -Path $mdPath
321 $outPath = Join-Path $script:tempDir 'README.md'
322
323 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
324
325 $content = Get-Content -Path $outPath -Raw
326 $content | Should -Match '# HVE Core'
327 $content | Should -Not -Match '# HVE Core All'
328 }
329
330 It 'Generates sorted artifact tables with descriptions grouped by kind' {
331 $collection = @{
332 id = 'multi'
333 name = 'Multi'
334 description = 'Multi-artifact test'
335 items = @(
336 @{ kind = 'agent'; path = '.github/agents/zebra.agent.md' },
337 @{ kind = 'agent'; path = '.github/agents/alpha.agent.md' },
338 @{ kind = 'prompt'; path = '.github/prompts/my-prompt.prompt.md' },
339 @{ kind = 'instruction'; path = '.github/instructions/my-instr.instructions.md' },
340 @{ kind = 'skill'; path = '.github/skills/my-skill/' }
341 )
342 }
343 $mdPath = Join-Path $script:tempDir 'multi.collection.md'
344 'Test body.' | Set-Content -Path $mdPath
345 $outPath = Join-Path $script:tempDir 'README.multi.md'
346
347 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
348
349 $content = Get-Content -Path $outPath -Raw
350 $content | Should -Match '### Chat Agents'
351 $content | Should -Match '\| Name \| Description \|'
352 $content | Should -Match '\*\*alpha\*\*.*Alpha agent description'
353 $content | Should -Match '\*\*zebra\*\*.*Zebra agent description'
354 $content | Should -Match '### Prompts'
355 $content | Should -Match '\*\*my-prompt\*\*.*My prompt description'
356 $content | Should -Match '### Instructions'
357 $content | Should -Match '\*\*my-instr\*\*.*My instruction description'
358 $content | Should -Match '### Skills'
359 $content | Should -Match '\*\*my-skill\*\*.*My skill description'
360 }
361
362 It 'Includes Full Edition link for non-default collections' {
363 $collection = @{
364 id = 'test-edition'
365 name = 'Test Edition'
366 description = 'Test edition test'
367 items = @()
368 }
369 $mdPath = Join-Path $script:tempDir 'test-edition.collection.md'
370 'Test edition body.' | Set-Content -Path $mdPath
371 $outPath = Join-Path $script:tempDir 'README.test-edition.md'
372
373 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
374
375 $content = Get-Content -Path $outPath -Raw
376 $content | Should -Match '## Full Edition'
377 $content | Should -Match 'HVE Core.*extension'
378 }
379
380 It 'Excludes Full Edition link for hve-core-all' {
381 $collection = @{
382 id = 'hve-core-all'
383 name = 'All'
384 description = 'Full bundle'
385 items = @()
386 }
387 $mdPath = Join-Path $script:tempDir 'all2.collection.md'
388 'All body.' | Set-Content -Path $mdPath
389 $outPath = Join-Path $script:tempDir 'README.all2.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 -Not -Match '## Full Edition'
395 }
396
397 It 'Includes common footer sections' {
398 $collection = @{
399 id = 'footer-test'
400 name = 'Footer'
401 description = 'Footer test'
402 items = @()
403 }
404 $mdPath = Join-Path $script:tempDir 'footer.collection.md'
405 'Footer body.' | Set-Content -Path $mdPath
406 $outPath = Join-Path $script:tempDir 'README.footer.md'
407
408 New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath
409
410 $content = Get-Content -Path $outPath -Raw
411 $content | Should -Match '## Getting Started'
412 $content | Should -Match '## Pre-release Channel'
413 $content | Should -Match '## Requirements'
414 $content | Should -Match '## License'
415 $content | Should -Match '## Support'
416 $content | Should -Match 'Microsoft ISE HVE Essentials'
417 }
418}
419
420#endregion Package Generation Function Tests
421
422Describe 'Get-AllowedMaturities' {
423 It 'Returns only stable for Stable channel' {
424 $result = Get-AllowedMaturities -Channel 'Stable'
425 $result | Should -Be @('stable')
426 }
427
428 It 'Returns all maturities for PreRelease channel' {
429 $result = Get-AllowedMaturities -Channel 'PreRelease'
430 $result | Should -Contain 'stable'
431 $result | Should -Contain 'preview'
432 $result | Should -Contain 'experimental'
433 }
434
435}
436
437Describe 'Test-CollectionMaturityEligible' {
438 It 'Returns eligible for stable collection on Stable channel' {
439 $manifest = @{ id = 'test'; maturity = 'stable' }
440 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
441 $result.IsEligible | Should -BeTrue
442 $result.Reason | Should -BeNullOrEmpty
443 }
444
445 It 'Returns eligible for stable collection on PreRelease channel' {
446 $manifest = @{ id = 'test'; maturity = 'stable' }
447 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
448 $result.IsEligible | Should -BeTrue
449 }
450
451 It 'Returns eligible for preview collection on Stable channel' {
452 $manifest = @{ id = 'test'; maturity = 'preview' }
453 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
454 $result.IsEligible | Should -BeTrue
455 }
456
457 It 'Returns eligible for preview collection on PreRelease channel' {
458 $manifest = @{ id = 'test'; maturity = 'preview' }
459 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
460 $result.IsEligible | Should -BeTrue
461 }
462
463 It 'Returns ineligible for experimental collection on Stable channel' {
464 $manifest = @{ id = 'exp-coll'; maturity = 'experimental' }
465 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
466 $result.IsEligible | Should -BeFalse
467 $result.Reason | Should -Match 'experimental.*excluded from Stable'
468 }
469
470 It 'Returns eligible for experimental collection on PreRelease channel' {
471 $manifest = @{ id = 'exp-coll'; maturity = 'experimental' }
472 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
473 $result.IsEligible | Should -BeTrue
474 }
475
476 It 'Returns ineligible for deprecated collection on Stable channel' {
477 $manifest = @{ id = 'old-coll'; maturity = 'deprecated' }
478 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
479 $result.IsEligible | Should -BeFalse
480 $result.Reason | Should -Match 'deprecated.*excluded from all channels'
481 }
482
483 It 'Returns ineligible for deprecated collection on PreRelease channel' {
484 $manifest = @{ id = 'old-coll'; maturity = 'deprecated' }
485 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
486 $result.IsEligible | Should -BeFalse
487 $result.Reason | Should -Match 'deprecated.*excluded from all channels'
488 }
489
490 It 'Defaults to stable when maturity key is absent' {
491 $manifest = @{ id = 'no-maturity' }
492 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
493 $result.IsEligible | Should -BeTrue
494 }
495
496 It 'Defaults to stable when maturity value is empty string' {
497 $manifest = @{ id = 'empty-maturity'; maturity = '' }
498 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
499 $result.IsEligible | Should -BeTrue
500 }
501
502 It 'Returns ineligible for unknown maturity value' {
503 $manifest = @{ id = 'bad-coll'; maturity = 'alpha' }
504 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease'
505 $result.IsEligible | Should -BeFalse
506 $result.Reason | Should -Match 'invalid maturity value'
507 }
508
509 It 'Returns hashtable with expected keys' {
510 $manifest = @{ id = 'test'; maturity = 'stable' }
511 $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable'
512 $result.Keys | Should -Contain 'IsEligible'
513 $result.Keys | Should -Contain 'Reason'
514 }
515}
516
517Describe 'Test-PathsExist' {
518 BeforeAll {
519 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
520 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
521 $script:extDir = Join-Path $script:tempDir 'extension'
522 $script:ghDir = Join-Path $script:tempDir '.github'
523 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
524 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
525 $script:pkgJson = Join-Path $script:extDir 'package.json'
526 '{}' | Set-Content -Path $script:pkgJson
527 }
528
529 AfterAll {
530 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
531 }
532
533 It 'Returns valid when all paths exist' {
534 $result = Test-PathsExist -ExtensionDir $script:extDir -PackageJsonPath $script:pkgJson -GitHubDir $script:ghDir
535 $result.IsValid | Should -BeTrue
536 $result.MissingPaths | Should -BeNullOrEmpty
537 }
538
539 It 'Returns invalid when extension dir missing' {
540 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-ext-dir-12345')
541 $result = Test-PathsExist -ExtensionDir $nonexistentPath -PackageJsonPath $script:pkgJson -GitHubDir $script:ghDir
542 $result.IsValid | Should -BeFalse
543 $result.MissingPaths | Should -Contain $nonexistentPath
544 }
545
546 It 'Collects multiple missing paths' {
547 $missing1 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-1')
548 $missing2 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-2')
549 $missing3 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-3')
550 $result = Test-PathsExist -ExtensionDir $missing1 -PackageJsonPath $missing2 -GitHubDir $missing3
551 $result.IsValid | Should -BeFalse
552 $result.MissingPaths.Count | Should -Be 3
553 }
554}
555
556Describe 'Get-DiscoveredAgents' {
557 BeforeAll {
558 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
559 $script:agentsDir = Join-Path $script:tempDir 'agents'
560 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
561
562 # Create test agent files
563 @'
564---
565description: "Stable agent"
566---
567'@ | Set-Content -Path (Join-Path $script:agentsDir 'stable.agent.md')
568
569 @'
570---
571description: "Preview agent"
572---
573'@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md')
574
575 }
576
577 AfterAll {
578 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
579 }
580
581 It 'Discovers agents matching allowed maturities' {
582 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @()
583 $result.DirectoryExists | Should -BeTrue
584 $result.Agents.Count | Should -Be 2
585 }
586
587 It 'Filters agents by maturity' {
588 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('preview') -ExcludedAgents @()
589 $result.Agents.Count | Should -Be 0
590 $result.Skipped.Count | Should -Be 2
591 }
592
593 It 'Excludes specified agents' {
594 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @('stable')
595 $result.Agents.Count | Should -Be 1
596 }
597
598 It 'Returns empty when directory does not exist' {
599 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-agents-dir-12345')
600 $result = Get-DiscoveredAgents -AgentsDir $nonexistentPath -AllowedMaturities @('stable') -ExcludedAgents @()
601 $result.DirectoryExists | Should -BeFalse
602 $result.Agents | Should -BeNullOrEmpty
603 }
604}
605
606Describe 'Get-DiscoveredPrompts' {
607 BeforeAll {
608 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
609 $script:promptsDir = Join-Path $script:tempDir 'prompts'
610 $script:ghDir = Join-Path $script:tempDir '.github'
611 New-Item -ItemType Directory -Path $script:promptsDir -Force | Out-Null
612 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
613
614 @'
615---
616description: "Test prompt"
617---
618'@ | Set-Content -Path (Join-Path $script:promptsDir 'test.prompt.md')
619 }
620
621 AfterAll {
622 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
623 }
624
625 It 'Discovers prompts in directory' {
626 $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
627 $result.DirectoryExists | Should -BeTrue
628 $result.Prompts.Count | Should -BeGreaterThan 0
629 }
630
631 It 'Returns empty when directory does not exist' {
632 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-prompts-dir-12345')
633 $result = Get-DiscoveredPrompts -PromptsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable')
634 $result.DirectoryExists | Should -BeFalse
635 }
636}
637
638Describe 'Get-DiscoveredInstructions' {
639 BeforeAll {
640 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
641 $script:instrDir = Join-Path $script:tempDir 'instructions'
642 $script:ghDir = Join-Path $script:tempDir '.github'
643 New-Item -ItemType Directory -Path $script:instrDir -Force | Out-Null
644 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
645
646 @'
647---
648description: "Test instruction"
649applyTo: "**/*.ps1"
650---
651'@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md')
652 }
653
654 AfterAll {
655 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
656 }
657
658 It 'Discovers instructions in directory' {
659 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
660 $result.DirectoryExists | Should -BeTrue
661 $result.Instructions.Count | Should -BeGreaterThan 0
662 }
663
664 It 'Returns empty when directory does not exist' {
665 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-instr-dir-12345')
666 $result = Get-DiscoveredInstructions -InstructionsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable')
667 $result.DirectoryExists | Should -BeFalse
668 }
669
670 It 'Skips repo-specific instructions in hve-core subdirectory' {
671 $hveCoreDir = Join-Path $script:instrDir 'hve-core'
672 New-Item -ItemType Directory -Path $hveCoreDir -Force | Out-Null
673 @'
674---
675description: "Repo-specific workflow instruction"
676applyTo: "**/.github/workflows/*.yml"
677---
678'@ | Set-Content -Path (Join-Path $hveCoreDir 'workflows.instructions.md')
679
680 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
681 $instrNames = $result.Instructions | ForEach-Object { $_.name }
682 $instrNames | Should -Not -Contain 'workflows-instructions'
683 $result.Skipped | Where-Object { $_.Reason -match 'repo-specific' } | Should -Not -BeNullOrEmpty
684 }
685
686 It 'Still discovers instructions in other subdirectories' {
687 $hveCoreDir = Join-Path $script:instrDir 'hve-core'
688 $otherDir = Join-Path $script:instrDir 'csharp'
689 New-Item -ItemType Directory -Path $hveCoreDir -Force | Out-Null
690 New-Item -ItemType Directory -Path $otherDir -Force | Out-Null
691 @'
692---
693description: "Repo-specific"
694applyTo: "**/.github/workflows/*.yml"
695---
696'@ | Set-Content -Path (Join-Path $hveCoreDir 'workflows.instructions.md')
697 @'
698---
699description: "C# instruction"
700applyTo: "**/*.cs"
701---
702'@ | Set-Content -Path (Join-Path $otherDir 'csharp.instructions.md')
703
704 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
705 $instrNames = $result.Instructions | ForEach-Object { $_.name }
706 $instrNames | Should -Contain 'csharp-instructions'
707 $instrNames | Should -Not -Contain 'workflows-instructions'
708 }
709}
710
711Describe 'Get-DiscoveredSkills' {
712 BeforeAll {
713 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
714 $script:skillsDir = Join-Path $script:tempDir 'skills'
715 New-Item -ItemType Directory -Path $script:skillsDir -Force | Out-Null
716
717 # Create test skill
718 $skillDir = Join-Path $script:skillsDir 'test-skill'
719 New-Item -ItemType Directory -Path $skillDir -Force | Out-Null
720 @'
721---
722name: test-skill
723description: "Test skill"
724---
725# Skill
726'@ | Set-Content -Path (Join-Path $skillDir 'SKILL.md')
727
728 # Create empty skill directory (no SKILL.md)
729 $emptySkillDir = Join-Path $script:skillsDir 'empty-skill'
730 New-Item -ItemType Directory -Path $emptySkillDir -Force | Out-Null
731
732 }
733
734 AfterAll {
735 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
736 }
737
738 It 'Discovers skills in directory' {
739 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
740 $result.DirectoryExists | Should -BeTrue
741 $result.Skills.Count | Should -Be 1
742 $result.Skills[0].name | Should -Be 'test-skill'
743 }
744
745 It 'Returns empty when directory does not exist' {
746 $nonexistent = Join-Path $script:tempDir 'nonexistent-skills'
747 $result = Get-DiscoveredSkills -SkillsDir $nonexistent -AllowedMaturities @('stable')
748 $result.DirectoryExists | Should -BeFalse
749 $result.Skills | Should -BeNullOrEmpty
750 }
751
752 It 'Filters skills when stable is not an allowed maturity' {
753 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('preview')
754 $result.Skills.Count | Should -Be 0
755 $result.Skipped.Count | Should -BeGreaterThan 0
756 }
757
758 It 'Skips directories without SKILL.md' {
759 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable')
760 $skippedNames = $result.Skipped | ForEach-Object { $_.Name }
761 $skippedNames | Should -Contain 'empty-skill'
762 }
763}
764
765Describe 'Get-CollectionManifest' {
766 BeforeAll {
767 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
768 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
769 }
770
771 AfterAll {
772 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
773 }
774
775 It 'Loads collection manifest from valid YAML path' {
776 $manifestFile = Join-Path $script:tempDir 'test.collection.yml'
777 @"
778id: test
779name: test-ext
780displayName: Test Extension
781description: Test
782items:
783 - hve-core-all
784"@ | Set-Content -Path $manifestFile
785
786 $result = Get-CollectionManifest -CollectionPath $manifestFile
787 $result | Should -Not -BeNullOrEmpty
788 $result.id | Should -Be 'test'
789 }
790
791 It 'Loads collection manifest from valid JSON path' {
792 $manifestFile = Join-Path $script:tempDir 'test.collection.json'
793 @{
794 '\$schema' = '../schemas/collection-manifest.schema.json'
795 id = 'test'
796 name = 'test-ext'
797 displayName = 'Test Extension'
798 description = 'Test'
799 items = @('hve-core-all')
800 } | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestFile
801
802 $result = Get-CollectionManifest -CollectionPath $manifestFile
803 $result | Should -Not -BeNullOrEmpty
804 $result.id | Should -Be 'test'
805 }
806
807 It 'Throws when path does not exist' {
808 $nonexistent = Join-Path $script:tempDir 'nonexistent.json'
809 { Get-CollectionManifest -CollectionPath $nonexistent } | Should -Throw '*not found*'
810 }
811
812 It 'Returns hashtable with expected keys' {
813 $manifestFile = Join-Path $script:tempDir 'keys.collection.yml'
814 @"
815id: keys
816name: keys-ext
817displayName: Keys
818description: Keys test
819items:
820 - developer
821"@ | Set-Content -Path $manifestFile
822
823 $result = Get-CollectionManifest -CollectionPath $manifestFile
824 $result.Keys | Should -Contain 'id'
825 $result.Keys | Should -Contain 'name'
826 $result.Keys | Should -Contain 'items'
827 }
828}
829
830Describe 'Test-GlobMatch' {
831 It 'Returns true for matching wildcard pattern' {
832 $result = Test-GlobMatch -Name 'rpi-agent' -Patterns @('rpi-*')
833 $result | Should -BeTrue
834 }
835
836 It 'Returns false for non-matching pattern' {
837 $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*')
838 $result | Should -BeFalse
839 }
840
841 It 'Matches against multiple patterns' {
842 $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*', 'mem*')
843 $result | Should -BeTrue
844 }
845
846 It 'Handles exact name match' {
847 $result = Test-GlobMatch -Name 'memory' -Patterns @('memory')
848 $result | Should -BeTrue
849 }
850}
851
852Describe 'Get-CollectionArtifacts' {
853 It 'Returns artifacts from collection items across supported kinds' {
854 $collection = @{
855 items = @(
856 @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' },
857 @{ kind = 'prompt'; path = '.github/prompts/dev-prompt.prompt.md' },
858 @{ kind = 'instruction'; path = '.github/instructions/dev/dev.instructions.md' },
859 @{ kind = 'skill'; path = '.github/skills/video-to-gif/' }
860 )
861 }
862
863 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable', 'preview')
864 $result.Agents | Should -Contain 'dev-agent'
865 $result.Prompts | Should -Contain 'dev-prompt'
866 $result.Instructions | Should -Contain 'dev/dev'
867 $result.Skills | Should -Contain 'video-to-gif'
868 }
869
870 It 'Uses item maturity when provided' {
871 $collection = @{
872 items = @(
873 @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md'; maturity = 'stable' },
874 @{ kind = 'agent'; path = '.github/agents/preview-dev.agent.md'; maturity = 'preview' }
875 )
876 }
877
878 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable')
879 $result.Agents | Should -Contain 'dev-agent'
880 $result.Agents | Should -Not -Contain 'preview-dev'
881 }
882
883 It 'Defaults to stable maturity when item maturity is omitted' {
884 $collection = @{
885 items = @(
886 @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' },
887 @{ kind = 'agent'; path = '.github/agents/preview-dev.agent.md' }
888 )
889 }
890
891 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable')
892 $result.Agents | Should -Contain 'dev-agent'
893 $result.Agents | Should -Contain 'preview-dev'
894 }
895
896 It 'Returns empty when collection has no items' {
897 $collection = @{ id = 'empty' }
898 $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable')
899 $result.Agents.Count | Should -Be 0
900 $result.Prompts.Count | Should -Be 0
901 $result.Instructions.Count | Should -Be 0
902 $result.Skills.Count | Should -Be 0
903 }
904}
905
906Describe 'Resolve-HandoffDependencies' {
907 BeforeAll {
908 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
909 $script:agentsDir = Join-Path $script:tempDir 'agents'
910 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
911
912 # Agent with no handoffs
913 @'
914---
915description: "Solo agent"
916---
917'@ | Set-Content -Path (Join-Path $script:agentsDir 'solo.agent.md')
918
919 # Agent with single handoff (object format matching real agents)
920 @'
921---
922description: "Parent agent"
923handoffs:
924 - label: "Go to child"
925 agent: child
926 prompt: Continue
927---
928'@ | Set-Content -Path (Join-Path $script:agentsDir 'parent.agent.md')
929
930 @'
931---
932description: "Child agent"
933---
934'@ | Set-Content -Path (Join-Path $script:agentsDir 'child.agent.md')
935
936 # Self-referential agent (object format)
937 @'
938---
939description: "Self agent"
940handoffs:
941 - label: "Self"
942 agent: self-ref
943---
944'@ | Set-Content -Path (Join-Path $script:agentsDir 'self-ref.agent.md')
945
946 # Circular chain (object format)
947 @'
948---
949description: "Chain A"
950handoffs:
951 - label: "To B"
952 agent: chain-b
953---
954'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-a.agent.md')
955
956 @'
957---
958description: "Chain B"
959handoffs:
960 - label: "To A"
961 agent: chain-a
962---
963'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-b.agent.md')
964
965 }
966
967 AfterAll {
968 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
969 }
970
971 It 'Returns seed agents when no handoffs' {
972 $result = Resolve-HandoffDependencies -SeedAgents @('solo') -AgentsDir $script:agentsDir
973 $result | Should -Contain 'solo'
974 $result.Count | Should -Be 1
975 }
976
977 It 'Resolves single-level handoff' {
978 $result = Resolve-HandoffDependencies -SeedAgents @('parent') -AgentsDir $script:agentsDir
979 $result | Should -Contain 'parent'
980 $result | Should -Contain 'child'
981 }
982
983 It 'Handles self-referential handoffs' {
984 $result = Resolve-HandoffDependencies -SeedAgents @('self-ref') -AgentsDir $script:agentsDir
985 $result | Should -Contain 'self-ref'
986 $result.Count | Should -Be 1
987 }
988
989 It 'Handles circular handoff chains' {
990 $result = Resolve-HandoffDependencies -SeedAgents @('chain-a') -AgentsDir $script:agentsDir
991 $result | Should -Contain 'chain-a'
992 $result | Should -Contain 'chain-b'
993 $result.Count | Should -Be 2
994 }
995}
996
997Describe 'Resolve-RequiresDependencies' {
998 It 'Resolves agent requires to include dependent prompts' {
999 $result = Resolve-RequiresDependencies `
1000 -ArtifactNames @{ agents = @('main') } `
1001 -AllowedMaturities @('stable') `
1002 -CollectionRequires @{ agents = @{ 'main' = @{ prompts = @('dep-prompt') } } } `
1003 -CollectionMaturities @{ prompts = @{ 'dep-prompt' = 'stable' } }
1004 $result.Prompts | Should -Contain 'dep-prompt'
1005 }
1006
1007 It 'Resolves transitive agent dependencies' {
1008 $result = Resolve-RequiresDependencies `
1009 -ArtifactNames @{ agents = @('top') } `
1010 -AllowedMaturities @('stable') `
1011 -CollectionRequires @{ agents = @{ 'top' = @{ agents = @('mid') }; 'mid' = @{ prompts = @('leaf-prompt') } } } `
1012 -CollectionMaturities @{ agents = @{ 'mid' = 'stable' }; prompts = @{ 'leaf-prompt' = 'stable' } }
1013 $result.Agents | Should -Contain 'mid'
1014 $result.Prompts | Should -Contain 'leaf-prompt'
1015 }
1016
1017 It 'Respects maturity filter on dependencies' {
1018 $result = Resolve-RequiresDependencies `
1019 -ArtifactNames @{ agents = @('main') } `
1020 -AllowedMaturities @('stable') `
1021 -CollectionRequires @{ agents = @{ 'main' = @{ prompts = @('exp-prompt') } } } `
1022 -CollectionMaturities @{ prompts = @{ 'exp-prompt' = 'experimental' } }
1023 $result.Prompts | Should -Not -Contain 'exp-prompt'
1024 }
1025}
1026
1027Describe 'Update-PackageJsonContributes' {
1028 It 'Updates contributes section with chat participants' {
1029 $packageJson = [PSCustomObject]@{
1030 name = 'test-extension'
1031 contributes = [PSCustomObject]@{}
1032 }
1033 $agents = @(
1034 @{ name = 'agent1'; description = 'Desc 1' }
1035 )
1036 $prompts = @(
1037 @{ name = 'prompt1'; description = 'Prompt desc' }
1038 )
1039 $instructions = @(
1040 @{ name = 'instr1'; description = 'Instr desc' }
1041 )
1042
1043 $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents $agents -ChatPromptFiles $prompts -ChatInstructions $instructions -ChatSkills @()
1044 $result.contributes | Should -Not -BeNullOrEmpty
1045 }
1046
1047 It 'Handles empty arrays' {
1048 $packageJson = [PSCustomObject]@{
1049 name = 'test-extension'
1050 contributes = [PSCustomObject]@{}
1051 }
1052
1053 $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents @() -ChatPromptFiles @() -ChatInstructions @() -ChatSkills @()
1054 $result | Should -Not -BeNullOrEmpty
1055 }
1056}
1057
1058Describe 'New-PrepareResult' {
1059 It 'Creates success result with counts' {
1060 $result = New-PrepareResult -Success $true -AgentCount 5 -PromptCount 10 -InstructionCount 15 -SkillCount 3 -Version '1.0.0'
1061 $result.Success | Should -BeTrue
1062 $result.AgentCount | Should -Be 5
1063 $result.PromptCount | Should -Be 10
1064 $result.InstructionCount | Should -Be 15
1065 $result.SkillCount | Should -Be 3
1066 $result.Version | Should -Be '1.0.0'
1067 $result.ErrorMessage | Should -BeNullOrEmpty
1068 }
1069
1070 It 'Creates failure result with error message' {
1071 $result = New-PrepareResult -Success $false -ErrorMessage 'Something went wrong'
1072 $result.Success | Should -BeFalse
1073 $result.ErrorMessage | Should -Be 'Something went wrong'
1074 $result.AgentCount | Should -Be 0
1075 $result.PromptCount | Should -Be 0
1076 $result.InstructionCount | Should -Be 0
1077 }
1078
1079 It 'Returns hashtable with all expected keys' {
1080 $result = New-PrepareResult -Success $true
1081 $result.Keys | Should -Contain 'Success'
1082 $result.Keys | Should -Contain 'AgentCount'
1083 $result.Keys | Should -Contain 'PromptCount'
1084 $result.Keys | Should -Contain 'InstructionCount'
1085 $result.Keys | Should -Contain 'SkillCount'
1086 $result.Keys | Should -Contain 'Version'
1087 $result.Keys | Should -Contain 'ErrorMessage'
1088 }
1089}
1090
1091Describe 'Invoke-PrepareExtension' {
1092 BeforeAll {
1093 $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString())
1094 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
1095
1096 # Create extension directory with package.json
1097 $script:extDir = Join-Path $script:tempDir 'extension'
1098 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
1099 @'
1100{
1101 "name": "test-extension",
1102 "version": "1.2.3",
1103 "contributes": {}
1104}
1105'@ | Set-Content -Path (Join-Path $script:extDir 'package.json')
1106
1107 # Create package template for generation
1108 $script:templatesDir = Join-Path $script:extDir 'templates'
1109 New-Item -ItemType Directory -Path $script:templatesDir -Force | Out-Null
1110 @'
1111{
1112 "name": "hve-core",
1113 "displayName": "HVE Core",
1114 "version": "1.2.3",
1115 "description": "Test extension",
1116 "publisher": "test-pub",
1117 "engines": { "vscode": "^1.80.0" },
1118 "contributes": {}
1119}
1120'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json')
1121
1122 # Create collections directory with a minimal hve-core-all collection
1123 $script:collectionsDir = Join-Path $script:tempDir 'collections'
1124 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
1125 @"
1126id: hve-core-all
1127name: hve-core
1128displayName: HVE Core
1129description: Test extension
1130"@ | Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml')
1131
1132 # Create .github structure
1133 $script:ghDir = Join-Path $script:tempDir '.github'
1134 $script:agentsDir = Join-Path $script:ghDir 'agents'
1135 $script:promptsDir = Join-Path $script:ghDir 'prompts'
1136 $script:instrDir = Join-Path $script:ghDir 'instructions'
1137 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
1138 New-Item -ItemType Directory -Path $script:promptsDir -Force | Out-Null
1139 New-Item -ItemType Directory -Path $script:instrDir -Force | Out-Null
1140
1141 # Create test agent
1142 @'
1143---
1144description: "Test agent"
1145---
1146# Agent
1147'@ | Set-Content -Path (Join-Path $script:agentsDir 'test.agent.md')
1148
1149 # Create test prompt
1150 @'
1151---
1152description: "Test prompt"
1153---
1154# Prompt
1155'@ | Set-Content -Path (Join-Path $script:promptsDir 'test.prompt.md')
1156
1157 # Create test instruction
1158 @'
1159---
1160description: "Test instruction"
1161applyTo: "**/*.ps1"
1162---
1163# Instruction
1164'@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md')
1165
1166 }
1167
1168 AfterAll {
1169 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1170 }
1171
1172 It 'Returns success result with correct counts' {
1173 $result = Invoke-PrepareExtension `
1174 -ExtensionDirectory $script:extDir `
1175 -RepoRoot $script:tempDir `
1176 -Channel 'Stable' `
1177 -DryRun
1178
1179 $result.Success | Should -BeTrue
1180 $result.AgentCount | Should -Be 1
1181 $result.PromptCount | Should -Be 1
1182 $result.InstructionCount | Should -Be 1
1183 $result.Version | Should -Be '1.2.3'
1184 }
1185
1186 It 'Fails when extension directory missing' {
1187 $nonexistentPath = Join-Path $TestDrive 'nonexistent-ext-dir-12345'
1188 $result = Invoke-PrepareExtension `
1189 -ExtensionDirectory $nonexistentPath `
1190 -RepoRoot $script:tempDir `
1191 -Channel 'Stable'
1192
1193 $result.Success | Should -BeFalse
1194 $result.ErrorMessage | Should -Not -BeNullOrEmpty
1195 }
1196
1197 It 'Respects channel filtering' {
1198 # Add preview agent
1199 @'
1200---
1201description: "Preview agent"
1202---
1203'@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md')
1204
1205 $collectionPath = Join-Path $script:tempDir 'channel-filter.collection.yml'
1206 @"
1207id: hve-core-all
1208name: hve-core-all
1209displayName: HVE Core - All
1210description: Channel filtering test
1211items:
1212 - kind: agent
1213 path: .github/agents/test.agent.md
1214 maturity: stable
1215 - kind: agent
1216 path: .github/agents/preview.agent.md
1217 maturity: preview
1218"@ | Set-Content -Path $collectionPath
1219
1220 $stableResult = Invoke-PrepareExtension `
1221 -ExtensionDirectory $script:extDir `
1222 -RepoRoot $script:tempDir `
1223 -Channel 'Stable' `
1224 -Collection $collectionPath `
1225 -DryRun
1226
1227 $preReleaseResult = Invoke-PrepareExtension `
1228 -ExtensionDirectory $script:extDir `
1229 -RepoRoot $script:tempDir `
1230 -Channel 'PreRelease' `
1231 -Collection $collectionPath `
1232 -DryRun
1233
1234 $preReleaseResult.AgentCount | Should -BeGreaterThan $stableResult.AgentCount
1235 }
1236
1237 It 'Filters prompts and instructions by maturity' {
1238 # Add experimental prompt
1239 @'
1240---
1241description: "Experimental prompt"
1242---
1243'@ | Set-Content -Path (Join-Path $script:promptsDir 'experimental.prompt.md')
1244
1245 # Add preview instruction
1246 @'
1247---
1248description: "Preview instruction"
1249applyTo: "**/*.js"
1250---
1251'@ | Set-Content -Path (Join-Path $script:instrDir 'preview.instructions.md')
1252
1253 $collectionPath = Join-Path $script:tempDir 'prompt-instruction-filter.collection.yml'
1254 @"
1255id: hve-core-all
1256name: hve-core-all
1257displayName: HVE Core - All
1258description: Prompt/instruction filtering test
1259items:
1260 - kind: agent
1261 path: .github/agents/test.agent.md
1262 maturity: stable
1263 - kind: prompt
1264 path: .github/prompts/test.prompt.md
1265 maturity: stable
1266 - kind: prompt
1267 path: .github/prompts/experimental.prompt.md
1268 maturity: experimental
1269 - kind: instruction
1270 path: .github/instructions/test.instructions.md
1271 maturity: stable
1272 - kind: instruction
1273 path: .github/instructions/preview.instructions.md
1274 maturity: preview
1275"@ | Set-Content -Path $collectionPath
1276
1277 $stableResult = Invoke-PrepareExtension `
1278 -ExtensionDirectory $script:extDir `
1279 -RepoRoot $script:tempDir `
1280 -Channel 'Stable' `
1281 -Collection $collectionPath `
1282 -DryRun
1283
1284 $preReleaseResult = Invoke-PrepareExtension `
1285 -ExtensionDirectory $script:extDir `
1286 -RepoRoot $script:tempDir `
1287 -Channel 'PreRelease' `
1288 -Collection $collectionPath `
1289 -DryRun
1290
1291 $preReleaseResult.PromptCount | Should -BeGreaterThan $stableResult.PromptCount
1292 $preReleaseResult.InstructionCount | Should -BeGreaterThan $stableResult.InstructionCount
1293 }
1294
1295 It 'Updates package.json when not DryRun' {
1296 $result = Invoke-PrepareExtension `
1297 -ExtensionDirectory $script:extDir `
1298 -RepoRoot $script:tempDir `
1299 -Channel 'Stable' `
1300 -DryRun:$false
1301
1302 $result.Success | Should -BeTrue
1303
1304 $pkgJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1305 $pkgJson.contributes.chatAgents | Should -Not -BeNullOrEmpty
1306 }
1307
1308 It 'Copies changelog when path provided' {
1309 $changelogPath = Join-Path $script:tempDir 'CHANGELOG.md'
1310 '# Changelog' | Set-Content -Path $changelogPath
1311
1312 $result = Invoke-PrepareExtension `
1313 -ExtensionDirectory $script:extDir `
1314 -RepoRoot $script:tempDir `
1315 -Channel 'Stable' `
1316 -ChangelogPath $changelogPath `
1317 -DryRun:$false
1318
1319 $result.Success | Should -BeTrue
1320 Test-Path (Join-Path $script:extDir 'CHANGELOG.md') | Should -BeTrue
1321 }
1322
1323 It 'Fails when package template is missing' {
1324 $badRoot = Join-Path $TestDrive 'bad-template-root'
1325 $badExtDir = Join-Path $badRoot 'extension'
1326 New-Item -ItemType Directory -Path $badExtDir -Force | Out-Null
1327 New-Item -ItemType Directory -Path (Join-Path $badRoot 'collections') -Force | Out-Null
1328 New-Item -ItemType Directory -Path (Join-Path $badRoot '.github/agents') -Force | Out-Null
1329 @"
1330id: test
1331"@ | Set-Content -Path (Join-Path $badRoot 'collections/test.collection.yml')
1332
1333 $result = Invoke-PrepareExtension `
1334 -ExtensionDirectory $badExtDir `
1335 -RepoRoot $badRoot `
1336 -Channel 'Stable'
1337
1338 $result.Success | Should -BeFalse
1339 $result.ErrorMessage | Should -Match 'Package generation failed'
1340 }
1341
1342 It 'Fails when no collection YAML files exist' {
1343 $emptyRoot = Join-Path $TestDrive 'empty-collections-root'
1344 $emptyExtDir = Join-Path $emptyRoot 'extension'
1345 New-Item -ItemType Directory -Path $emptyExtDir -Force | Out-Null
1346 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'collections') -Force | Out-Null
1347 New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'extension/templates') -Force | Out-Null
1348 New-Item -ItemType Directory -Path (Join-Path $emptyRoot '.github/agents') -Force | Out-Null
1349 @{ name = 'test'; version = '1.0.0' } | ConvertTo-Json | Set-Content -Path (Join-Path $emptyRoot 'extension/templates/package.template.json')
1350
1351 $result = Invoke-PrepareExtension `
1352 -ExtensionDirectory $emptyExtDir `
1353 -RepoRoot $emptyRoot `
1354 -Channel 'Stable'
1355
1356 $result.Success | Should -BeFalse
1357 $result.ErrorMessage | Should -Match 'Package generation failed'
1358 }
1359
1360 Context 'Collection template copy' {
1361 BeforeAll {
1362 # Developer collection manifest (in collections/ for generation)
1363 $script:devCollectionYaml = Join-Path $script:collectionsDir 'developer.collection.yml'
1364 @"
1365id: developer
1366name: hve-developer
1367displayName: HVE Core - Developer Edition
1368description: Developer edition
1369"@ | Set-Content -Path $script:devCollectionYaml
1370 $script:devCollectionPath = $script:devCollectionYaml
1371
1372 # hve-core-all collection manifest (default)
1373 $script:allCollectionPath = Join-Path $script:tempDir 'hve-core-all.collection.yml'
1374 @"
1375id: hve-core-all
1376name: hve-core-all
1377displayName: HVE Core - All
1378description: All artifacts
1379"@ | Set-Content -Path $script:allCollectionPath
1380
1381 # Collection manifest referencing a missing template
1382 $script:missingCollectionPath = Join-Path $script:tempDir 'nonexistent.collection.yml'
1383 @"
1384id: nonexistent
1385name: nonexistent
1386displayName: Nonexistent
1387description: Missing template
1388"@ | Set-Content -Path $script:missingCollectionPath
1389
1390 }
1391
1392 AfterEach {
1393 # Clean up backup files left by collection template copy
1394 $bakPath = Join-Path $script:extDir 'package.json.bak'
1395 if (Test-Path $bakPath) {
1396 Remove-Item -Path $bakPath -Force
1397 }
1398 }
1399
1400 It 'Skips template copy when no collection specified' {
1401 $result = Invoke-PrepareExtension `
1402 -ExtensionDirectory $script:extDir `
1403 -RepoRoot $script:tempDir `
1404 -Channel 'Stable' `
1405 -DryRun
1406
1407 $result.Success | Should -BeTrue
1408 # package.json should contain the generated hve-core-all content (not a collection template)
1409 $currentJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1410 $currentJson.name | Should -Be 'hve-core'
1411 Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse
1412 }
1413
1414 It 'Skips template copy for hve-core-all collection' {
1415 $result = Invoke-PrepareExtension `
1416 -ExtensionDirectory $script:extDir `
1417 -RepoRoot $script:tempDir `
1418 -Channel 'Stable' `
1419 -Collection $script:allCollectionPath `
1420 -DryRun
1421
1422 $result.Success | Should -BeTrue
1423 Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse
1424 }
1425
1426 It 'Returns error when collection template file missing' {
1427 $result = Invoke-PrepareExtension `
1428 -ExtensionDirectory $script:extDir `
1429 -RepoRoot $script:tempDir `
1430 -Channel 'Stable' `
1431 -Collection $script:missingCollectionPath `
1432 -DryRun
1433
1434 $result.Success | Should -BeFalse
1435 $result.ErrorMessage | Should -Match 'Collection template not found'
1436 }
1437
1438 It 'Copies template to package.json for non-default collection' {
1439 $result = Invoke-PrepareExtension `
1440 -ExtensionDirectory $script:extDir `
1441 -RepoRoot $script:tempDir `
1442 -Channel 'Stable' `
1443 -Collection $script:devCollectionPath `
1444 -DryRun
1445
1446 $result.Success | Should -BeTrue
1447 $updatedJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1448 $updatedJson.name | Should -Be 'hve-developer'
1449 }
1450
1451 It 'Creates package.json.bak backup before template copy' {
1452 $result = Invoke-PrepareExtension `
1453 -ExtensionDirectory $script:extDir `
1454 -RepoRoot $script:tempDir `
1455 -Channel 'Stable' `
1456 -Collection $script:devCollectionPath `
1457 -DryRun
1458
1459 $result.Success | Should -BeTrue
1460 $bakPath = Join-Path $script:extDir 'package.json.bak'
1461 Test-Path $bakPath | Should -BeTrue
1462 # Backup should contain the hve-core-all (canonical) generated content
1463 $bakJson = Get-Content -Path $bakPath -Raw | ConvertFrom-Json
1464 $bakJson.name | Should -Be 'hve-core'
1465 }
1466 }
1467
1468 Context 'Collection maturity gating' {
1469 BeforeAll {
1470 # Deprecated collection in collections/ directory for generation
1471 $script:deprecatedCollectionPath = Join-Path $script:collectionsDir 'deprecated-coll.collection.yml'
1472 @"
1473id: deprecated-coll
1474name: deprecated-ext
1475displayName: Deprecated Collection
1476description: Deprecated collection for testing
1477maturity: deprecated
1478"@ | Set-Content -Path $script:deprecatedCollectionPath
1479
1480 # Experimental collection in collections/ directory for generation
1481 $script:experimentalCollectionPath = Join-Path $script:collectionsDir 'experimental-coll.collection.yml'
1482 @"
1483id: experimental-coll
1484name: experimental-ext
1485displayName: Experimental Collection
1486description: Experimental collection for testing
1487maturity: experimental
1488"@ | Set-Content -Path $script:experimentalCollectionPath
1489 }
1490
1491 It 'Returns early success for deprecated collection on Stable channel' {
1492 $result = Invoke-PrepareExtension `
1493 -ExtensionDirectory $script:extDir `
1494 -RepoRoot $script:tempDir `
1495 -Channel 'Stable' `
1496 -Collection $script:deprecatedCollectionPath `
1497 -DryRun
1498
1499 $result.Success | Should -BeTrue
1500 $result.AgentCount | Should -Be 0
1501 }
1502
1503 It 'Returns early success for deprecated collection on PreRelease channel' {
1504 $result = Invoke-PrepareExtension `
1505 -ExtensionDirectory $script:extDir `
1506 -RepoRoot $script:tempDir `
1507 -Channel 'PreRelease' `
1508 -Collection $script:deprecatedCollectionPath `
1509 -DryRun
1510
1511 $result.Success | Should -BeTrue
1512 $result.AgentCount | Should -Be 0
1513 }
1514
1515 It 'Returns early success for experimental collection on Stable channel' {
1516 $result = Invoke-PrepareExtension `
1517 -ExtensionDirectory $script:extDir `
1518 -RepoRoot $script:tempDir `
1519 -Channel 'Stable' `
1520 -Collection $script:experimentalCollectionPath `
1521 -DryRun
1522
1523 $result.Success | Should -BeTrue
1524 $result.AgentCount | Should -Be 0
1525 }
1526
1527 It 'Processes experimental collection on PreRelease channel' {
1528 $result = Invoke-PrepareExtension `
1529 -ExtensionDirectory $script:extDir `
1530 -RepoRoot $script:tempDir `
1531 -Channel 'PreRelease' `
1532 -Collection $script:experimentalCollectionPath `
1533 -DryRun
1534
1535 $result.Success | Should -BeTrue
1536 $result.ErrorMessage | Should -Be ''
1537 }
1538 }
1539}
1540
1541#region Additional Coverage Tests
1542
1543Describe 'Get-ArtifactDescription' {
1544 BeforeAll {
1545 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1546 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
1547 }
1548
1549 AfterAll {
1550 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1551 }
1552
1553 It 'Returns empty string when file does not exist' {
1554 $result = Get-ArtifactDescription -FilePath (Join-Path $script:tempDir 'nonexistent.md')
1555 $result | Should -Be ''
1556 }
1557
1558 It 'Returns empty string when file has no frontmatter' {
1559 $path = Join-Path $script:tempDir 'no-frontmatter.md'
1560 '# Just a heading' | Set-Content -Path $path
1561 $result = Get-ArtifactDescription -FilePath $path
1562 $result | Should -Be ''
1563 }
1564
1565 It 'Returns empty string when frontmatter has no description' {
1566 $path = Join-Path $script:tempDir 'no-desc.md'
1567 @"
1568---
1569applyTo: "**/*.ps1"
1570---
1571# No description
1572"@ | Set-Content -Path $path
1573 $result = Get-ArtifactDescription -FilePath $path
1574 $result | Should -Be ''
1575 }
1576
1577 It 'Returns description from valid frontmatter' {
1578 $path = Join-Path $script:tempDir 'valid.md'
1579 @"
1580---
1581description: "My artifact description"
1582---
1583# Valid
1584"@ | Set-Content -Path $path
1585 $result = Get-ArtifactDescription -FilePath $path
1586 $result | Should -Be 'My artifact description'
1587 }
1588
1589 It 'Strips branding suffix from description' {
1590 $path = Join-Path $script:tempDir 'branded.md'
1591 @"
1592---
1593description: "Some tool - Brought to you by microsoft/hve-core"
1594---
1595# Branded
1596"@ | Set-Content -Path $path
1597 $result = Get-ArtifactDescription -FilePath $path
1598 $result | Should -Be 'Some tool'
1599 }
1600
1601 It 'Returns empty string when frontmatter YAML is invalid' {
1602 $path = Join-Path $script:tempDir 'bad-yaml.md'
1603 @"
1604---
1605description: [invalid: yaml: :
1606---
1607# Bad
1608"@ | Set-Content -Path $path
1609 $result = Get-ArtifactDescription -FilePath $path
1610 $result | Should -Be ''
1611 }
1612}
1613
1614Describe 'Get-CollectionArtifactKey - default branch' {
1615 It 'Handles unknown kind with matching suffix' {
1616 $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/my-file.custom.md'
1617 $result | Should -Be 'my-file'
1618 }
1619
1620 It 'Handles unknown kind with .md extension but no matching suffix' {
1621 $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/readme.md'
1622 $result | Should -Be 'readme'
1623 }
1624
1625 It 'Handles unknown kind with non-md file' {
1626 $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/config.json'
1627 $result | Should -Be 'config.json'
1628 }
1629}
1630
1631Describe 'Test-TemplateConsistency' {
1632 BeforeAll {
1633 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1634 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
1635 }
1636
1637 AfterAll {
1638 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1639 }
1640
1641 It 'Returns inconsistent when template file not found' {
1642 $manifest = @{ name = 'test'; displayName = 'Test'; description = 'Desc' }
1643 $result = Test-TemplateConsistency -TemplatePath (Join-Path $script:tempDir 'nonexistent.json') -CollectionManifest $manifest
1644 $result.IsConsistent | Should -BeFalse
1645 $result.Mismatches.Count | Should -Be 1
1646 $result.Mismatches[0].Field | Should -Be 'file'
1647 $result.Mismatches[0].Message | Should -Match 'not found'
1648 }
1649
1650 It 'Returns inconsistent when template is invalid JSON' {
1651 $badPath = Join-Path $script:tempDir 'bad-template.json'
1652 'not valid json {{{' | Set-Content -Path $badPath
1653 $manifest = @{ name = 'test' }
1654 $result = Test-TemplateConsistency -TemplatePath $badPath -CollectionManifest $manifest
1655 $result.IsConsistent | Should -BeFalse
1656 $result.Mismatches[0].Message | Should -Match 'Failed to parse'
1657 }
1658
1659 It 'Returns consistent when fields match' {
1660 $path = Join-Path $script:tempDir 'matching.json'
1661 @{ name = 'hve-rpi'; displayName = 'HVE RPI'; description = 'RPI tools' } | ConvertTo-Json | Set-Content -Path $path
1662 $manifest = @{ name = 'hve-rpi'; displayName = 'HVE RPI'; description = 'RPI tools' }
1663 $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest
1664 $result.IsConsistent | Should -BeTrue
1665 $result.Mismatches.Count | Should -Be 0
1666 }
1667
1668 It 'Reports mismatches for diverging fields' {
1669 $path = Join-Path $script:tempDir 'diverging.json'
1670 @{ name = 'old-name'; displayName = 'Old Name'; description = 'Old desc' } | ConvertTo-Json | Set-Content -Path $path
1671 $manifest = @{ name = 'new-name'; displayName = 'New Name'; description = 'New desc' }
1672 $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest
1673 $result.IsConsistent | Should -BeFalse
1674 $result.Mismatches.Count | Should -Be 3
1675 }
1676
1677 It 'Skips comparison when field missing in either side' {
1678 $path = Join-Path $script:tempDir 'partial.json'
1679 @{ name = 'test' } | ConvertTo-Json | Set-Content -Path $path
1680 $manifest = @{ displayName = 'Test Display' }
1681 $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest
1682 $result.IsConsistent | Should -BeTrue
1683 }
1684}
1685
1686Describe 'Update-PackageJsonContributes - existing contributes fields' {
1687 It 'Updates existing chatAgents field via else branch' {
1688 $packageJson = [PSCustomObject]@{
1689 name = 'test-extension'
1690 contributes = [PSCustomObject]@{
1691 chatAgents = @(@{ path = './old.agent.md' })
1692 chatPromptFiles = @(@{ path = './old.prompt.md' })
1693 chatInstructions = @(@{ path = './old.instr.md' })
1694 chatSkills = @(@{ path = './old.skill' })
1695 }
1696 }
1697 $agents = @(@{ name = 'new-agent'; path = './.github/agents/new.agent.md' })
1698 $prompts = @(@{ name = 'new-prompt'; path = './.github/prompts/new.prompt.md' })
1699 $instructions = @(@{ name = 'new-instr'; path = './.github/instructions/new.instructions.md' })
1700 $skills = @(@{ name = 'new-skill'; path = './.github/skills/new-skill' })
1701
1702 $result = Update-PackageJsonContributes -PackageJson $packageJson `
1703 -ChatAgents $agents `
1704 -ChatPromptFiles $prompts `
1705 -ChatInstructions $instructions `
1706 -ChatSkills $skills
1707
1708 $result.contributes.chatAgents[0].path | Should -Be './.github/agents/new.agent.md'
1709 $result.contributes.chatPromptFiles[0].path | Should -Be './.github/prompts/new.prompt.md'
1710 $result.contributes.chatInstructions[0].path | Should -Be './.github/instructions/new.instructions.md'
1711 $result.contributes.chatSkills[0].path | Should -Be './.github/skills/new-skill'
1712 }
1713
1714 It 'Adds contributes section when missing' {
1715 $packageJson = [PSCustomObject]@{
1716 name = 'bare-extension'
1717 }
1718
1719 $result = Update-PackageJsonContributes -PackageJson $packageJson `
1720 -ChatAgents @() `
1721 -ChatPromptFiles @() `
1722 -ChatInstructions @() `
1723 -ChatSkills @()
1724
1725 $result.contributes | Should -Not -BeNullOrEmpty
1726 }
1727}
1728
1729Describe 'Resolve-HandoffDependencies - additional cases' {
1730 BeforeAll {
1731 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1732 $script:agentsDir = Join-Path $script:tempDir 'agents'
1733 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
1734
1735 # Agent with string-format handoffs
1736 @'
1737---
1738description: "String handoff agent"
1739handoffs:
1740 - string-target
1741---
1742'@ | Set-Content -Path (Join-Path $script:agentsDir 'string-handoff.agent.md')
1743
1744 @'
1745---
1746description: "String target"
1747---
1748'@ | Set-Content -Path (Join-Path $script:agentsDir 'string-target.agent.md')
1749 }
1750
1751 AfterAll {
1752 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1753 }
1754
1755 It 'Resolves string-format handoffs' {
1756 $result = Resolve-HandoffDependencies -SeedAgents @('string-handoff') -AgentsDir $script:agentsDir
1757 $result | Should -Contain 'string-handoff'
1758 $result | Should -Contain 'string-target'
1759 }
1760
1761 It 'Warns but continues when handoff target file is missing' {
1762 $result = Resolve-HandoffDependencies -SeedAgents @('missing-agent') -AgentsDir $script:agentsDir 3>&1
1763 # The function emits a warning and returns the seed agent
1764 $agentNames = @($result | Where-Object { $_ -is [string] })
1765 $agentNames | Should -Contain 'missing-agent'
1766 }
1767}
1768
1769Describe 'Get-DiscoveredPrompts - maturity filtering' {
1770 BeforeAll {
1771 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1772 $script:promptsDir = Join-Path $script:tempDir 'prompts'
1773 $script:ghDir = Join-Path $script:tempDir '.github'
1774 New-Item -ItemType Directory -Path $script:promptsDir -Force | Out-Null
1775 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
1776
1777 @'
1778---
1779description: "Stable prompt"
1780---
1781'@ | Set-Content -Path (Join-Path $script:promptsDir 'stable.prompt.md')
1782 }
1783
1784 AfterAll {
1785 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1786 }
1787
1788 It 'Skips prompts when none match allowed maturities' {
1789 $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('experimental')
1790 $result.Prompts.Count | Should -Be 0
1791 $result.Skipped.Count | Should -Be 1
1792 }
1793}
1794
1795Describe 'Get-DiscoveredInstructions - maturity filtering' {
1796 BeforeAll {
1797 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1798 $script:instrDir = Join-Path $script:tempDir 'instructions'
1799 $script:ghDir = Join-Path $script:tempDir '.github'
1800 New-Item -ItemType Directory -Path $script:instrDir -Force | Out-Null
1801 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
1802
1803 @'
1804---
1805description: "Test instruction"
1806applyTo: "**/*.ps1"
1807---
1808'@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md')
1809 }
1810
1811 AfterAll {
1812 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1813 }
1814
1815 It 'Skips instructions when none match allowed maturities' {
1816 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('experimental')
1817 $result.Instructions.Count | Should -Be 0
1818 $result.Skipped.Count | Should -Be 1
1819 }
1820}
1821
1822Describe 'Invoke-PrepareExtension - error cases' {
1823 BeforeAll {
1824 $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString())
1825 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
1826
1827 $script:extDir = Join-Path $script:tempDir 'extension'
1828 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
1829
1830 $script:templatesDir = Join-Path $script:extDir 'templates'
1831 New-Item -ItemType Directory -Path $script:templatesDir -Force | Out-Null
1832 @'
1833{
1834 "name": "hve-core",
1835 "displayName": "HVE Core",
1836 "version": "1.0.0",
1837 "description": "Test extension",
1838 "publisher": "test-pub",
1839 "engines": { "vscode": "^1.80.0" },
1840 "contributes": {}
1841}
1842'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json')
1843
1844 $script:collectionsDir = Join-Path $script:tempDir 'collections'
1845 New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null
1846 @"
1847id: hve-core-all
1848name: hve-core
1849displayName: HVE Core
1850description: Test
1851"@ | Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml')
1852
1853 $script:ghDir = Join-Path $script:tempDir '.github'
1854 New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'agents') -Force | Out-Null
1855 New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'prompts') -Force | Out-Null
1856 New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'instructions') -Force | Out-Null
1857 }
1858
1859 AfterAll {
1860 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
1861 }
1862
1863 It 'Fails when package.json has invalid JSON' {
1864 # Write invalid JSON and mock generation to preserve it
1865 $badPkgPath = Join-Path $script:extDir 'package.json'
1866 'NOT VALID JSON' | Set-Content -Path $badPkgPath
1867
1868 Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) }
1869
1870 $result = Invoke-PrepareExtension `
1871 -ExtensionDirectory $script:extDir `
1872 -RepoRoot $script:tempDir `
1873 -Channel 'Stable'
1874
1875 $result.Success | Should -BeFalse
1876 $result.ErrorMessage | Should -Match 'Failed to parse package.json'
1877 }
1878
1879 It 'Fails when package.json lacks version field' {
1880 $badPkgPath = Join-Path $script:extDir 'package.json'
1881 @{ name = 'test-no-version' } | ConvertTo-Json | Set-Content -Path $badPkgPath
1882
1883 Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) }
1884
1885 $result = Invoke-PrepareExtension `
1886 -ExtensionDirectory $script:extDir `
1887 -RepoRoot $script:tempDir `
1888 -Channel 'Stable'
1889
1890 $result.Success | Should -BeFalse
1891 $result.ErrorMessage | Should -Match "does not contain a 'version' field"
1892 }
1893
1894 It 'Fails when version format is invalid' {
1895 $badPkgPath = Join-Path $script:extDir 'package.json'
1896 @{ name = 'test'; version = 'not-semver' } | ConvertTo-Json | Set-Content -Path $badPkgPath
1897
1898 Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) }
1899
1900 $result = Invoke-PrepareExtension `
1901 -ExtensionDirectory $script:extDir `
1902 -RepoRoot $script:tempDir `
1903 -Channel 'Stable'
1904
1905 $result.Success | Should -BeFalse
1906 $result.ErrorMessage | Should -Match 'Invalid version format'
1907 }
1908
1909 It 'Warns when changelog path specified but file not found' {
1910 $validPkgPath = Join-Path $script:extDir 'package.json'
1911 @{ name = 'test'; version = '1.0.0'; contributes = @{} } | ConvertTo-Json -Depth 5 | Set-Content -Path $validPkgPath
1912
1913 $result = Invoke-PrepareExtension `
1914 -ExtensionDirectory $script:extDir `
1915 -RepoRoot $script:tempDir `
1916 -Channel 'Stable' `
1917 -ChangelogPath (Join-Path $script:tempDir 'NONEXISTENT-CHANGELOG.md') 3>&1
1918
1919 # Filter out the result hashtable from warnings
1920 $hashtableResult = $result | Where-Object { $_ -is [hashtable] }
1921 if ($hashtableResult) {
1922 $hashtableResult.Success | Should -BeTrue
1923 }
1924 }
1925
1926 Context 'Collection with requires dependencies' {
1927 BeforeAll {
1928 $script:reqCollectionPath = Join-Path $script:tempDir 'requires-test.collection.yml'
1929 @"
1930id: hve-core-all
1931name: hve-core-all
1932displayName: HVE Core All
1933description: Requires test
1934items:
1935 - kind: agent
1936 path: .github/agents/main.agent.md
1937 maturity: stable
1938 requires:
1939 prompts:
1940 - dep-prompt
1941 - kind: prompt
1942 path: .github/prompts/dep-prompt.prompt.md
1943 maturity: stable
1944"@ | Set-Content -Path $script:reqCollectionPath
1945
1946 # Create required agent and prompt files
1947 @'
1948---
1949description: "Main agent"
1950---
1951'@ | Set-Content -Path (Join-Path $script:ghDir 'agents/main.agent.md')
1952
1953 @'
1954---
1955description: "Dependent prompt"
1956---
1957'@ | Set-Content -Path (Join-Path $script:ghDir 'prompts/dep-prompt.prompt.md')
1958
1959 # Restore valid package.json
1960 $validPkgPath = Join-Path $script:extDir 'package.json'
1961 @{ name = 'hve-core'; version = '1.0.0'; contributes = @{} } | ConvertTo-Json -Depth 5 | Set-Content -Path $validPkgPath
1962 }
1963
1964 It 'Resolves requires dependencies in collection' {
1965 $result = Invoke-PrepareExtension `
1966 -ExtensionDirectory $script:extDir `
1967 -RepoRoot $script:tempDir `
1968 -Channel 'Stable' `
1969 -Collection $script:reqCollectionPath `
1970 -DryRun
1971
1972 $result.Success | Should -BeTrue
1973 $result.AgentCount | Should -BeGreaterOrEqual 1
1974 $result.PromptCount | Should -BeGreaterOrEqual 1
1975 }
1976 }
1977}
1978
1979Describe 'Invoke-ExtensionCollectionsGeneration - collection manifest errors' {
1980 BeforeAll {
1981 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
1982
1983 $collectionsDir = Join-Path $script:tempDir 'collections'
1984 $templatesDir = Join-Path $script:tempDir 'extension/templates'
1985 New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null
1986 New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null
1987
1988 @{
1989 name = 'hve-core'
1990 displayName = 'HVE Core'
1991 version = '1.0.0'
1992 description = 'default'
1993 publisher = 'test-pub'
1994 engines = @{ vscode = '^1.80.0' }
1995 contributes = @{}
1996 } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json')
1997 }
1998
1999 AfterAll {
2000 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2001 }
2002
2003 It 'Throws when collection id is empty' {
2004 $collectionsDir = Join-Path $script:tempDir 'collections'
2005 Remove-Item -Path "$collectionsDir/*" -Force -ErrorAction SilentlyContinue
2006 @"
2007id:
2008name: empty-id
2009"@ | Set-Content -Path (Join-Path $collectionsDir 'empty.collection.yml')
2010
2011 { Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir } | Should -Throw '*Collection id is required*'
2012 }
2013
2014 It 'Throws when collection manifest is not a hashtable' {
2015 $collectionsDir = Join-Path $script:tempDir 'collections'
2016 Remove-Item -Path "$collectionsDir/*" -Force -ErrorAction SilentlyContinue
2017 # YAML that parses as a scalar string
2018 'just a string' | Set-Content -Path (Join-Path $collectionsDir 'bad.collection.yml')
2019
2020 { Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir } | Should -Throw '*must be a hashtable*'
2021 }
2022}
2023
2024Describe 'Invoke-ExtensionCollectionsGeneration - README generation' {
2025 BeforeAll {
2026 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
2027
2028 $collectionsDir = Join-Path $script:tempDir 'collections'
2029 $templatesDir = Join-Path $script:tempDir 'extension/templates'
2030 New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null
2031 New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null
2032
2033 # Package template
2034 @{
2035 name = 'hve-core'
2036 displayName = 'HVE Core'
2037 version = '1.0.0'
2038 description = 'default'
2039 publisher = 'test-pub'
2040 engines = @{ vscode = '^1.80.0' }
2041 contributes = @{}
2042 } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json')
2043
2044 # README template
2045 $repoRoot = (Get-Item "$PSScriptRoot/../../..").FullName
2046 $realTemplatePath = Join-Path $repoRoot 'extension/templates/README.template.md'
2047 if (Test-Path $realTemplatePath) {
2048 Copy-Item -Path $realTemplatePath -Destination (Join-Path $templatesDir 'README.template.md')
2049 }
2050 else {
2051 @"
2052# {{DISPLAY_NAME}}
2053
2054> {{DESCRIPTION}}
2055
2056{{BODY}}
2057
2058{{ARTIFACTS}}
2059
2060{{FULL_EDITION}}
2061"@ | Set-Content -Path (Join-Path $templatesDir 'README.template.md')
2062 }
2063
2064 # Collection with a .collection.md body file
2065 @"
2066id: readme-test
2067name: README Test
2068displayName: HVE Core - README Test
2069description: Test readme generation
2070"@ | Set-Content -Path (Join-Path $collectionsDir 'readme-test.collection.yml')
2071
2072 'Body content for readme test.' | Set-Content -Path (Join-Path $collectionsDir 'readme-test.collection.md')
2073
2074 # hve-core-all needed for the defaults
2075 @"
2076id: hve-core-all
2077name: hve-core
2078displayName: HVE Core
2079description: All artifacts
2080"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.yml')
2081 }
2082
2083 AfterAll {
2084 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
2085 }
2086
2087 It 'Generates README files for collections with .collection.md' {
2088 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2089 $readmePath = Join-Path $script:tempDir 'extension/README.readme-test.md'
2090 Test-Path $readmePath | Should -BeTrue
2091 $content = Get-Content -Path $readmePath -Raw
2092 $content | Should -Match 'Body content for readme test'
2093 }
2094
2095 It 'Skips README generation when .collection.md is missing' {
2096 $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir
2097 # hve-core-all has no .md body in this test setup
2098 $readmePath = Join-Path $script:tempDir 'extension/README.md'
2099 Test-Path $readmePath | Should -BeFalse
2100 }
2101}
2102
2103#endregion Additional Coverage Tests
2104