microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e69486a5f809ede45c63c0a31358c12912bd5168

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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