microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/claude-support

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

913lines · 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 'Get-ExtensionOutputPath' {
66 BeforeAll {
67 $script:testDir = [System.IO.Path]::GetTempPath().TrimEnd([System.IO.Path]::DirectorySeparatorChar)
68 }
69
70 It 'Constructs correct output path' {
71 $result = Get-ExtensionOutputPath -ExtensionDirectory $script:testDir -ExtensionName 'my-extension' -PackageVersion '1.0.0'
72 $expected = [System.IO.Path]::Combine($script:testDir, 'my-extension-1.0.0.vsix')
73 $result | Should -Be $expected
74 }
75
76 It 'Handles pre-release version numbers' {
77 $result = Get-ExtensionOutputPath -ExtensionDirectory $script:testDir -ExtensionName 'ext' -PackageVersion '2.1.0-preview.1'
78 $expected = [System.IO.Path]::Combine($script:testDir, 'ext-2.1.0-preview.1.vsix')
79 $result | Should -Be $expected
80 }
81}
82
83Describe 'Test-ExtensionManifestValid' {
84 It 'Returns valid result for proper manifest' {
85 $manifest = [PSCustomObject]@{
86 name = 'my-extension'
87 version = '1.0.0'
88 publisher = 'my-publisher'
89 engines = [PSCustomObject]@{ vscode = '^1.80.0' }
90 }
91 $result = Test-ExtensionManifestValid -ManifestContent $manifest
92 $result.IsValid | Should -BeTrue
93 $result.Errors | Should -BeNullOrEmpty
94 }
95
96 It 'Returns invalid when name missing' {
97 $manifest = @{
98 version = '1.0.0'
99 publisher = 'pub'
100 engines = @{ vscode = '^1.80.0' }
101 }
102 $result = Test-ExtensionManifestValid -ManifestContent $manifest
103 $result.IsValid | Should -BeFalse
104 $result.Errors | Should -Contain "Missing required 'name' field"
105 }
106
107 It 'Returns invalid when version missing' {
108 $manifest = @{
109 name = 'ext'
110 publisher = 'pub'
111 engines = @{ vscode = '^1.80.0' }
112 }
113 $result = Test-ExtensionManifestValid -ManifestContent $manifest
114 $result.IsValid | Should -BeFalse
115 $result.Errors | Should -Contain "Missing required 'version' field"
116 }
117
118 It 'Returns invalid when publisher missing' {
119 $manifest = @{
120 name = 'ext'
121 version = '1.0.0'
122 engines = @{ vscode = '^1.80.0' }
123 }
124 $result = Test-ExtensionManifestValid -ManifestContent $manifest
125 $result.IsValid | Should -BeFalse
126 $result.Errors | Should -Contain "Missing required 'publisher' field"
127 }
128
129 It 'Returns invalid when engines.vscode missing' {
130 $manifest = @{
131 name = 'ext'
132 version = '1.0.0'
133 publisher = 'pub'
134 }
135 $result = Test-ExtensionManifestValid -ManifestContent $manifest
136 $result.IsValid | Should -BeFalse
137 $result.Errors | Should -Contain "Missing required 'engines' field"
138 }
139
140 It 'Returns invalid when engines exists but vscode key missing' {
141 $manifest = [PSCustomObject]@{
142 name = 'ext'
143 version = '1.0.0'
144 publisher = 'pub'
145 engines = [PSCustomObject]@{ node = '>=16' }
146 }
147 $result = Test-ExtensionManifestValid -ManifestContent $manifest
148 $result.IsValid | Should -BeFalse
149 $result.Errors | Should -Contain "Missing required 'engines.vscode' field"
150 }
151
152 It 'Returns invalid when version format is incorrect' {
153 $manifest = [PSCustomObject]@{
154 name = 'ext'
155 version = 'invalid-version'
156 publisher = 'pub'
157 engines = [PSCustomObject]@{ vscode = '^1.80.0' }
158 }
159 $result = Test-ExtensionManifestValid -ManifestContent $manifest
160 $result.IsValid | Should -BeFalse
161 $result.Errors | Should -Match 'Invalid version format'
162 }
163
164 It 'Collects multiple errors' {
165 $manifest = @{}
166 $result = Test-ExtensionManifestValid -ManifestContent $manifest
167 $result.IsValid | Should -BeFalse
168 $result.Errors.Count | Should -BeGreaterThan 1
169 }
170}
171
172Describe 'Get-VscePackageCommand' {
173 It 'Returns npx command structure for npx type' {
174 $result = Get-VscePackageCommand -CommandType 'npx'
175 $result.Executable | Should -Be 'npx'
176 $result.Arguments | Should -Contain '@vscode/vsce'
177 $result.Arguments | Should -Contain 'package'
178 }
179
180 It 'Returns vsce command for vsce type' {
181 $result = Get-VscePackageCommand -CommandType 'vsce'
182 $result.Executable | Should -Be 'vsce'
183 $result.Arguments | Should -Contain 'package'
184 }
185
186 It 'Includes --pre-release flag when specified' {
187 $result = Get-VscePackageCommand -CommandType 'npx' -PreRelease
188 $result.Arguments | Should -Contain '--pre-release'
189 }
190
191 It 'Excludes --pre-release flag when not specified' {
192 $result = Get-VscePackageCommand -CommandType 'npx'
193 $result.Arguments | Should -Not -Contain '--pre-release'
194 }
195}
196
197Describe 'New-PackagingResult' {
198 BeforeAll {
199 $script:testVsixPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath().TrimEnd([System.IO.Path]::DirectorySeparatorChar), 'ext.vsix')
200 }
201
202 It 'Creates success result with all properties' {
203 $result = New-PackagingResult -Success $true -OutputPath $script:testVsixPath -Version '1.0.0' -ErrorMessage $null
204 $result.Success | Should -BeTrue
205 $result.OutputPath | Should -Be $script:testVsixPath
206 $result.Version | Should -Be '1.0.0'
207 $result.ErrorMessage | Should -BeNullOrEmpty
208 }
209
210 It 'Creates failure result with error message' {
211 $result = New-PackagingResult -Success $false -OutputPath $null -Version $null -ErrorMessage 'Packaging failed'
212 $result.Success | Should -BeFalse
213 $result.ErrorMessage | Should -Be 'Packaging failed'
214 }
215
216 It 'Creates result with default empty strings for optional parameters' {
217 $result = New-PackagingResult -Success $true
218 $result.Success | Should -BeTrue
219 $result.OutputPath | Should -Be ''
220 $result.Version | Should -Be ''
221 $result.ErrorMessage | Should -Be ''
222 }
223
224 It 'Creates failure result with only error message specified' {
225 $result = New-PackagingResult -Success $false -ErrorMessage 'Something went wrong'
226 $result.Success | Should -BeFalse
227 $result.ErrorMessage | Should -Be 'Something went wrong'
228 $result.OutputPath | Should -Be ''
229 $result.Version | Should -Be ''
230 }
231}
232
233Describe 'Get-ResolvedPackageVersion' {
234 It 'Returns specified version when provided' {
235 $result = Get-ResolvedPackageVersion -SpecifiedVersion '2.0.0' -ManifestVersion '1.0.0' -DevPatchNumber ''
236 $result.IsValid | Should -BeTrue
237 $result.PackageVersion | Should -Be '2.0.0'
238 }
239
240 It 'Returns manifest version when no specified version' {
241 $result = Get-ResolvedPackageVersion -SpecifiedVersion '' -ManifestVersion '1.5.0' -DevPatchNumber ''
242 $result.IsValid | Should -BeTrue
243 $result.PackageVersion | Should -Be '1.5.0'
244 }
245
246 It 'Applies dev patch number when provided' {
247 $result = Get-ResolvedPackageVersion -SpecifiedVersion '' -ManifestVersion '1.0.0' -DevPatchNumber '42'
248 $result.IsValid | Should -BeTrue
249 $result.PackageVersion | Should -Be '1.0.0-dev.42'
250 }
251
252 It 'Specified version with dev patch appends dev suffix' {
253 $result = Get-ResolvedPackageVersion -SpecifiedVersion '3.0.0' -ManifestVersion '1.0.0' -DevPatchNumber '99'
254 $result.IsValid | Should -BeTrue
255 $result.PackageVersion | Should -Be '3.0.0-dev.99'
256 }
257
258 It 'Returns invalid for malformed specified version' {
259 $result = Get-ResolvedPackageVersion -SpecifiedVersion 'not-a-version' -ManifestVersion '1.0.0' -DevPatchNumber ''
260 $result.IsValid | Should -BeFalse
261 $result.ErrorMessage | Should -Match 'Invalid version format specified'
262 }
263
264 It 'Returns invalid for malformed manifest version when no specified version' {
265 $result = Get-ResolvedPackageVersion -SpecifiedVersion '' -ManifestVersion 'bad-version' -DevPatchNumber ''
266 $result.IsValid | Should -BeFalse
267 $result.ErrorMessage | Should -Match 'Invalid version format in package.json'
268 }
269
270 It 'Extracts base version from manifest with prerelease suffix' {
271 $result = Get-ResolvedPackageVersion -SpecifiedVersion '' -ManifestVersion '1.2.3-beta.1' -DevPatchNumber ''
272 $result.IsValid | Should -BeTrue
273 $result.BaseVersion | Should -Be '1.2.3'
274 $result.PackageVersion | Should -Be '1.2.3'
275 }
276
277 It 'Returns BaseVersion correctly when specified version provided' {
278 $result = Get-ResolvedPackageVersion -SpecifiedVersion '4.5.6' -ManifestVersion '1.0.0' -DevPatchNumber ''
279 $result.IsValid | Should -BeTrue
280 $result.BaseVersion | Should -Be '4.5.6'
281 }
282}
283
284Describe 'Invoke-PackageExtension' {
285 BeforeAll {
286 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "pkg-ext-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
287 $script:extDir = Join-Path $script:testRoot 'extension'
288 $script:repoRoot = Join-Path $script:testRoot 'repo'
289 }
290
291 BeforeEach {
292 # Create fresh test directories for each test
293 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
294 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
295 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
296 New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null
297 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
298 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
299 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
300 }
301
302 AfterEach {
303 if (Test-Path $script:testRoot) {
304 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
305 }
306 }
307
308 It 'Returns failure when extension directory does not exist' {
309 $nonexistentPath = Join-Path ([System.IO.Path]::GetTempPath()) "nonexistent-ext-$([guid]::NewGuid().ToString('N').Substring(0,8))"
310 $result = Invoke-PackageExtension -ExtensionDirectory $nonexistentPath -RepoRoot $script:repoRoot
311 $result.Success | Should -BeFalse
312 $result.ErrorMessage | Should -Match 'Extension directory not found'
313 }
314
315 It 'Returns failure when package.json missing' {
316 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
317 $result.Success | Should -BeFalse
318 $result.ErrorMessage | Should -Match 'package.json not found'
319 }
320
321 It 'Returns failure when .github directory missing' {
322 # Create package.json but remove .github
323 $manifest = @{
324 name = 'test-ext'
325 version = '1.0.0'
326 publisher = 'test'
327 engines = @{ vscode = '^1.80.0' }
328 }
329 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
330 Remove-Item -Path (Join-Path $script:repoRoot '.github') -Recurse -Force
331
332 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
333 $result.Success | Should -BeFalse
334 $result.ErrorMessage | Should -Match '.github directory not found'
335 }
336
337 It 'Returns failure for invalid JSON in package.json' {
338 Set-Content -Path (Join-Path $script:extDir 'package.json') -Value '{ invalid json }'
339
340 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
341 $result.Success | Should -BeFalse
342 $result.ErrorMessage | Should -Match 'Failed to parse package.json'
343 }
344
345 It 'Returns failure for invalid manifest missing required fields' {
346 $manifest = @{ name = 'only-name' } # Missing version, publisher, engines
347 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
348
349 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
350 $result.Success | Should -BeFalse
351 $result.ErrorMessage | Should -Match 'Invalid package.json'
352 }
353
354 It 'Returns failure for invalid specified version format' {
355 $manifest = @{
356 name = 'test-ext'
357 version = '1.0.0'
358 publisher = 'test'
359 engines = @{ vscode = '^1.80.0' }
360 }
361 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
362
363 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -Version 'invalid-version'
364 $result.Success | Should -BeFalse
365 $result.ErrorMessage | Should -Match 'Invalid version format'
366 }
367
368 It 'Returns structured result hashtable with expected keys' {
369 Mock Test-VsceAvailable { return @{ IsAvailable = $false; CommandType = ''; Command = '' } }
370
371 $manifest = @{
372 name = 'test-ext'
373 version = '1.0.0'
374 publisher = 'test'
375 engines = @{ vscode = '^1.80.0' }
376 }
377 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
378
379 # Will fail at vsce availability check, validates structure
380 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
381
382 $result | Should -BeOfType [hashtable]
383 $result.Keys | Should -Contain 'Success'
384 $result.Keys | Should -Contain 'ErrorMessage'
385 }
386
387 It 'Applies DevPatchNumber to version correctly' {
388 Mock Test-VsceAvailable { return @{ IsAvailable = $false; CommandType = ''; Command = '' } }
389
390 $manifest = @{
391 name = 'test-ext'
392 version = '2.0.0'
393 publisher = 'test'
394 engines = @{ vscode = '^1.80.0' }
395 }
396 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
397
398 # Will fail at vsce availability check, validates version resolution path
399 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -DevPatchNumber '123'
400
401 # Even on failure, the result indicates version was processed
402 $result | Should -BeOfType [hashtable]
403 }
404
405 It 'Copies changelog when valid path provided' {
406 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
407 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
408
409 $manifest = @{
410 name = 'test-ext'
411 version = '1.0.0'
412 publisher = 'test'
413 engines = @{ vscode = '^1.80.0' }
414 }
415 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
416
417 # Create a changelog file
418 $changelogPath = Join-Path $script:repoRoot 'CHANGELOG.md'
419 Set-Content -Path $changelogPath -Value '# Changelog'
420
421 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -ChangelogPath $changelogPath
422
423 # Changelog should be copied to extension directory
424 $destChangelog = Join-Path $script:extDir 'CHANGELOG.md'
425 Test-Path $destChangelog | Should -BeTrue
426 $result | Should -Not -BeNullOrEmpty
427 }
428
429 It 'Warns when changelog path does not exist' {
430 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
431 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
432 Mock Write-Warning { }
433
434 $manifest = @{
435 name = 'test-ext'
436 version = '1.0.0'
437 publisher = 'test'
438 engines = @{ vscode = '^1.80.0' }
439 }
440 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
441
442 $nonexistentChangelog = Join-Path ([System.IO.Path]::GetTempPath()) "changelog-$([guid]::NewGuid().ToString('N').Substring(0,8)).md"
443 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -ChangelogPath $nonexistentChangelog
444
445 Should -Invoke Write-Warning -Times 1
446 $result | Should -Not -BeNullOrEmpty
447 }
448
449 It 'Returns failure when vsce command fails with non-zero exit code' {
450 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
451 Mock Get-VscePackageCommand { return @{ Executable = 'pwsh'; Arguments = @('-Command', 'exit 1') } }
452
453 $manifest = @{
454 name = 'test-ext'
455 version = '1.0.0'
456 publisher = 'test'
457 engines = @{ vscode = '^1.80.0' }
458 }
459 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
460
461 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
462
463 $result.Success | Should -BeFalse
464 $result.ErrorMessage | Should -Match 'vsce package command failed|The term'
465 }
466
467 It 'Returns failure when CIHelpers.psm1 missing' {
468 # Create package.json and .github, but remove CIHelpers.psm1
469 $manifest = @{
470 name = 'test-ext'
471 version = '1.0.0'
472 publisher = 'test'
473 engines = @{ vscode = '^1.80.0' }
474 }
475 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
476 Remove-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Force
477
478 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
479 $result.Success | Should -BeFalse
480 $result.ErrorMessage | Should -Match 'CIHelpers.psm1 not found'
481 }
482}
483
484Describe 'Test-PackagingInputsValid' {
485 BeforeAll {
486 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "pkg-inputs-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
487 $script:extDir = Join-Path $script:testRoot 'extension'
488 $script:repoRoot = Join-Path $script:testRoot 'repo'
489 }
490
491 BeforeEach {
492 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
493 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
494 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
495 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
496 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock'
497 Set-Content -Path (Join-Path $script:extDir 'package.json') -Value '{}'
498 }
499
500 AfterEach {
501 if (Test-Path $script:testRoot) {
502 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
503 }
504 }
505
506 It 'Returns valid when all paths exist' {
507 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
508 $result.IsValid | Should -BeTrue
509 $result.Errors | Should -BeNullOrEmpty
510 }
511
512 It 'Returns resolved paths in result' {
513 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
514 $result.PackageJsonPath | Should -BeLike '*package.json'
515 $result.GitHubDir | Should -BeLike '*.github'
516 $result.CIHelpersPath | Should -BeLike '*CIHelpers.psm1'
517 }
518
519 It 'Returns error when extension directory not found' {
520 $nonexistent = Join-Path ([System.IO.Path]::GetTempPath()) "nonexistent-$([guid]::NewGuid().ToString('N').Substring(0,8))"
521 $result = Test-PackagingInputsValid -ExtensionDirectory $nonexistent -RepoRoot $script:repoRoot
522 $result.IsValid | Should -BeFalse
523 # Function accumulates multiple errors; extension dir missing cascades to package.json missing
524 $result.Errors | Should -Match 'Extension directory not found|package.json not found'
525 }
526
527 It 'Returns error when package.json not found' {
528 Remove-Item -Path (Join-Path $script:extDir 'package.json') -Force
529 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
530 $result.IsValid | Should -BeFalse
531 $result.Errors | Should -Match 'package.json not found'
532 }
533
534 It 'Returns error when .github directory not found' {
535 Remove-Item -Path (Join-Path $script:repoRoot '.github') -Recurse -Force
536 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
537 $result.IsValid | Should -BeFalse
538 $result.Errors | Should -Match '.github directory not found'
539 }
540
541 It 'Returns error when CIHelpers.psm1 not found' {
542 Remove-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Force
543 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
544 $result.IsValid | Should -BeFalse
545 $result.Errors | Should -Match 'CIHelpers.psm1 not found'
546 }
547
548 It 'Collects multiple errors' {
549 Remove-Item -Path (Join-Path $script:extDir 'package.json') -Force
550 Remove-Item -Path (Join-Path $script:repoRoot '.github') -Recurse -Force
551 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
552 $result.IsValid | Should -BeFalse
553 $result.Errors.Count | Should -BeGreaterOrEqual 2
554 }
555}
556
557Describe 'Get-PackagingDirectorySpec' {
558 BeforeAll {
559 # Use platform-agnostic temp paths for cross-platform CI compatibility
560 $script:repoRoot = Join-Path ([System.IO.Path]::GetTempPath()) 'spec-repo'
561 $script:extDir = Join-Path ([System.IO.Path]::GetTempPath()) 'spec-ext'
562 }
563
564 It 'Returns array of 4 directory specifications' {
565 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
566 $result.Count | Should -Be 4
567 }
568
569 It 'Includes .github directory specification' {
570 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
571 $githubSpec = $result | Where-Object { $_.Source -like '*.github' }
572 $githubSpec | Should -Not -BeNullOrEmpty
573 $githubSpec.Destination | Should -BeLike '*.github'
574 $githubSpec.IsFile | Should -BeFalse
575 }
576
577 It 'Includes dev-tools directory specification' {
578 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
579 $devToolsSpec = $result | Where-Object { $_.Source -like '*dev-tools' }
580 $devToolsSpec | Should -Not -BeNullOrEmpty
581 $devToolsSpec.IsFile | Should -BeFalse
582 }
583
584 It 'Includes CIHelpers.psm1 file specification' {
585 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
586 $ciHelpersSpec = $result | Where-Object { $_.Source -like '*CIHelpers.psm1' }
587 $ciHelpersSpec | Should -Not -BeNullOrEmpty
588 $ciHelpersSpec.IsFile | Should -BeTrue
589 }
590
591 It 'Includes docs/templates directory specification' {
592 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
593 $templatesSpec = $result | Where-Object { $_.Source -like '*templates' }
594 $templatesSpec | Should -Not -BeNullOrEmpty
595 $templatesSpec.IsFile | Should -BeFalse
596 }
597
598 It 'Uses correct path joining for source and destination' {
599 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
600 foreach ($spec in $result) {
601 $spec.Source | Should -Not -BeNullOrEmpty
602 $spec.Destination | Should -Not -BeNullOrEmpty
603 }
604 }
605}
606
607Describe 'Invoke-VsceCommand' {
608 BeforeAll {
609 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "vsce-cmd-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
610 }
611
612 BeforeEach {
613 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
614 }
615
616 AfterEach {
617 if (Test-Path $script:testDir) {
618 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
619 }
620 }
621
622 It 'Returns hashtable with Success and ExitCode' {
623 $result = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 0') -WorkingDirectory $script:testDir
624 $result | Should -BeOfType [hashtable]
625 $result.Keys | Should -Contain 'Success'
626 $result.Keys | Should -Contain 'ExitCode'
627 }
628
629 It 'Returns Success true for zero exit code' {
630 $result = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 0') -WorkingDirectory $script:testDir
631 $result.Success | Should -BeTrue
632 $result.ExitCode | Should -Be 0
633 }
634
635 It 'Returns Success false for non-zero exit code' {
636 $result = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 42') -WorkingDirectory $script:testDir
637 $result.Success | Should -BeFalse
638 $result.ExitCode | Should -Be 42
639 }
640
641 It 'Restores working directory after execution' {
642 $originalDir = Get-Location
643 $null = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 0') -WorkingDirectory $script:testDir
644 (Get-Location).Path | Should -Be $originalDir.Path
645 }
646
647 It 'Uses cmd wrapper when UseWindowsWrapper specified with npx' -Skip:(-not $IsWindows) {
648 # Test that cmd wrapper path executes without error
649 # npx --help outputs text to the pipeline alongside the hashtable return value
650 $output = Invoke-VsceCommand -Executable 'npx' -Arguments @('--help') -WorkingDirectory $script:testDir -UseWindowsWrapper
651 # Filter for the hashtable return (command output also flows through pipeline)
652 $result = $output | Where-Object { $_ -is [hashtable] }
653 $result | Should -Not -BeNullOrEmpty
654 $result.Keys | Should -Contain 'Success'
655 }
656}
657
658Describe 'Remove-PackagingArtifacts' {
659 BeforeAll {
660 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "rm-artifacts-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
661 }
662
663 BeforeEach {
664 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
665 }
666
667 AfterEach {
668 if (Test-Path $script:testDir) {
669 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
670 }
671 }
672
673 It 'Removes existing directories' {
674 New-Item -Path (Join-Path $script:testDir '.github') -ItemType Directory -Force | Out-Null
675 New-Item -Path (Join-Path $script:testDir 'scripts') -ItemType Directory -Force | Out-Null
676
677 Remove-PackagingArtifacts -ExtensionDirectory $script:testDir
678
679 Test-Path (Join-Path $script:testDir '.github') | Should -BeFalse
680 Test-Path (Join-Path $script:testDir 'scripts') | Should -BeFalse
681 }
682
683 It 'Silently skips non-existent directories' {
684 { Remove-PackagingArtifacts -ExtensionDirectory $script:testDir } | Should -Not -Throw
685 }
686
687 It 'Uses custom directory names when specified' {
688 New-Item -Path (Join-Path $script:testDir 'custom1') -ItemType Directory -Force | Out-Null
689 New-Item -Path (Join-Path $script:testDir 'custom2') -ItemType Directory -Force | Out-Null
690
691 Remove-PackagingArtifacts -ExtensionDirectory $script:testDir -DirectoryNames @('custom1', 'custom2')
692
693 Test-Path (Join-Path $script:testDir 'custom1') | Should -BeFalse
694 Test-Path (Join-Path $script:testDir 'custom2') | Should -BeFalse
695 }
696
697 It 'Removes nested contents recursively' {
698 $nestedDir = Join-Path $script:testDir '.github/nested/deep'
699 New-Item -Path $nestedDir -ItemType Directory -Force | Out-Null
700 Set-Content -Path (Join-Path $nestedDir 'file.txt') -Value 'content'
701
702 Remove-PackagingArtifacts -ExtensionDirectory $script:testDir -DirectoryNames @('.github')
703
704 Test-Path (Join-Path $script:testDir '.github') | Should -BeFalse
705 }
706}
707
708Describe 'Restore-PackageJsonVersion' {
709 BeforeAll {
710 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "restore-ver-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
711 }
712
713 BeforeEach {
714 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
715 }
716
717 AfterEach {
718 if (Test-Path $script:testDir) {
719 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
720 }
721 }
722
723 It 'Restores original version to package.json' {
724 $packageJsonPath = Join-Path $script:testDir 'package.json'
725 $packageJson = @{ name = 'test'; version = '2.0.0' }
726 $packageJson | ConvertTo-Json | Set-Content -Path $packageJsonPath
727
728 $obj = Get-Content $packageJsonPath | ConvertFrom-Json
729 Restore-PackageJsonVersion -PackageJsonPath $packageJsonPath -PackageJson $obj -OriginalVersion '1.0.0'
730
731 $updated = Get-Content $packageJsonPath | ConvertFrom-Json
732 $updated.version | Should -Be '1.0.0'
733 }
734
735 It 'Returns early when OriginalVersion is null' {
736 $packageJsonPath = Join-Path $script:testDir 'package.json'
737 $packageJson = @{ name = 'test'; version = '2.0.0' }
738 $packageJson | ConvertTo-Json | Set-Content -Path $packageJsonPath
739
740 $obj = Get-Content $packageJsonPath | ConvertFrom-Json
741 { Restore-PackageJsonVersion -PackageJsonPath $packageJsonPath -PackageJson $obj -OriginalVersion $null } | Should -Not -Throw
742
743 $unchanged = Get-Content $packageJsonPath | ConvertFrom-Json
744 $unchanged.version | Should -Be '2.0.0'
745 }
746
747 It 'Returns early when PackageJson is null' {
748 $packageJsonPath = Join-Path $script:testDir 'package.json'
749 Set-Content -Path $packageJsonPath -Value '{"version": "2.0.0"}'
750
751 { Restore-PackageJsonVersion -PackageJsonPath $packageJsonPath -PackageJson $null -OriginalVersion '1.0.0' } | Should -Not -Throw
752
753 $unchanged = Get-Content $packageJsonPath | ConvertFrom-Json
754 $unchanged.version | Should -Be '2.0.0'
755 }
756
757 It 'Returns early when PackageJsonPath is null' {
758 $packageJson = @{ name = 'test'; version = '2.0.0' }
759 { Restore-PackageJsonVersion -PackageJsonPath $null -PackageJson $packageJson -OriginalVersion '1.0.0' } | Should -Not -Throw
760 }
761
762 It 'Handles write failure gracefully' {
763 Mock Write-Warning {}
764 $invalidPath = Join-Path $script:testDir 'nonexistent/package.json'
765 $packageJson = [PSCustomObject]@{ name = 'test'; version = '2.0.0' }
766
767 { Restore-PackageJsonVersion -PackageJsonPath $invalidPath -PackageJson $packageJson -OriginalVersion '1.0.0' } | Should -Not -Throw
768 }
769}
770
771Describe 'CI Integration - Package-Extension' {
772 BeforeAll {
773 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "ci-int-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
774 $script:extDir = Join-Path $script:testRoot 'extension'
775 $script:repoRoot = Join-Path $script:testRoot 'repo'
776 }
777
778 AfterAll {
779 if (Test-Path $script:testRoot) {
780 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
781 }
782 }
783
784 Context 'GitHub Actions environment' {
785 BeforeEach {
786 Initialize-MockCIEnvironment
787 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
788 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
789 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
790 New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null
791 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
792 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
793 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
794
795 $manifest = @{
796 name = 'test-ext'
797 version = '1.0.0'
798 publisher = 'test'
799 engines = @{ vscode = '^1.80.0' }
800 }
801 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
802 }
803
804 AfterEach {
805 Clear-MockCIEnvironment
806 if (Test-Path $script:testRoot) {
807 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
808 }
809 }
810
811 It 'Sets version output variable on successful package' {
812 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
813 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
814
815 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
816 Set-Content -Path $vsixPath -Value 'fake-vsix'
817
818 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
819
820 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
821 $outputContent | Should -Match 'version=1\.0\.0'
822 }
823
824 It 'Sets vsix-file output variable on successful package' {
825 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
826 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
827
828 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
829 Set-Content -Path $vsixPath -Value 'fake-vsix'
830
831 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
832
833 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
834 $outputContent | Should -Match 'vsix-file=test-ext-1\.0\.0\.vsix'
835 }
836
837 It 'Sets pre-release output variable when PreRelease specified' {
838 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
839 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
840
841 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
842 Set-Content -Path $vsixPath -Value 'fake-vsix'
843
844 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -PreRelease
845
846 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
847 $outputContent | Should -Match 'pre-release=True'
848 }
849
850 It 'Sets pre-release output variable to false when PreRelease not specified' {
851 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
852 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
853
854 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
855 Set-Content -Path $vsixPath -Value 'fake-vsix'
856
857 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
858
859 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
860 $outputContent | Should -Match 'pre-release=False'
861 }
862
863 It 'Returns failure result when vsce command fails' {
864 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
865 Mock Get-VscePackageCommand { return @{ Executable = 'pwsh'; Arguments = @('-Command', 'exit 1') } }
866
867 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
868
869 $result.Success | Should -BeFalse
870 $result.ErrorMessage | Should -Match 'vsce package command failed'
871 }
872 }
873
874 Context 'Local environment' {
875 BeforeEach {
876 Clear-MockCIEnvironment
877
878 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
879 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
880 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
881 New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null
882 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
883 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
884 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
885
886 $manifest = @{
887 name = 'test-ext'
888 version = '1.0.0'
889 publisher = 'test'
890 engines = @{ vscode = '^1.80.0' }
891 }
892 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
893 }
894
895 AfterEach {
896 if (Test-Path $script:testRoot) {
897 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
898 }
899 }
900
901 It 'Completes without error when not in CI environment' {
902 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
903 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
904
905 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
906 Set-Content -Path $vsixPath -Value 'fake-vsix'
907
908 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
909
910 $result.Success | Should -BeTrue
911 }
912 }
913}
914