microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/research-single-dynamic-rewrite

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

1080lines · 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
9Describe 'Get-AllowedMaturities' {
10 It 'Returns only stable for Stable channel' {
11 $result = Get-AllowedMaturities -Channel 'Stable'
12 $result | Should -Be @('stable')
13 }
14
15 It 'Returns all maturities for PreRelease channel' {
16 $result = Get-AllowedMaturities -Channel 'PreRelease'
17 $result | Should -Contain 'stable'
18 $result | Should -Contain 'preview'
19 $result | Should -Contain 'experimental'
20 }
21
22}
23
24Describe 'Get-FrontmatterData' {
25 BeforeAll {
26 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
27 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
28 }
29
30 AfterAll {
31 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
32 }
33
34 It 'Extracts description from frontmatter' {
35 $testFile = Join-Path $script:tempDir 'test.md'
36 @'
37---
38description: "Test description"
39---
40# Content
41'@ | Set-Content -Path $testFile
42
43 $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'fallback'
44 $result.description | Should -Be 'Test description'
45 }
46
47 It 'Returns hashtable with only description key' {
48 $testFile = Join-Path $script:tempDir 'desc-only.md'
49 @'
50---
51description: "Desc"
52maturity: preview
53---
54# Content
55'@ | Set-Content -Path $testFile
56
57 $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'fallback'
58 $result.Keys | Should -Contain 'description'
59 $result.Keys | Should -Not -Contain 'maturity'
60 }
61
62 It 'Uses fallback description when not in frontmatter' {
63 $testFile = Join-Path $script:tempDir 'no-desc.md'
64 @'
65---
66applyTo: "**"
67---
68# Content
69'@ | Set-Content -Path $testFile
70
71 $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'My Fallback'
72 $result.description | Should -Be 'My Fallback'
73 }
74}
75
76Describe 'Test-PathsExist' {
77 BeforeAll {
78 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
79 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
80 $script:extDir = Join-Path $script:tempDir 'extension'
81 $script:ghDir = Join-Path $script:tempDir '.github'
82 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
83 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
84 $script:pkgJson = Join-Path $script:extDir 'package.json'
85 '{}' | Set-Content -Path $script:pkgJson
86 }
87
88 AfterAll {
89 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
90 }
91
92 It 'Returns valid when all paths exist' {
93 $result = Test-PathsExist -ExtensionDir $script:extDir -PackageJsonPath $script:pkgJson -GitHubDir $script:ghDir
94 $result.IsValid | Should -BeTrue
95 $result.MissingPaths | Should -BeNullOrEmpty
96 }
97
98 It 'Returns invalid when extension dir missing' {
99 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-ext-dir-12345')
100 $result = Test-PathsExist -ExtensionDir $nonexistentPath -PackageJsonPath $script:pkgJson -GitHubDir $script:ghDir
101 $result.IsValid | Should -BeFalse
102 $result.MissingPaths | Should -Contain $nonexistentPath
103 }
104
105 It 'Collects multiple missing paths' {
106 $missing1 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-1')
107 $missing2 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-2')
108 $missing3 = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'missing-path-3')
109 $result = Test-PathsExist -ExtensionDir $missing1 -PackageJsonPath $missing2 -GitHubDir $missing3
110 $result.IsValid | Should -BeFalse
111 $result.MissingPaths.Count | Should -Be 3
112 }
113}
114
115Describe 'Get-DiscoveredAgents' {
116 BeforeAll {
117 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
118 $script:agentsDir = Join-Path $script:tempDir 'agents'
119 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
120
121 # Create test agent files
122 @'
123---
124description: "Stable agent"
125---
126'@ | Set-Content -Path (Join-Path $script:agentsDir 'stable.agent.md')
127
128 @'
129---
130description: "Preview agent"
131---
132'@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md')
133
134 $script:mockRegistry = @{
135 agents = @{
136 'stable' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
137 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() }
138 }
139 }
140 }
141
142 AfterAll {
143 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
144 }
145
146 It 'Discovers agents matching allowed maturities' {
147 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @() -Registry $script:mockRegistry
148 $result.DirectoryExists | Should -BeTrue
149 $result.Agents.Count | Should -Be 2
150 }
151
152 It 'Filters agents by maturity' {
153 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -ExcludedAgents @() -Registry $script:mockRegistry
154 $result.Agents.Count | Should -Be 1
155 $result.Skipped.Count | Should -Be 1
156 }
157
158 It 'Excludes specified agents' {
159 $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @('stable') -Registry $script:mockRegistry
160 $result.Agents.Count | Should -Be 1
161 }
162
163 It 'Returns empty when directory does not exist' {
164 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-agents-dir-12345')
165 $result = Get-DiscoveredAgents -AgentsDir $nonexistentPath -AllowedMaturities @('stable') -ExcludedAgents @()
166 $result.DirectoryExists | Should -BeFalse
167 $result.Agents | Should -BeNullOrEmpty
168 }
169}
170
171Describe 'Get-DiscoveredPrompts' {
172 BeforeAll {
173 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
174 $script:promptsDir = Join-Path $script:tempDir 'prompts'
175 $script:ghDir = Join-Path $script:tempDir '.github'
176 New-Item -ItemType Directory -Path $script:promptsDir -Force | Out-Null
177 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
178
179 @'
180---
181description: "Test prompt"
182---
183'@ | Set-Content -Path (Join-Path $script:promptsDir 'test.prompt.md')
184 }
185
186 AfterAll {
187 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
188 }
189
190 It 'Discovers prompts in directory' {
191 $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
192 $result.DirectoryExists | Should -BeTrue
193 $result.Prompts.Count | Should -BeGreaterThan 0
194 }
195
196 It 'Returns empty when directory does not exist' {
197 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-prompts-dir-12345')
198 $result = Get-DiscoveredPrompts -PromptsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable')
199 $result.DirectoryExists | Should -BeFalse
200 }
201}
202
203Describe 'Get-DiscoveredInstructions' {
204 BeforeAll {
205 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
206 $script:instrDir = Join-Path $script:tempDir 'instructions'
207 $script:ghDir = Join-Path $script:tempDir '.github'
208 New-Item -ItemType Directory -Path $script:instrDir -Force | Out-Null
209 New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null
210
211 @'
212---
213description: "Test instruction"
214applyTo: "**/*.ps1"
215---
216'@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md')
217 }
218
219 AfterAll {
220 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
221 }
222
223 It 'Discovers instructions in directory' {
224 $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable')
225 $result.DirectoryExists | Should -BeTrue
226 $result.Instructions.Count | Should -BeGreaterThan 0
227 }
228
229 It 'Returns empty when directory does not exist' {
230 $nonexistentPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'nonexistent-instr-dir-12345')
231 $result = Get-DiscoveredInstructions -InstructionsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable')
232 $result.DirectoryExists | Should -BeFalse
233 }
234}
235
236Describe 'Get-RegistryData' {
237 BeforeAll {
238 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
239 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
240 }
241
242 AfterAll {
243 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
244 }
245
246 It 'Loads registry from valid path' {
247 $registryFile = Join-Path $script:tempDir 'registry.json'
248 @{ agents = @{ 'test' = @{ maturity = 'stable' } } } | ConvertTo-Json -Depth 5 | Set-Content -Path $registryFile
249
250 $result = Get-RegistryData -RegistryPath $registryFile
251 $result | Should -Not -BeNullOrEmpty
252 }
253
254 It 'Throws when path does not exist' {
255 $nonexistent = Join-Path $script:tempDir 'nonexistent.json'
256 { Get-RegistryData -RegistryPath $nonexistent } | Should -Throw '*not found*'
257 }
258
259 It 'Returns hashtable with expected keys' {
260 $registryFile = Join-Path $script:tempDir 'registry2.json'
261 @{
262 agents = @{ 'a' = @{ maturity = 'stable' } }
263 prompts = @{ 'p' = @{ maturity = 'stable' } }
264 } | ConvertTo-Json -Depth 5 | Set-Content -Path $registryFile
265
266 $result = Get-RegistryData -RegistryPath $registryFile
267 $result.Keys | Should -Contain 'agents'
268 $result.Keys | Should -Contain 'prompts'
269 }
270}
271
272Describe 'Get-DiscoveredSkills' {
273 BeforeAll {
274 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
275 $script:skillsDir = Join-Path $script:tempDir 'skills'
276 New-Item -ItemType Directory -Path $script:skillsDir -Force | Out-Null
277
278 # Create test skill
279 $skillDir = Join-Path $script:skillsDir 'test-skill'
280 New-Item -ItemType Directory -Path $skillDir -Force | Out-Null
281 @'
282---
283name: test-skill
284description: "Test skill"
285---
286# Skill
287'@ | Set-Content -Path (Join-Path $skillDir 'SKILL.md')
288
289 # Create empty skill directory (no SKILL.md)
290 $emptySkillDir = Join-Path $script:skillsDir 'empty-skill'
291 New-Item -ItemType Directory -Path $emptySkillDir -Force | Out-Null
292
293 $script:mockRegistry = @{
294 skills = @{
295 'test-skill' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
296 }
297 }
298 }
299
300 AfterAll {
301 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
302 }
303
304 It 'Discovers skills in directory' {
305 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry
306 $result.DirectoryExists | Should -BeTrue
307 $result.Skills.Count | Should -Be 1
308 $result.Skills[0].name | Should -Be 'test-skill'
309 }
310
311 It 'Returns empty when directory does not exist' {
312 $nonexistent = Join-Path $script:tempDir 'nonexistent-skills'
313 $result = Get-DiscoveredSkills -SkillsDir $nonexistent -AllowedMaturities @('stable')
314 $result.DirectoryExists | Should -BeFalse
315 $result.Skills | Should -BeNullOrEmpty
316 }
317
318 It 'Filters skills by maturity from registry' {
319 $previewRegistry = @{
320 skills = @{
321 'test-skill' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() }
322 }
323 }
324 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') -Registry $previewRegistry
325 $result.Skills.Count | Should -Be 0
326 $result.Skipped.Count | Should -BeGreaterThan 0
327 }
328
329 It 'Skips directories without SKILL.md' {
330 $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry
331 $skippedNames = $result.Skipped | ForEach-Object { $_.Name }
332 $skippedNames | Should -Contain 'empty-skill'
333 }
334}
335
336Describe 'Get-CollectionManifest' {
337 BeforeAll {
338 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
339 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
340 }
341
342 AfterAll {
343 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
344 }
345
346 It 'Loads collection manifest from valid path' {
347 $manifestFile = Join-Path $script:tempDir 'test.collection.json'
348 @{
349 '$schema' = '../schemas/collection.schema.json'
350 id = 'test'
351 name = 'test-ext'
352 displayName = 'Test Extension'
353 description = 'Test'
354 personas = @('hve-core-all')
355 } | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestFile
356
357 $result = Get-CollectionManifest -CollectionPath $manifestFile
358 $result | Should -Not -BeNullOrEmpty
359 $result.id | Should -Be 'test'
360 }
361
362 It 'Throws when path does not exist' {
363 $nonexistent = Join-Path $script:tempDir 'nonexistent.json'
364 { Get-CollectionManifest -CollectionPath $nonexistent } | Should -Throw '*not found*'
365 }
366
367 It 'Returns hashtable with expected keys' {
368 $manifestFile = Join-Path $script:tempDir 'keys.collection.json'
369 @{
370 '$schema' = '../schemas/collection.schema.json'
371 id = 'keys'
372 name = 'keys-ext'
373 displayName = 'Keys'
374 description = 'Keys test'
375 personas = @('developer')
376 } | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestFile
377
378 $result = Get-CollectionManifest -CollectionPath $manifestFile
379 $result.Keys | Should -Contain 'id'
380 $result.Keys | Should -Contain 'name'
381 $result.Keys | Should -Contain 'personas'
382 }
383}
384
385Describe 'Test-GlobMatch' {
386 It 'Returns true for matching wildcard pattern' {
387 $result = Test-GlobMatch -Name 'rpi-agent' -Patterns @('rpi-*')
388 $result | Should -BeTrue
389 }
390
391 It 'Returns false for non-matching pattern' {
392 $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*')
393 $result | Should -BeFalse
394 }
395
396 It 'Matches against multiple patterns' {
397 $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*', 'mem*')
398 $result | Should -BeTrue
399 }
400
401 It 'Handles exact name match' {
402 $result = Test-GlobMatch -Name 'memory' -Patterns @('memory')
403 $result | Should -BeTrue
404 }
405}
406
407Describe 'Get-CollectionArtifacts' {
408 BeforeAll {
409 $script:registry = @{
410 agents = @{
411 'dev-agent' = @{ maturity = 'stable'; personas = @('developer'); tags = @() }
412 'all-agent' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
413 'preview-dev' = @{ maturity = 'preview'; personas = @('developer'); tags = @() }
414 }
415 prompts = @{
416 'dev-prompt' = @{ maturity = 'stable'; personas = @('developer'); tags = @() }
417 }
418 instructions = @{}
419 skills = @{}
420 }
421 }
422
423 It 'Filters by persona' {
424 $collection = @{ personas = @('developer') }
425 $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview')
426 $result.Agents | Should -Contain 'dev-agent'
427 $result.Agents | Should -Not -Contain 'all-agent'
428 }
429
430 It 'Applies include patterns' {
431 $collection = @{
432 personas = @('developer')
433 include = @{ agents = @('dev-*') }
434 }
435 $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview')
436 $result.Agents | Should -Contain 'dev-agent'
437 }
438
439 It 'Applies exclude patterns' {
440 $collection = @{
441 personas = @('developer')
442 exclude = @{ agents = @('preview-*') }
443 }
444 $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview')
445 $result.Agents | Should -Contain 'dev-agent'
446 $result.Agents | Should -Not -Contain 'preview-dev'
447 }
448
449 It 'Respects maturity filter' {
450 $collection = @{ personas = @('developer') }
451 $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable')
452 $result.Agents | Should -Contain 'dev-agent'
453 $result.Agents | Should -Not -Contain 'preview-dev'
454 }
455}
456
457Describe 'Resolve-HandoffDependencies' {
458 BeforeAll {
459 $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
460 $script:agentsDir = Join-Path $script:tempDir 'agents'
461 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
462
463 # Agent with no handoffs
464 @'
465---
466description: "Solo agent"
467---
468'@ | Set-Content -Path (Join-Path $script:agentsDir 'solo.agent.md')
469
470 # Agent with single handoff (object format matching real agents)
471 @'
472---
473description: "Parent agent"
474handoffs:
475 - label: "Go to child"
476 agent: child
477 prompt: Continue
478---
479'@ | Set-Content -Path (Join-Path $script:agentsDir 'parent.agent.md')
480
481 @'
482---
483description: "Child agent"
484---
485'@ | Set-Content -Path (Join-Path $script:agentsDir 'child.agent.md')
486
487 # Self-referential agent (object format)
488 @'
489---
490description: "Self agent"
491handoffs:
492 - label: "Self"
493 agent: self-ref
494---
495'@ | Set-Content -Path (Join-Path $script:agentsDir 'self-ref.agent.md')
496
497 # Circular chain (object format)
498 @'
499---
500description: "Chain A"
501handoffs:
502 - label: "To B"
503 agent: chain-b
504---
505'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-a.agent.md')
506
507 @'
508---
509description: "Chain B"
510handoffs:
511 - label: "To A"
512 agent: chain-a
513---
514'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-b.agent.md')
515
516 $script:mockRegistry = @{
517 agents = @{
518 'solo' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
519 'parent' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
520 'child' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
521 'self-ref' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
522 'chain-a' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
523 'chain-b' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
524 }
525 }
526 }
527
528 AfterAll {
529 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
530 }
531
532 It 'Returns seed agents when no handoffs' {
533 $result = Resolve-HandoffDependencies -SeedAgents @('solo') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry
534 $result | Should -Contain 'solo'
535 $result.Count | Should -Be 1
536 }
537
538 It 'Resolves single-level handoff' {
539 $result = Resolve-HandoffDependencies -SeedAgents @('parent') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry
540 $result | Should -Contain 'parent'
541 $result | Should -Contain 'child'
542 }
543
544 It 'Handles self-referential handoffs' {
545 $result = Resolve-HandoffDependencies -SeedAgents @('self-ref') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry
546 $result | Should -Contain 'self-ref'
547 $result.Count | Should -Be 1
548 }
549
550 It 'Handles circular handoff chains' {
551 $result = Resolve-HandoffDependencies -SeedAgents @('chain-a') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry
552 $result | Should -Contain 'chain-a'
553 $result | Should -Contain 'chain-b'
554 $result.Count | Should -Be 2
555 }
556}
557
558Describe 'Resolve-RequiresDependencies' {
559 It 'Resolves agent requires to include dependent prompts' {
560 $registry = @{
561 agents = @{
562 'main' = @{
563 maturity = 'stable'
564 personas = @('hve-core-all')
565 requires = @{ prompts = @('dep-prompt') }
566 }
567 }
568 prompts = @{
569 'dep-prompt' = @{ maturity = 'stable'; personas = @('hve-core-all') }
570 }
571 }
572 $result = Resolve-RequiresDependencies -ArtifactNames @{ agents = @('main') } -Registry $registry -AllowedMaturities @('stable')
573 $result.Prompts | Should -Contain 'dep-prompt'
574 }
575
576 It 'Resolves transitive agent dependencies' {
577 $registry = @{
578 agents = @{
579 'top' = @{
580 maturity = 'stable'
581 personas = @('hve-core-all')
582 requires = @{ agents = @('mid') }
583 }
584 'mid' = @{
585 maturity = 'stable'
586 personas = @('hve-core-all')
587 requires = @{ prompts = @('leaf-prompt') }
588 }
589 }
590 prompts = @{
591 'leaf-prompt' = @{ maturity = 'stable'; personas = @('hve-core-all') }
592 }
593 }
594 $result = Resolve-RequiresDependencies -ArtifactNames @{ agents = @('top') } -Registry $registry -AllowedMaturities @('stable')
595 $result.Agents | Should -Contain 'mid'
596 $result.Prompts | Should -Contain 'leaf-prompt'
597 }
598
599 It 'Respects maturity filter on dependencies' {
600 $registry = @{
601 agents = @{
602 'main' = @{
603 maturity = 'stable'
604 personas = @('hve-core-all')
605 requires = @{ prompts = @('exp-prompt') }
606 }
607 }
608 prompts = @{
609 'exp-prompt' = @{ maturity = 'experimental'; personas = @('hve-core-all') }
610 }
611 }
612 $result = Resolve-RequiresDependencies -ArtifactNames @{ agents = @('main') } -Registry $registry -AllowedMaturities @('stable')
613 $result.Prompts | Should -Not -Contain 'exp-prompt'
614 }
615}
616
617Describe 'Update-PackageJsonContributes' {
618 It 'Updates contributes section with chat participants' {
619 $packageJson = [PSCustomObject]@{
620 name = 'test-extension'
621 contributes = [PSCustomObject]@{}
622 }
623 $agents = @(
624 @{ name = 'agent1'; description = 'Desc 1' }
625 )
626 $prompts = @(
627 @{ name = 'prompt1'; description = 'Prompt desc' }
628 )
629 $instructions = @(
630 @{ name = 'instr1'; description = 'Instr desc' }
631 )
632
633 $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents $agents -ChatPromptFiles $prompts -ChatInstructions $instructions -ChatSkills @()
634 $result.contributes | Should -Not -BeNullOrEmpty
635 }
636
637 It 'Handles empty arrays' {
638 $packageJson = [PSCustomObject]@{
639 name = 'test-extension'
640 contributes = [PSCustomObject]@{}
641 }
642
643 $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents @() -ChatPromptFiles @() -ChatInstructions @() -ChatSkills @()
644 $result | Should -Not -BeNullOrEmpty
645 }
646}
647
648Describe 'New-PrepareResult' {
649 It 'Creates success result with counts' {
650 $result = New-PrepareResult -Success $true -AgentCount 5 -PromptCount 10 -InstructionCount 15 -SkillCount 3 -Version '1.0.0'
651 $result.Success | Should -BeTrue
652 $result.AgentCount | Should -Be 5
653 $result.PromptCount | Should -Be 10
654 $result.InstructionCount | Should -Be 15
655 $result.SkillCount | Should -Be 3
656 $result.Version | Should -Be '1.0.0'
657 $result.ErrorMessage | Should -BeNullOrEmpty
658 }
659
660 It 'Creates failure result with error message' {
661 $result = New-PrepareResult -Success $false -ErrorMessage 'Something went wrong'
662 $result.Success | Should -BeFalse
663 $result.ErrorMessage | Should -Be 'Something went wrong'
664 $result.AgentCount | Should -Be 0
665 $result.PromptCount | Should -Be 0
666 $result.InstructionCount | Should -Be 0
667 }
668
669 It 'Returns hashtable with all expected keys' {
670 $result = New-PrepareResult -Success $true
671 $result.Keys | Should -Contain 'Success'
672 $result.Keys | Should -Contain 'AgentCount'
673 $result.Keys | Should -Contain 'PromptCount'
674 $result.Keys | Should -Contain 'InstructionCount'
675 $result.Keys | Should -Contain 'SkillCount'
676 $result.Keys | Should -Contain 'Version'
677 $result.Keys | Should -Contain 'ErrorMessage'
678 }
679}
680
681Describe 'Invoke-PrepareExtension' {
682 BeforeAll {
683 $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString())
684 New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null
685
686 # Create extension directory with package.json
687 $script:extDir = Join-Path $script:tempDir 'extension'
688 New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null
689 @'
690{
691 "name": "test-extension",
692 "version": "1.2.3",
693 "contributes": {}
694}
695'@ | Set-Content -Path (Join-Path $script:extDir 'package.json')
696
697 # Create .github structure
698 $script:ghDir = Join-Path $script:tempDir '.github'
699 $script:agentsDir = Join-Path $script:ghDir 'agents'
700 $script:promptsDir = Join-Path $script:ghDir 'prompts'
701 $script:instrDir = Join-Path $script:ghDir 'instructions'
702 New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null
703 New-Item -ItemType Directory -Path $script:promptsDir -Force | Out-Null
704 New-Item -ItemType Directory -Path $script:instrDir -Force | Out-Null
705
706 # Create test agent
707 @'
708---
709description: "Test agent"
710---
711# Agent
712'@ | Set-Content -Path (Join-Path $script:agentsDir 'test.agent.md')
713
714 # Create test prompt
715 @'
716---
717description: "Test prompt"
718---
719# Prompt
720'@ | Set-Content -Path (Join-Path $script:promptsDir 'test.prompt.md')
721
722 # Create test instruction
723 @'
724---
725description: "Test instruction"
726applyTo: "**/*.ps1"
727---
728# Instruction
729'@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md')
730
731 # Create mock registry for all Invoke-PrepareExtension tests
732 $registryContent = @{
733 version = "1.0"
734 personas = @{ definitions = @{ 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } } }
735 agents = @{
736 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
737 }
738 prompts = @{
739 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
740 }
741 instructions = @{
742 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
743 }
744 skills = @{}
745 }
746 $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json')
747 }
748
749 AfterAll {
750 Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue
751 }
752
753 It 'Returns success result with correct counts' {
754 $result = Invoke-PrepareExtension `
755 -ExtensionDirectory $script:extDir `
756 -RepoRoot $script:tempDir `
757 -Channel 'Stable' `
758 -DryRun
759
760 $result.Success | Should -BeTrue
761 $result.AgentCount | Should -Be 1
762 $result.PromptCount | Should -Be 1
763 $result.InstructionCount | Should -Be 1
764 $result.Version | Should -Be '1.2.3'
765 }
766
767 It 'Fails when extension directory missing' {
768 $nonexistentPath = Join-Path $TestDrive 'nonexistent-ext-dir-12345'
769 $result = Invoke-PrepareExtension `
770 -ExtensionDirectory $nonexistentPath `
771 -RepoRoot $script:tempDir `
772 -Channel 'Stable'
773
774 $result.Success | Should -BeFalse
775 $result.ErrorMessage | Should -Match 'Required paths not found'
776 }
777
778 It 'Respects channel filtering' {
779 # Add preview agent
780 @'
781---
782description: "Preview agent"
783---
784'@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md')
785
786 # Update registry with preview agent
787 $registryContent = @{
788 version = "1.0"
789 personas = @{ definitions = @{ 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } } }
790 agents = @{
791 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
792 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() }
793 }
794 prompts = @{
795 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
796 }
797 instructions = @{
798 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
799 }
800 skills = @{}
801 }
802 $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json')
803
804 $stableResult = Invoke-PrepareExtension `
805 -ExtensionDirectory $script:extDir `
806 -RepoRoot $script:tempDir `
807 -Channel 'Stable' `
808 -DryRun
809
810 $preReleaseResult = Invoke-PrepareExtension `
811 -ExtensionDirectory $script:extDir `
812 -RepoRoot $script:tempDir `
813 -Channel 'PreRelease' `
814 -DryRun
815
816 $preReleaseResult.AgentCount | Should -BeGreaterThan $stableResult.AgentCount
817 }
818
819 It 'Filters prompts and instructions by maturity' {
820 # Add experimental prompt
821 @'
822---
823description: "Experimental prompt"
824---
825'@ | Set-Content -Path (Join-Path $script:promptsDir 'experimental.prompt.md')
826
827 # Add preview instruction
828 @'
829---
830description: "Preview instruction"
831applyTo: "**/*.js"
832---
833'@ | Set-Content -Path (Join-Path $script:instrDir 'preview.instructions.md')
834
835 # Update registry with all artifacts
836 $registryContent = @{
837 version = "1.0"
838 personas = @{ definitions = @{ 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } } }
839 agents = @{
840 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
841 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() }
842 }
843 prompts = @{
844 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
845 'experimental' = @{ maturity = 'experimental'; personas = @('hve-core-all'); tags = @() }
846 }
847 instructions = @{
848 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() }
849 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() }
850 }
851 skills = @{}
852 }
853 $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json')
854
855 $stableResult = Invoke-PrepareExtension `
856 -ExtensionDirectory $script:extDir `
857 -RepoRoot $script:tempDir `
858 -Channel 'Stable' `
859 -DryRun
860
861 $preReleaseResult = Invoke-PrepareExtension `
862 -ExtensionDirectory $script:extDir `
863 -RepoRoot $script:tempDir `
864 -Channel 'PreRelease' `
865 -DryRun
866
867 $preReleaseResult.PromptCount | Should -BeGreaterThan $stableResult.PromptCount
868 $preReleaseResult.InstructionCount | Should -BeGreaterThan $stableResult.InstructionCount
869 }
870
871 It 'Updates package.json when not DryRun' {
872 $result = Invoke-PrepareExtension `
873 -ExtensionDirectory $script:extDir `
874 -RepoRoot $script:tempDir `
875 -Channel 'Stable' `
876 -DryRun:$false
877
878 $result.Success | Should -BeTrue
879
880 $pkgJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
881 $pkgJson.contributes.chatAgents | Should -Not -BeNullOrEmpty
882 }
883
884 It 'Copies changelog when path provided' {
885 $changelogPath = Join-Path $script:tempDir 'CHANGELOG.md'
886 '# Changelog' | Set-Content -Path $changelogPath
887
888 $result = Invoke-PrepareExtension `
889 -ExtensionDirectory $script:extDir `
890 -RepoRoot $script:tempDir `
891 -Channel 'Stable' `
892 -ChangelogPath $changelogPath `
893 -DryRun:$false
894
895 $result.Success | Should -BeTrue
896 Test-Path (Join-Path $script:extDir 'CHANGELOG.md') | Should -BeTrue
897 }
898
899 It 'Fails when package.json has invalid JSON' {
900 $badJsonDir = Join-Path $TestDrive 'bad-json-ext'
901 New-Item -ItemType Directory -Path $badJsonDir -Force | Out-Null
902 '{ invalid json }' | Set-Content -Path (Join-Path $badJsonDir 'package.json')
903
904 # Create .github structure for this test
905 $badGhDir = Join-Path (Split-Path $badJsonDir -Parent) '.github'
906 New-Item -ItemType Directory -Path (Join-Path $badGhDir 'agents') -Force | Out-Null
907
908 $result = Invoke-PrepareExtension `
909 -ExtensionDirectory $badJsonDir `
910 -RepoRoot (Split-Path $badJsonDir -Parent) `
911 -Channel 'Stable'
912
913 $result.Success | Should -BeFalse
914 $result.ErrorMessage | Should -Match 'Failed to parse package.json'
915 }
916
917 It 'Fails when package.json missing version field' {
918 $noVersionDir = Join-Path $TestDrive 'no-version-ext'
919 New-Item -ItemType Directory -Path $noVersionDir -Force | Out-Null
920 '{"name": "test"}' | Set-Content -Path (Join-Path $noVersionDir 'package.json')
921
922 # Create .github structure for this test
923 $noVersionGhDir = Join-Path (Split-Path $noVersionDir -Parent) '.github'
924 New-Item -ItemType Directory -Path (Join-Path $noVersionGhDir 'agents') -Force | Out-Null
925
926 $result = Invoke-PrepareExtension `
927 -ExtensionDirectory $noVersionDir `
928 -RepoRoot (Split-Path $noVersionDir -Parent) `
929 -Channel 'Stable'
930
931 $result.Success | Should -BeFalse
932 $result.ErrorMessage | Should -Match "does not contain a 'version' field"
933 }
934
935 It 'Fails when version format is invalid' {
936 $badVersionDir = Join-Path $TestDrive 'bad-version-ext'
937 New-Item -ItemType Directory -Path $badVersionDir -Force | Out-Null
938 '{"name": "test", "version": "invalid"}' | Set-Content -Path (Join-Path $badVersionDir 'package.json')
939
940 # Create .github structure for this test
941 $badVersionGhDir = Join-Path (Split-Path $badVersionDir -Parent) '.github'
942 New-Item -ItemType Directory -Path (Join-Path $badVersionGhDir 'agents') -Force | Out-Null
943
944 $result = Invoke-PrepareExtension `
945 -ExtensionDirectory $badVersionDir `
946 -RepoRoot (Split-Path $badVersionDir -Parent) `
947 -Channel 'Stable'
948
949 $result.Success | Should -BeFalse
950 $result.ErrorMessage | Should -Match 'Invalid version format'
951 }
952
953 Context 'Metadata override from collection manifest (Option A)' {
954 BeforeAll {
955 # Developer collection manifest with publisher
956 $script:devCollectionPath = Join-Path $script:tempDir 'developer.collection.json'
957 @{
958 id = 'developer'
959 name = 'hve-developer'
960 displayName = 'HVE Core - Developer Edition'
961 description = 'Developer edition'
962 publisher = 'ise-hve-essentials'
963 personas = @('developer')
964 } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:devCollectionPath
965
966 # hve-core-all collection manifest (default)
967 $script:allCollectionPath = Join-Path $script:tempDir 'hve-core-all.collection.json'
968 @{
969 id = 'hve-core-all'
970 name = 'hve-core'
971 displayName = 'HVE Core'
972 description = 'All artifacts'
973 publisher = 'ise-hve-essentials'
974 personas = @('hve-core-all')
975 } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:allCollectionPath
976
977 # Collection manifest missing required fields
978 $script:incompleteCollectionPath = Join-Path $script:tempDir 'incomplete.collection.json'
979 @{
980 id = 'incomplete'
981 name = 'incomplete'
982 personas = @('incomplete')
983 } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:incompleteCollectionPath
984
985 # Update registry with developer persona entry
986 $registryContent = @{
987 version = '1.0'
988 personas = @{
989 definitions = @{
990 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' }
991 'developer' = @{ name = 'Developer'; description = 'Developer artifacts' }
992 }
993 }
994 agents = @{
995 'test' = @{ maturity = 'stable'; personas = @('hve-core-all', 'developer'); tags = @(); description = 'Test agent' }
996 }
997 prompts = @{
998 'test' = @{ maturity = 'stable'; personas = @('hve-core-all', 'developer'); tags = @(); description = 'Test prompt' }
999 }
1000 instructions = @{
1001 'test' = @{ maturity = 'stable'; personas = @('hve-core-all', 'developer'); tags = @(); description = 'Test instructions' }
1002 }
1003 skills = @{}
1004 }
1005 $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json')
1006 }
1007
1008 BeforeEach {
1009 $script:originalPackageJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw
1010 }
1011
1012 AfterEach {
1013 $script:originalPackageJson | Set-Content -Path (Join-Path $script:extDir 'package.json')
1014 }
1015
1016 It 'Skips metadata override when no collection specified' {
1017 $result = Invoke-PrepareExtension `
1018 -ExtensionDirectory $script:extDir `
1019 -RepoRoot $script:tempDir `
1020 -Channel 'Stable' `
1021 -DryRun
1022
1023 $result.Success | Should -BeTrue
1024 $currentContent = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw
1025 $currentContent | Should -Be $script:originalPackageJson
1026 }
1027
1028 It 'Skips metadata override for hve-core-all collection' {
1029 $result = Invoke-PrepareExtension `
1030 -ExtensionDirectory $script:extDir `
1031 -RepoRoot $script:tempDir `
1032 -Channel 'Stable' `
1033 -Collection $script:allCollectionPath `
1034 -DryRun
1035
1036 $result.Success | Should -BeTrue
1037 }
1038
1039 It 'Returns error when collection manifest missing required fields' {
1040 $result = Invoke-PrepareExtension `
1041 -ExtensionDirectory $script:extDir `
1042 -RepoRoot $script:tempDir `
1043 -Channel 'Stable' `
1044 -Collection $script:incompleteCollectionPath `
1045 -DryRun
1046
1047 $result.Success | Should -BeFalse
1048 $result.ErrorMessage | Should -Match 'missing required fields'
1049 }
1050
1051 It 'Applies metadata override from collection manifest for non-default collection' {
1052 $result = Invoke-PrepareExtension `
1053 -ExtensionDirectory $script:extDir `
1054 -RepoRoot $script:tempDir `
1055 -Channel 'Stable' `
1056 -Collection $script:devCollectionPath `
1057 -DryRun
1058
1059 $result.Success | Should -BeTrue
1060 $updatedJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
1061 $updatedJson.name | Should -Be 'hve-developer'
1062 $updatedJson.displayName | Should -Be 'HVE Core - Developer Edition'
1063 $updatedJson.description | Should -Be 'Developer edition'
1064 $updatedJson.publisher | Should -Be 'ise-hve-essentials'
1065 }
1066
1067 It 'Does not create package.json.bak backup (Option A uses git restore)' {
1068 $result = Invoke-PrepareExtension `
1069 -ExtensionDirectory $script:extDir `
1070 -RepoRoot $script:tempDir `
1071 -Channel 'Stable' `
1072 -Collection $script:devCollectionPath `
1073 -DryRun
1074
1075 $result.Success | Should -BeTrue
1076 $bakPath = Join-Path $script:extDir 'package.json.bak'
1077 Test-Path $bakPath | Should -BeFalse
1078 }
1079 }
1080}
1081