microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/context-working

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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