microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat-ds-agent

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

1392lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4
5BeforeAll {
6 . $PSScriptRoot/../../extension/Package-Extension.ps1
7 Import-Module "$PSScriptRoot/../Mocks/GitMocks.psm1" -Force
8 Import-Module "$PSScriptRoot/../../lib/Modules/CIHelpers.psm1" -Force
9}
10
11Describe 'Test-VsceAvailable' {
12 It 'Returns hashtable with IsAvailable property' {
13 $result = Test-VsceAvailable
14 $result | Should -BeOfType [hashtable]
15 $result.Keys | Should -Contain 'IsAvailable'
16 }
17
18 It 'Returns CommandType when available' {
19 $result = Test-VsceAvailable
20 if ($result.IsAvailable) {
21 $result.CommandType | Should -BeIn @('npx', 'vsce')
22 $result.Command | Should -Not -BeNullOrEmpty
23 }
24 }
25
26 It 'Returns vsce when vsce command is found' {
27 Mock Get-Command {
28 param($Name, $ErrorAction)
29 $null = $ErrorAction # Suppress PSScriptAnalyzer warning
30 if ($Name -eq 'vsce') {
31 return [PSCustomObject]@{ Source = 'C:\bin\vsce.cmd' }
32 }
33 return $null
34 }
35 $result = Test-VsceAvailable
36 $result.IsAvailable | Should -BeTrue
37 $result.CommandType | Should -Be 'vsce'
38 $result.Command | Should -Be 'C:\bin\vsce.cmd'
39 }
40
41 It 'Returns npx when only npx command is found' {
42 Mock Get-Command {
43 param($Name, $ErrorAction)
44 $null = $ErrorAction # Suppress PSScriptAnalyzer warning
45 if ($Name -eq 'npx') {
46 return [PSCustomObject]@{ Source = 'C:\bin\npx.cmd' }
47 }
48 return $null
49 }
50 $result = Test-VsceAvailable
51 $result.IsAvailable | Should -BeTrue
52 $result.CommandType | Should -Be 'npx'
53 $result.Command | Should -Be 'C:\bin\npx.cmd'
54 }
55
56 It 'Returns not available when neither vsce nor npx exist' {
57 Mock Get-Command { return $null }
58 $result = Test-VsceAvailable
59 $result.IsAvailable | Should -BeFalse
60 $result.CommandType | Should -BeNullOrEmpty
61 $result.Command | Should -BeNullOrEmpty
62 }
63}
64
65Describe 'Test-ExtensionManifestValid' {
66 It 'Returns valid result for proper manifest' {
67 $manifest = [PSCustomObject]@{
68 name = 'my-extension'
69 version = '1.0.0'
70 publisher = 'my-publisher'
71 engines = [PSCustomObject]@{ vscode = '^1.80.0' }
72 }
73 $result = Test-ExtensionManifestValid -ManifestContent $manifest
74 $result.IsValid | Should -BeTrue
75 $result.Errors | Should -BeNullOrEmpty
76 }
77
78 It 'Returns invalid when name missing' {
79 $manifest = @{
80 version = '1.0.0'
81 publisher = 'pub'
82 engines = @{ vscode = '^1.80.0' }
83 }
84 $result = Test-ExtensionManifestValid -ManifestContent $manifest
85 $result.IsValid | Should -BeFalse
86 $result.Errors | Should -Contain "Missing required 'name' field"
87 }
88
89 It 'Returns invalid when version missing' {
90 $manifest = @{
91 name = 'ext'
92 publisher = 'pub'
93 engines = @{ vscode = '^1.80.0' }
94 }
95 $result = Test-ExtensionManifestValid -ManifestContent $manifest
96 $result.IsValid | Should -BeFalse
97 $result.Errors | Should -Contain "Missing required 'version' field"
98 }
99
100 It 'Returns invalid when publisher missing' {
101 $manifest = @{
102 name = 'ext'
103 version = '1.0.0'
104 engines = @{ vscode = '^1.80.0' }
105 }
106 $result = Test-ExtensionManifestValid -ManifestContent $manifest
107 $result.IsValid | Should -BeFalse
108 $result.Errors | Should -Contain "Missing required 'publisher' field"
109 }
110
111 It 'Returns invalid when engines.vscode missing' {
112 $manifest = @{
113 name = 'ext'
114 version = '1.0.0'
115 publisher = 'pub'
116 }
117 $result = Test-ExtensionManifestValid -ManifestContent $manifest
118 $result.IsValid | Should -BeFalse
119 $result.Errors | Should -Contain "Missing required 'engines' field"
120 }
121
122 It 'Returns invalid when engines exists but vscode key missing' {
123 $manifest = [PSCustomObject]@{
124 name = 'ext'
125 version = '1.0.0'
126 publisher = 'pub'
127 engines = [PSCustomObject]@{ node = '>=16' }
128 }
129 $result = Test-ExtensionManifestValid -ManifestContent $manifest
130 $result.IsValid | Should -BeFalse
131 $result.Errors | Should -Contain "Missing required 'engines.vscode' field"
132 }
133
134 It 'Returns invalid when version format is incorrect' {
135 $manifest = [PSCustomObject]@{
136 name = 'ext'
137 version = 'invalid-version'
138 publisher = 'pub'
139 engines = [PSCustomObject]@{ vscode = '^1.80.0' }
140 }
141 $result = Test-ExtensionManifestValid -ManifestContent $manifest
142 $result.IsValid | Should -BeFalse
143 $result.Errors | Should -Match 'Invalid version format'
144 }
145
146 It 'Collects multiple errors' {
147 $manifest = @{}
148 $result = Test-ExtensionManifestValid -ManifestContent $manifest
149 $result.IsValid | Should -BeFalse
150 $result.Errors.Count | Should -BeGreaterThan 1
151 }
152}
153
154Describe 'Get-VscePackageCommand' {
155 It 'Returns npx command structure for npx type' {
156 $result = Get-VscePackageCommand -CommandType 'npx'
157 $result.Executable | Should -Be 'npx'
158 $result.Arguments | Should -Contain '@vscode/vsce'
159 $result.Arguments | Should -Contain 'package'
160 }
161
162 It 'Returns vsce command for vsce type' {
163 $result = Get-VscePackageCommand -CommandType 'vsce'
164 $result.Executable | Should -Be 'vsce'
165 $result.Arguments | Should -Contain 'package'
166 }
167
168 It 'Includes --pre-release flag when specified' {
169 $result = Get-VscePackageCommand -CommandType 'npx' -PreRelease
170 $result.Arguments | Should -Contain '--pre-release'
171 }
172
173 It 'Excludes --pre-release flag when not specified' {
174 $result = Get-VscePackageCommand -CommandType 'npx'
175 $result.Arguments | Should -Not -Contain '--pre-release'
176 }
177}
178
179Describe 'New-PackagingResult' {
180 BeforeAll {
181 $script:testVsixPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath().TrimEnd([System.IO.Path]::DirectorySeparatorChar), 'ext.vsix')
182 }
183
184 It 'Creates success result with all properties' {
185 $result = New-PackagingResult -Success $true -OutputPath $script:testVsixPath -Version '1.0.0' -ErrorMessage $null
186 $result.Success | Should -BeTrue
187 $result.OutputPath | Should -Be $script:testVsixPath
188 $result.Version | Should -Be '1.0.0'
189 $result.ErrorMessage | Should -BeNullOrEmpty
190 }
191
192 It 'Creates failure result with error message' {
193 $result = New-PackagingResult -Success $false -OutputPath $null -Version $null -ErrorMessage 'Packaging failed'
194 $result.Success | Should -BeFalse
195 $result.ErrorMessage | Should -Be 'Packaging failed'
196 }
197
198 It 'Creates result with default empty strings for optional parameters' {
199 $result = New-PackagingResult -Success $true
200 $result.Success | Should -BeTrue
201 $result.OutputPath | Should -Be ''
202 $result.Version | Should -Be ''
203 $result.ErrorMessage | Should -Be ''
204 }
205
206 It 'Creates failure result with only error message specified' {
207 $result = New-PackagingResult -Success $false -ErrorMessage 'Something went wrong'
208 $result.Success | Should -BeFalse
209 $result.ErrorMessage | Should -Be 'Something went wrong'
210 $result.OutputPath | Should -Be ''
211 $result.Version | Should -Be ''
212 }
213}
214
215Describe 'Get-ResolvedPackageVersion' {
216 It 'Returns specified version when provided' {
217 $result = Get-ResolvedPackageVersion -SpecifiedVersion '2.0.0' -ManifestVersion '1.0.0' -DevPatchNumber ''
218 $result.IsValid | Should -BeTrue
219 $result.PackageVersion | Should -Be '2.0.0'
220 }
221
222 It 'Returns manifest version when no specified version' {
223 $result = Get-ResolvedPackageVersion -SpecifiedVersion '' -ManifestVersion '1.5.0' -DevPatchNumber ''
224 $result.IsValid | Should -BeTrue
225 $result.PackageVersion | Should -Be '1.5.0'
226 }
227
228 It 'Applies dev patch number when provided' {
229 $result = Get-ResolvedPackageVersion -SpecifiedVersion '' -ManifestVersion '1.0.0' -DevPatchNumber '42'
230 $result.IsValid | Should -BeTrue
231 $result.PackageVersion | Should -Be '1.0.0-dev.42'
232 }
233
234 It 'Specified version with dev patch appends dev suffix' {
235 $result = Get-ResolvedPackageVersion -SpecifiedVersion '3.0.0' -ManifestVersion '1.0.0' -DevPatchNumber '99'
236 $result.IsValid | Should -BeTrue
237 $result.PackageVersion | Should -Be '3.0.0-dev.99'
238 }
239
240 It 'Returns invalid for malformed specified version' {
241 $result = Get-ResolvedPackageVersion -SpecifiedVersion 'not-a-version' -ManifestVersion '1.0.0' -DevPatchNumber ''
242 $result.IsValid | Should -BeFalse
243 $result.ErrorMessage | Should -Match 'Invalid version format specified'
244 }
245
246 It 'Returns invalid for malformed manifest version when no specified version' {
247 $result = Get-ResolvedPackageVersion -SpecifiedVersion '' -ManifestVersion 'bad-version' -DevPatchNumber ''
248 $result.IsValid | Should -BeFalse
249 $result.ErrorMessage | Should -Match 'Invalid version format in package.json'
250 }
251
252 It 'Extracts base version from manifest with prerelease suffix' {
253 $result = Get-ResolvedPackageVersion -SpecifiedVersion '' -ManifestVersion '1.2.3-beta.1' -DevPatchNumber ''
254 $result.IsValid | Should -BeTrue
255 $result.BaseVersion | Should -Be '1.2.3'
256 $result.PackageVersion | Should -Be '1.2.3'
257 }
258
259 It 'Returns BaseVersion correctly when specified version provided' {
260 $result = Get-ResolvedPackageVersion -SpecifiedVersion '4.5.6' -ManifestVersion '1.0.0' -DevPatchNumber ''
261 $result.IsValid | Should -BeTrue
262 $result.BaseVersion | Should -Be '4.5.6'
263 }
264}
265
266Describe 'Invoke-PackageExtension' {
267 BeforeAll {
268 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "pkg-ext-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
269 $script:extDir = Join-Path $script:testRoot 'extension'
270 $script:repoRoot = Join-Path $script:testRoot 'repo'
271 }
272
273 BeforeEach {
274 # Create fresh test directories for each test
275 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
276 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
277 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
278 New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null
279 New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null
280 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
281 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
282 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
283 }
284
285 AfterEach {
286 if (Test-Path $script:testRoot) {
287 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
288 }
289 }
290
291 It 'Returns failure when extension directory does not exist' {
292 $nonexistentPath = Join-Path ([System.IO.Path]::GetTempPath()) "nonexistent-ext-$([guid]::NewGuid().ToString('N').Substring(0,8))"
293 $result = Invoke-PackageExtension -ExtensionDirectory $nonexistentPath -RepoRoot $script:repoRoot
294 $result.Success | Should -BeFalse
295 $result.ErrorMessage | Should -Match 'Extension directory not found'
296 }
297
298 It 'Returns failure when package.json missing' {
299 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
300 $result.Success | Should -BeFalse
301 $result.ErrorMessage | Should -Match 'package.json not found'
302 }
303
304 It 'Returns failure when .github directory missing' {
305 # Create package.json but remove .github
306 $manifest = @{
307 name = 'test-ext'
308 version = '1.0.0'
309 publisher = 'test'
310 engines = @{ vscode = '^1.80.0' }
311 }
312 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
313 Remove-Item -Path (Join-Path $script:repoRoot '.github') -Recurse -Force
314
315 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
316 $result.Success | Should -BeFalse
317 $result.ErrorMessage | Should -Match '.github directory not found'
318 }
319
320 It 'Returns failure for invalid JSON in package.json' {
321 Set-Content -Path (Join-Path $script:extDir 'package.json') -Value '{ invalid json }'
322
323 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
324 $result.Success | Should -BeFalse
325 $result.ErrorMessage | Should -Match 'Failed to parse package.json'
326 }
327
328 It 'Returns failure for invalid manifest missing required fields' {
329 $manifest = @{ name = 'only-name' } # Missing version, publisher, engines
330 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
331
332 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
333 $result.Success | Should -BeFalse
334 $result.ErrorMessage | Should -Match 'Invalid package.json'
335 }
336
337 It 'Returns failure for invalid specified version format' {
338 $manifest = @{
339 name = 'test-ext'
340 version = '1.0.0'
341 publisher = 'test'
342 engines = @{ vscode = '^1.80.0' }
343 }
344 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
345
346 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -Version 'invalid-version'
347 $result.Success | Should -BeFalse
348 $result.ErrorMessage | Should -Match 'Invalid version format'
349 }
350
351 It 'Returns structured result hashtable with expected keys' {
352 Mock Test-VsceAvailable { return @{ IsAvailable = $false; CommandType = ''; Command = '' } }
353
354 $manifest = @{
355 name = 'test-ext'
356 version = '1.0.0'
357 publisher = 'test'
358 engines = @{ vscode = '^1.80.0' }
359 }
360 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
361
362 # Will fail at vsce availability check, validates structure
363 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
364
365 $result | Should -BeOfType [hashtable]
366 $result.Keys | Should -Contain 'Success'
367 $result.Keys | Should -Contain 'ErrorMessage'
368 }
369
370 It 'Applies DevPatchNumber to version correctly' {
371 Mock Test-VsceAvailable { return @{ IsAvailable = $false; CommandType = ''; Command = '' } }
372
373 $manifest = @{
374 name = 'test-ext'
375 version = '2.0.0'
376 publisher = 'test'
377 engines = @{ vscode = '^1.80.0' }
378 }
379 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
380
381 # Will fail at vsce availability check, validates version resolution path
382 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -DevPatchNumber '123'
383
384 # Even on failure, the result indicates version was processed
385 $result | Should -BeOfType [hashtable]
386 }
387
388 It 'Copies changelog when valid path provided' {
389 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
390 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
391
392 $manifest = @{
393 name = 'test-ext'
394 version = '1.0.0'
395 publisher = 'test'
396 engines = @{ vscode = '^1.80.0' }
397 }
398 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
399
400 # Create a changelog file
401 $changelogPath = Join-Path $script:repoRoot 'CHANGELOG.md'
402 Set-Content -Path $changelogPath -Value '# Changelog'
403
404 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -ChangelogPath $changelogPath
405
406 # Changelog should be copied to extension directory
407 $destChangelog = Join-Path $script:extDir 'CHANGELOG.md'
408 Test-Path $destChangelog | Should -BeTrue
409 $result | Should -Not -BeNullOrEmpty
410 }
411
412 It 'Warns when changelog path does not exist' {
413 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
414 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
415 Mock Write-Warning { }
416
417 $manifest = @{
418 name = 'test-ext'
419 version = '1.0.0'
420 publisher = 'test'
421 engines = @{ vscode = '^1.80.0' }
422 }
423 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
424
425 $nonexistentChangelog = Join-Path ([System.IO.Path]::GetTempPath()) "changelog-$([guid]::NewGuid().ToString('N').Substring(0,8)).md"
426 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -ChangelogPath $nonexistentChangelog
427
428 Should -Invoke Write-Warning -Times 1
429 $result | Should -Not -BeNullOrEmpty
430 }
431
432 It 'Returns failure when vsce command fails with non-zero exit code' {
433 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
434 Mock Get-VscePackageCommand { return @{ Executable = 'pwsh'; Arguments = @('-Command', 'exit 1') } }
435
436 $manifest = @{
437 name = 'test-ext'
438 version = '1.0.0'
439 publisher = 'test'
440 engines = @{ vscode = '^1.80.0' }
441 }
442 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
443
444 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
445
446 $result.Success | Should -BeFalse
447 $result.ErrorMessage | Should -Match 'vsce package command failed|The term'
448 }
449
450 It 'Returns failure when CIHelpers.psm1 missing' {
451 # Create package.json and .github, but remove CIHelpers.psm1
452 $manifest = @{
453 name = 'test-ext'
454 version = '1.0.0'
455 publisher = 'test'
456 engines = @{ vscode = '^1.80.0' }
457 }
458 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
459 Remove-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Force
460
461 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
462 $result.Success | Should -BeFalse
463 $result.ErrorMessage | Should -Match 'CIHelpers.psm1 not found'
464 }
465
466 Context 'Package.json backup restore' {
467 It 'Does not create backup when no collection specified' {
468 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
469 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
470
471 $manifest = @{
472 name = 'test-ext'
473 version = '1.0.0'
474 publisher = 'test'
475 engines = @{ vscode = '^1.80.0' }
476 }
477 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
478
479 # Create fake vsix so packaging succeeds
480 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
481 Set-Content -Path $vsixPath -Value 'fake-vsix'
482
483 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
484
485 Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse
486 }
487
488 It 'Restores package.json from backup after packaging' {
489 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
490 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
491
492 # Original package.json content (will be overwritten by template)
493 $originalManifest = @{
494 name = 'hve-core'
495 version = '1.0.0'
496 publisher = 'test'
497 engines = @{ vscode = '^1.80.0' }
498 }
499
500 # Simulate post-template state: template content in package.json, original backed up
501 $templateManifest = @{
502 name = 'hve-developer'
503 version = '1.0.0'
504 publisher = 'test'
505 engines = @{ vscode = '^1.80.0' }
506 }
507 $templateManifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
508 $originalManifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json.bak')
509
510 # Create fake vsix so packaging succeeds
511 $vsixPath = Join-Path $script:extDir 'hve-developer-1.0.0.vsix'
512 Set-Content -Path $vsixPath -Value 'fake-vsix'
513
514 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
515
516 # Verify the original manifest was restored
517 $restored = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
518 $restored.name | Should -Be 'hve-core'
519 }
520
521 It 'Removes backup file after restore' {
522 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
523 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
524
525 $manifest = @{
526 name = 'test-ext'
527 version = '1.0.0'
528 publisher = 'test'
529 engines = @{ vscode = '^1.80.0' }
530 }
531 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
532
533 # Create a backup file manually to simulate Invoke-PrepareExtension behavior
534 $backupManifest = @{
535 name = 'original-ext'
536 version = '1.0.0'
537 publisher = 'test'
538 engines = @{ vscode = '^1.80.0' }
539 }
540 $backupManifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json.bak')
541
542 # Create fake vsix so packaging succeeds
543 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
544 Set-Content -Path $vsixPath -Value 'fake-vsix'
545
546 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
547
548 Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse
549 }
550
551 It 'Restored package.json contains original metadata' {
552 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
553 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
554
555 # Original manifest backed up before template was applied
556 $originalManifest = @{
557 name = 'hve-core-original'
558 version = '2.5.0'
559 publisher = 'original-pub'
560 description = 'Original description'
561 engines = @{ vscode = '^1.80.0' }
562 }
563
564 # Template manifest currently in package.json
565 $templateManifest = @{
566 name = 'hve-test-collection'
567 version = '2.5.0'
568 publisher = 'test-pub'
569 description = 'Test description'
570 engines = @{ vscode = '^1.80.0' }
571 }
572 $templateManifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
573 $originalManifest | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $script:extDir 'package.json.bak')
574
575 # Create fake vsix matching the template name
576 $vsixPath = Join-Path $script:extDir 'hve-test-collection-2.5.0.vsix'
577 Set-Content -Path $vsixPath -Value 'fake-vsix'
578
579 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
580
581 $restored = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json
582 $restored.name | Should -Be 'hve-core-original'
583 $restored.publisher | Should -Be 'original-pub'
584 $restored.description | Should -Be 'Original description'
585 }
586 }
587
588 It 'Cleans pre-existing copied directories before preparing extension' {
589 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
590 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
591
592 $manifest = @{
593 name = 'test-ext'
594 version = '1.0.0'
595 publisher = 'test'
596 engines = @{ vscode = '^1.80.0' }
597 }
598 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
599
600 # Pre-create directories that should be cleaned before packaging
601 $preExistingGithub = Join-Path $script:extDir '.github/stale'
602 $preExistingScripts = Join-Path $script:extDir 'scripts/old'
603 New-Item -Path $preExistingGithub -ItemType Directory -Force | Out-Null
604 Set-Content -Path (Join-Path $preExistingGithub 'leftover.md') -Value 'stale'
605 New-Item -Path $preExistingScripts -ItemType Directory -Force | Out-Null
606 Set-Content -Path (Join-Path $preExistingScripts 'leftover.ps1') -Value 'stale'
607
608 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
609 Set-Content -Path $vsixPath -Value 'fake-vsix'
610
611 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
612
613 # Stale files should have been removed during pre-clean
614 $result | Should -BeOfType [hashtable]
615 }
616
617 It 'Returns failure when an unexpected error occurs during orchestration' {
618 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
619 Mock Get-PackagingDirectorySpec { throw 'Simulated unexpected failure' }
620
621 $manifest = @{
622 name = 'test-ext'
623 version = '1.0.0'
624 publisher = 'test'
625 engines = @{ vscode = '^1.80.0' }
626 }
627 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
628
629 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
630
631 $result.Success | Should -BeFalse
632 $result.ErrorMessage | Should -Match 'Simulated unexpected failure'
633 }
634}
635
636Describe 'Test-PackagingInputsValid' {
637 BeforeAll {
638 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "pkg-inputs-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
639 $script:extDir = Join-Path $script:testRoot 'extension'
640 $script:repoRoot = Join-Path $script:testRoot 'repo'
641 }
642
643 BeforeEach {
644 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
645 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
646 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
647 New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null
648 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
649 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock'
650 Set-Content -Path (Join-Path $script:extDir 'package.json') -Value '{}'
651 }
652
653 AfterEach {
654 if (Test-Path $script:testRoot) {
655 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
656 }
657 }
658
659 It 'Returns valid when all paths exist' {
660 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
661 $result.IsValid | Should -BeTrue
662 $result.Errors | Should -BeNullOrEmpty
663 }
664
665 It 'Returns resolved paths in result' {
666 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
667 $result.PackageJsonPath | Should -BeLike '*package.json'
668 $result.GitHubDir | Should -BeLike '*.github'
669 $result.CIHelpersPath | Should -BeLike '*CIHelpers.psm1'
670 }
671
672 It 'Returns error when extension directory not found' {
673 $nonexistent = Join-Path ([System.IO.Path]::GetTempPath()) "nonexistent-$([guid]::NewGuid().ToString('N').Substring(0,8))"
674 $result = Test-PackagingInputsValid -ExtensionDirectory $nonexistent -RepoRoot $script:repoRoot
675 $result.IsValid | Should -BeFalse
676 # Function accumulates multiple errors; extension dir missing cascades to package.json missing
677 $result.Errors | Should -Match 'Extension directory not found|package.json not found'
678 }
679
680 It 'Returns error when package.json not found' {
681 Remove-Item -Path (Join-Path $script:extDir 'package.json') -Force
682 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
683 $result.IsValid | Should -BeFalse
684 $result.Errors | Should -Match 'package.json not found'
685 }
686
687 It 'Returns error when .github directory not found' {
688 Remove-Item -Path (Join-Path $script:repoRoot '.github') -Recurse -Force
689 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
690 $result.IsValid | Should -BeFalse
691 $result.Errors | Should -Match '.github directory not found'
692 }
693
694 It 'Returns error when CIHelpers.psm1 not found' {
695 Remove-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Force
696 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
697 $result.IsValid | Should -BeFalse
698 $result.Errors | Should -Match 'CIHelpers.psm1 not found'
699 }
700
701 It 'Collects multiple errors' {
702 Remove-Item -Path (Join-Path $script:extDir 'package.json') -Force
703 Remove-Item -Path (Join-Path $script:repoRoot '.github') -Recurse -Force
704 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
705 $result.IsValid | Should -BeFalse
706 $result.Errors.Count | Should -BeGreaterOrEqual 2
707 }
708}
709
710Describe 'Get-PackagingDirectorySpec' {
711 BeforeAll {
712 # Use platform-agnostic temp paths for cross-platform CI compatibility
713 $script:repoRoot = Join-Path ([System.IO.Path]::GetTempPath()) 'spec-repo'
714 $script:extDir = Join-Path ([System.IO.Path]::GetTempPath()) 'spec-ext'
715 }
716
717 It 'Returns array of 4 directory specifications' {
718 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
719 $result.Count | Should -Be 4
720 }
721
722 It 'Includes .github directory specification' {
723 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
724 $githubSpec = $result | Where-Object { $_.Source -like '*.github' }
725 $githubSpec | Should -Not -BeNullOrEmpty
726 $githubSpec.Destination | Should -BeLike '*.github'
727 $githubSpec.IsFile | Should -BeFalse
728 }
729
730 It 'Includes dev-tools directory specification' {
731 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
732 $devToolsSpec = $result | Where-Object { $_.Source -like '*dev-tools' }
733 $devToolsSpec | Should -Not -BeNullOrEmpty
734 $devToolsSpec.IsFile | Should -BeFalse
735 }
736
737 It 'Includes CIHelpers.psm1 file specification' {
738 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
739 $ciHelpersSpec = $result | Where-Object { $_.Source -like '*CIHelpers.psm1' }
740 $ciHelpersSpec | Should -Not -BeNullOrEmpty
741 $ciHelpersSpec.IsFile | Should -BeTrue
742 }
743
744 It 'Includes docs/templates directory specification' {
745 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
746 $templatesSpec = $result | Where-Object { $_.Source -like '*templates' }
747 $templatesSpec | Should -Not -BeNullOrEmpty
748 $templatesSpec.IsFile | Should -BeFalse
749 }
750
751 It 'Uses correct path joining for source and destination' {
752 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
753 foreach ($spec in $result) {
754 $spec.Source | Should -Not -BeNullOrEmpty
755 $spec.Destination | Should -Not -BeNullOrEmpty
756 }
757 }
758}
759
760Describe 'Invoke-VsceCommand' {
761 BeforeAll {
762 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "vsce-cmd-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
763 }
764
765 BeforeEach {
766 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
767 }
768
769 AfterEach {
770 if (Test-Path $script:testDir) {
771 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
772 }
773 }
774
775 It 'Returns hashtable with Success and ExitCode' {
776 $result = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 0') -WorkingDirectory $script:testDir
777 $result | Should -BeOfType [hashtable]
778 $result.Keys | Should -Contain 'Success'
779 $result.Keys | Should -Contain 'ExitCode'
780 }
781
782 It 'Returns Success true for zero exit code' {
783 $result = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 0') -WorkingDirectory $script:testDir
784 $result.Success | Should -BeTrue
785 $result.ExitCode | Should -Be 0
786 }
787
788 It 'Returns Success false for non-zero exit code' {
789 $result = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 42') -WorkingDirectory $script:testDir
790 $result.Success | Should -BeFalse
791 $result.ExitCode | Should -Be 42
792 }
793
794 It 'Restores working directory after execution' {
795 $originalDir = Get-Location
796 $null = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 0') -WorkingDirectory $script:testDir
797 (Get-Location).Path | Should -Be $originalDir.Path
798 }
799
800 It 'Uses cmd wrapper when UseWindowsWrapper specified with npx' -Skip:(-not $IsWindows) {
801 # Test that cmd wrapper path executes without error
802 # npx --help outputs text to the pipeline alongside the hashtable return value
803 $output = Invoke-VsceCommand -Executable 'npx' -Arguments @('--help') -WorkingDirectory $script:testDir -UseWindowsWrapper
804 # Filter for the hashtable return (command output also flows through pipeline)
805 $result = $output | Where-Object { $_ -is [hashtable] }
806 $result | Should -Not -BeNullOrEmpty
807 $result.Keys | Should -Contain 'Success'
808 }
809}
810
811Describe 'Remove-PackagingArtifacts' {
812 BeforeAll {
813 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "rm-artifacts-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
814 }
815
816 BeforeEach {
817 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
818 }
819
820 AfterEach {
821 if (Test-Path $script:testDir) {
822 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
823 }
824 }
825
826 It 'Removes existing directories' {
827 New-Item -Path (Join-Path $script:testDir '.github') -ItemType Directory -Force | Out-Null
828 New-Item -Path (Join-Path $script:testDir 'scripts') -ItemType Directory -Force | Out-Null
829
830 Remove-PackagingArtifacts -ExtensionDirectory $script:testDir
831
832 Test-Path (Join-Path $script:testDir '.github') | Should -BeFalse
833 Test-Path (Join-Path $script:testDir 'scripts') | Should -BeFalse
834 }
835
836 It 'Silently skips non-existent directories' {
837 { Remove-PackagingArtifacts -ExtensionDirectory $script:testDir } | Should -Not -Throw
838 }
839
840 It 'Uses custom directory names when specified' {
841 New-Item -Path (Join-Path $script:testDir 'custom1') -ItemType Directory -Force | Out-Null
842 New-Item -Path (Join-Path $script:testDir 'custom2') -ItemType Directory -Force | Out-Null
843
844 Remove-PackagingArtifacts -ExtensionDirectory $script:testDir -DirectoryNames @('custom1', 'custom2')
845
846 Test-Path (Join-Path $script:testDir 'custom1') | Should -BeFalse
847 Test-Path (Join-Path $script:testDir 'custom2') | Should -BeFalse
848 }
849
850 It 'Removes nested contents recursively' {
851 $nestedDir = Join-Path $script:testDir '.github/nested/deep'
852 New-Item -Path $nestedDir -ItemType Directory -Force | Out-Null
853 Set-Content -Path (Join-Path $nestedDir 'file.txt') -Value 'content'
854
855 Remove-PackagingArtifacts -ExtensionDirectory $script:testDir -DirectoryNames @('.github')
856
857 Test-Path (Join-Path $script:testDir '.github') | Should -BeFalse
858 }
859}
860
861Describe 'Restore-PackageJsonVersion' {
862 BeforeAll {
863 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "restore-ver-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
864 }
865
866 BeforeEach {
867 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
868 }
869
870 AfterEach {
871 if (Test-Path $script:testDir) {
872 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
873 }
874 }
875
876 It 'Restores original version to package.json' {
877 $packageJsonPath = Join-Path $script:testDir 'package.json'
878 $packageJson = @{ name = 'test'; version = '2.0.0' }
879 $packageJson | ConvertTo-Json | Set-Content -Path $packageJsonPath
880
881 $obj = Get-Content $packageJsonPath | ConvertFrom-Json
882 Restore-PackageJsonVersion -PackageJsonPath $packageJsonPath -PackageJson $obj -OriginalVersion '1.0.0'
883
884 $updated = Get-Content $packageJsonPath | ConvertFrom-Json
885 $updated.version | Should -Be '1.0.0'
886 }
887
888 It 'Returns early when OriginalVersion is null' {
889 $packageJsonPath = Join-Path $script:testDir 'package.json'
890 $packageJson = @{ name = 'test'; version = '2.0.0' }
891 $packageJson | ConvertTo-Json | Set-Content -Path $packageJsonPath
892
893 $obj = Get-Content $packageJsonPath | ConvertFrom-Json
894 { Restore-PackageJsonVersion -PackageJsonPath $packageJsonPath -PackageJson $obj -OriginalVersion $null } | Should -Not -Throw
895
896 $unchanged = Get-Content $packageJsonPath | ConvertFrom-Json
897 $unchanged.version | Should -Be '2.0.0'
898 }
899
900 It 'Returns early when PackageJson is null' {
901 $packageJsonPath = Join-Path $script:testDir 'package.json'
902 Set-Content -Path $packageJsonPath -Value '{"version": "2.0.0"}'
903
904 { Restore-PackageJsonVersion -PackageJsonPath $packageJsonPath -PackageJson $null -OriginalVersion '1.0.0' } | Should -Not -Throw
905
906 $unchanged = Get-Content $packageJsonPath | ConvertFrom-Json
907 $unchanged.version | Should -Be '2.0.0'
908 }
909
910 It 'Returns early when PackageJsonPath is null' {
911 $packageJson = @{ name = 'test'; version = '2.0.0' }
912 { Restore-PackageJsonVersion -PackageJsonPath $null -PackageJson $packageJson -OriginalVersion '1.0.0' } | Should -Not -Throw
913 }
914
915 It 'Handles write failure gracefully' {
916 Mock Write-Warning {}
917 $invalidPath = Join-Path $script:testDir 'nonexistent/package.json'
918 $packageJson = [PSCustomObject]@{ name = 'test'; version = '2.0.0' }
919
920 { Restore-PackageJsonVersion -PackageJsonPath $invalidPath -PackageJson $packageJson -OriginalVersion '1.0.0' } | Should -Not -Throw
921 }
922}
923
924Describe 'Get-CollectionReadmePath' {
925 BeforeAll {
926 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "collection-readme-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
927 $script:extDir = Join-Path $script:testDir 'extension'
928 }
929
930 BeforeEach {
931 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
932 }
933
934 AfterEach {
935 if (Test-Path $script:testDir) {
936 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
937 }
938 }
939
940 It 'Returns null for hve-core-all collection' {
941 $collectionPath = Join-Path $script:testDir 'collection.yml'
942 @"
943id: hve-core-all
944name: all
945"@ | Set-Content $collectionPath
946
947 $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir
948 $result | Should -BeNullOrEmpty
949 }
950
951 It 'Returns collection README path when file exists' {
952 $collectionPath = Join-Path $script:testDir 'collection.yml'
953 @"
954id: developer
955name: dev
956"@ | Set-Content $collectionPath
957
958 $collectionReadme = Join-Path $script:extDir 'README.developer.md'
959 Set-Content -Path $collectionReadme -Value '# Developer README'
960
961 $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir
962 $result | Should -Be $collectionReadme
963 }
964
965 It 'Returns null when collection README file does not exist' {
966 $collectionPath = Join-Path $script:testDir 'collection.yml'
967 @"
968id: security
969name: sec
970"@ | Set-Content $collectionPath
971
972 $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir
973 $result | Should -BeNullOrEmpty
974 }
975}
976
977Describe 'Set-CollectionReadme' {
978 BeforeAll {
979 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "set-readme-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
980 }
981
982 BeforeEach {
983 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
984 Set-Content -Path (Join-Path $script:testDir 'README.md') -Value '# Original README'
985 }
986
987 AfterEach {
988 if (Test-Path $script:testDir) {
989 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
990 }
991 }
992
993 It 'Swaps README.md with collection README and creates backup' {
994 $collectionReadmePath = Join-Path $script:testDir 'README.developer.md'
995 Set-Content -Path $collectionReadmePath -Value '# Developer README'
996
997 Set-CollectionReadme -ExtensionDirectory $script:testDir -CollectionReadmePath $collectionReadmePath -Operation Swap
998
999 $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw
1000 $readmeContent | Should -Match 'Developer README'
1001
1002 Test-Path (Join-Path $script:testDir 'README.md.bak') | Should -BeTrue
1003 $backupContent = Get-Content -Path (Join-Path $script:testDir 'README.md.bak') -Raw
1004 $backupContent | Should -Match 'Original README'
1005 }
1006
1007 It 'Warns and returns early when no collection path for swap' {
1008 Mock Write-Warning {}
1009 Set-CollectionReadme -ExtensionDirectory $script:testDir -Operation Swap
1010
1011 Should -Invoke Write-Warning -Times 1
1012 $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw
1013 $readmeContent | Should -Match 'Original README'
1014 }
1015
1016 It 'Restores README.md from backup and removes backup file' {
1017 # Create backup state
1018 Set-Content -Path (Join-Path $script:testDir 'README.md.bak') -Value '# Original README'
1019 Set-Content -Path (Join-Path $script:testDir 'README.md') -Value '# Collection README'
1020
1021 Set-CollectionReadme -ExtensionDirectory $script:testDir -Operation Restore
1022
1023 $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw
1024 $readmeContent | Should -Match 'Original README'
1025 Test-Path (Join-Path $script:testDir 'README.md.bak') | Should -BeFalse
1026 }
1027
1028 It 'Restore is a no-op when no backup exists' {
1029 { Set-CollectionReadme -ExtensionDirectory $script:testDir -Operation Restore } | Should -Not -Throw
1030 $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw
1031 $readmeContent | Should -Match 'Original README'
1032 }
1033}
1034
1035Describe 'Copy-CollectionArtifacts' {
1036 BeforeAll {
1037 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "copy-col-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
1038 $script:extDir = Join-Path $script:testDir 'extension'
1039 $script:repoRoot = Join-Path $script:testDir 'repo'
1040 }
1041
1042 BeforeEach {
1043 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
1044 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
1045 }
1046
1047 AfterEach {
1048 if (Test-Path $script:testDir) {
1049 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
1050 }
1051 }
1052
1053 It 'Copies agents from repo to extension directory' {
1054 # Create source agent
1055 $agentsSrc = Join-Path $script:repoRoot '.github/agents'
1056 New-Item -Path $agentsSrc -ItemType Directory -Force | Out-Null
1057 Set-Content -Path (Join-Path $agentsSrc 'task-planner.agent.md') -Value '# Agent'
1058
1059 # Create package.json with contributes referencing agents
1060 $pkgJson = @{
1061 contributes = @{
1062 chatAgents = @(
1063 @{ path = './.github/agents/task-planner.agent.md' }
1064 )
1065 }
1066 }
1067 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1068
1069 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1070
1071 Test-Path (Join-Path $script:extDir '.github/agents/task-planner.agent.md') | Should -BeTrue
1072 }
1073
1074 It 'Copies prompts from repo to extension directory' {
1075 # Create source prompt
1076 $promptsSrc = Join-Path $script:repoRoot '.github/prompts'
1077 New-Item -Path $promptsSrc -ItemType Directory -Force | Out-Null
1078 Set-Content -Path (Join-Path $promptsSrc 'my-prompt.prompt.md') -Value '# Prompt'
1079
1080 $pkgJson = @{
1081 contributes = @{
1082 chatPromptFiles = @(
1083 @{ path = './.github/prompts/my-prompt.prompt.md' }
1084 )
1085 }
1086 }
1087 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1088
1089 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1090
1091 Test-Path (Join-Path $script:extDir '.github/prompts/my-prompt.prompt.md') | Should -BeTrue
1092 }
1093
1094 It 'Copies instructions from repo to extension directory' {
1095 # Create source instruction
1096 $instrSrc = Join-Path $script:repoRoot '.github/instructions'
1097 New-Item -Path $instrSrc -ItemType Directory -Force | Out-Null
1098 Set-Content -Path (Join-Path $instrSrc 'commit-message.instructions.md') -Value '# Instructions'
1099
1100 $pkgJson = @{
1101 contributes = @{
1102 chatInstructions = @(
1103 @{ path = './.github/instructions/commit-message.instructions.md' }
1104 )
1105 }
1106 }
1107 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1108
1109 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1110
1111 Test-Path (Join-Path $script:extDir '.github/instructions/commit-message.instructions.md') | Should -BeTrue
1112 }
1113
1114 It 'Copies skills recursively from repo to extension directory' {
1115 # Create source skill with nested file
1116 $skillSrc = Join-Path $script:repoRoot '.github/skills/video-to-gif'
1117 New-Item -Path $skillSrc -ItemType Directory -Force | Out-Null
1118 Set-Content -Path (Join-Path $skillSrc 'SKILL.md') -Value '# Skill'
1119
1120 $pkgJson = @{
1121 contributes = @{
1122 chatSkills = @(
1123 @{ path = './.github/skills/video-to-gif' }
1124 )
1125 }
1126 }
1127 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1128
1129 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1130
1131 Test-Path (Join-Path $script:extDir '.github/skills/video-to-gif') | Should -BeTrue
1132 }
1133
1134 It 'Skips missing source files without error' {
1135 $pkgJson = @{
1136 contributes = @{
1137 chatAgents = @( @{ path = './.github/agents/nonexistent.agent.md' } )
1138 chatPromptFiles = @( @{ path = './.github/prompts/nonexistent.prompt.md' } )
1139 chatInstructions = @( @{ path = './.github/instructions/nonexistent.instructions.md' } )
1140 chatSkills = @( @{ path = './.github/skills/nonexistent' } )
1141 }
1142 }
1143 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1144
1145 { Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} } | Should -Not -Throw
1146 }
1147
1148 It 'Handles empty contributes sections' {
1149 $pkgJson = @{ contributes = @{} }
1150 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1151
1152 { Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} } | Should -Not -Throw
1153 }
1154}
1155
1156Describe 'Invoke-PackageExtension - Collection mode' {
1157 BeforeAll {
1158 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "pkg-col-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
1159 $script:extDir = Join-Path $script:testRoot 'extension'
1160 $script:repoRoot = Join-Path $script:testRoot 'repo'
1161 }
1162
1163 BeforeEach {
1164 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
1165 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
1166 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
1167 New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null
1168 New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null
1169 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
1170 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
1171 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
1172
1173 $manifest = @{
1174 name = 'test-ext'
1175 version = '1.0.0'
1176 publisher = 'test'
1177 engines = @{ vscode = '^1.80.0' }
1178 contributes = @{}
1179 }
1180 $manifest | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1181 Set-Content -Path (Join-Path $script:extDir 'README.md') -Value '# Default README'
1182 }
1183
1184 AfterEach {
1185 if (Test-Path $script:testRoot) {
1186 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
1187 }
1188 }
1189
1190 It 'Uses collection-filtered artifact copy when Collection specified' {
1191 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1192 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1193
1194 $collectionPath = Join-Path $script:testRoot 'collection.yml'
1195 @"
1196id: developer
1197name: dev
1198displayName: Developer
1199items:
1200 - developer
1201"@ | Set-Content $collectionPath
1202
1203 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1204 Set-Content -Path $vsixPath -Value 'fake-vsix'
1205
1206 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -Collection $collectionPath
1207 $result | Should -BeOfType [hashtable]
1208 }
1209
1210 It 'Swaps collection README when collection has matching collection README' {
1211 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1212 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1213
1214 $collectionPath = Join-Path $script:testRoot 'collection.yml'
1215 @"
1216id: developer
1217name: dev
1218displayName: Developer
1219items:
1220 - developer
1221"@ | Set-Content $collectionPath
1222
1223 # Create collection README in extension directory
1224 Set-Content -Path (Join-Path $script:extDir 'README.developer.md') -Value '# Developer Collection'
1225
1226 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1227 Set-Content -Path $vsixPath -Value 'fake-vsix'
1228
1229 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -Collection $collectionPath
1230
1231 # README should be restored after packaging completes
1232 $readmeContent = Get-Content -Path (Join-Path $script:extDir 'README.md') -Raw
1233 $readmeContent | Should -Match 'Default README'
1234 $result | Should -BeOfType [hashtable]
1235 }
1236
1237 It 'Returns failure when no vsix file generated after successful vsce command' {
1238 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1239 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1240
1241 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1242
1243 $result.Success | Should -BeFalse
1244 $result.ErrorMessage | Should -Match 'No .vsix file found after packaging'
1245 }
1246}
1247
1248Describe 'CI Integration - Package-Extension' {
1249 BeforeAll {
1250 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "ci-int-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
1251 $script:extDir = Join-Path $script:testRoot 'extension'
1252 $script:repoRoot = Join-Path $script:testRoot 'repo'
1253 }
1254
1255 AfterAll {
1256 if (Test-Path $script:testRoot) {
1257 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
1258 }
1259 }
1260
1261 Context 'GitHub Actions environment' {
1262 BeforeEach {
1263 Initialize-MockCIEnvironment
1264 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
1265 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
1266 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
1267 New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null
1268 New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null
1269 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
1270 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
1271 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
1272
1273 $manifest = @{
1274 name = 'test-ext'
1275 version = '1.0.0'
1276 publisher = 'test'
1277 engines = @{ vscode = '^1.80.0' }
1278 }
1279 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
1280 }
1281
1282 AfterEach {
1283 Clear-MockCIEnvironment
1284 if (Test-Path $script:testRoot) {
1285 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
1286 }
1287 }
1288
1289 It 'Sets version output variable on successful package' {
1290 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1291 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1292
1293 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1294 Set-Content -Path $vsixPath -Value 'fake-vsix'
1295
1296 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1297
1298 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
1299 $outputContent | Should -Match 'version=1\.0\.0'
1300 }
1301
1302 It 'Sets vsix-file output variable on successful package' {
1303 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1304 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1305
1306 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1307 Set-Content -Path $vsixPath -Value 'fake-vsix'
1308
1309 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1310
1311 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
1312 $outputContent | Should -Match 'vsix-file=test-ext-1\.0\.0\.vsix'
1313 }
1314
1315 It 'Sets pre-release output variable when PreRelease specified' {
1316 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1317 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1318
1319 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1320 Set-Content -Path $vsixPath -Value 'fake-vsix'
1321
1322 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -PreRelease
1323
1324 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
1325 $outputContent | Should -Match 'pre-release=True'
1326 }
1327
1328 It 'Sets pre-release output variable to false when PreRelease not specified' {
1329 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1330 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1331
1332 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1333 Set-Content -Path $vsixPath -Value 'fake-vsix'
1334
1335 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1336
1337 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
1338 $outputContent | Should -Match 'pre-release=False'
1339 }
1340
1341 It 'Returns failure result when vsce command fails' {
1342 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1343 Mock Get-VscePackageCommand { return @{ Executable = 'pwsh'; Arguments = @('-Command', 'exit 1') } }
1344
1345 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1346
1347 $result.Success | Should -BeFalse
1348 $result.ErrorMessage | Should -Match 'vsce package command failed'
1349 }
1350 }
1351
1352 Context 'Local environment' {
1353 BeforeEach {
1354 Clear-MockCIEnvironment
1355
1356 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
1357 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
1358 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
1359 New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null
1360 New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null
1361 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
1362 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
1363 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
1364
1365 $manifest = @{
1366 name = 'test-ext'
1367 version = '1.0.0'
1368 publisher = 'test'
1369 engines = @{ vscode = '^1.80.0' }
1370 }
1371 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
1372 }
1373
1374 AfterEach {
1375 if (Test-Path $script:testRoot) {
1376 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
1377 }
1378 }
1379
1380 It 'Completes without error when not in CI environment' {
1381 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1382 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1383
1384 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1385 Set-Content -Path $vsixPath -Value 'fake-vsix'
1386
1387 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1388
1389 $result.Success | Should -BeTrue
1390 }
1391 }
1392}
1393