microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/rai-planner-guide-alignment

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

2919lines · modecode

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