microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/context-working

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

1688lines · 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 'Excludes dev artifacts when copying .github in full mode' {
635 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
636 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
637
638 $manifest = @{
639 name = 'test-ext'
640 version = '1.0.0'
641 publisher = 'test'
642 engines = @{ vscode = '^1.80.0' }
643 }
644 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
645
646 # Create skills with .venv and __pycache__ in the repo
647 $skillDir = Join-Path $script:repoRoot '.github/skills/my-skill'
648 New-Item -Path $skillDir -ItemType Directory -Force | Out-Null
649 Set-Content -Path (Join-Path $skillDir 'SKILL.md') -Value '# Skill'
650 New-Item -Path (Join-Path $skillDir '.venv/lib') -ItemType Directory -Force | Out-Null
651 Set-Content -Path (Join-Path $skillDir '.venv/lib/pkg.py') -Value 'pass'
652 New-Item -Path (Join-Path $skillDir '__pycache__') -ItemType Directory -Force | Out-Null
653 Set-Content -Path (Join-Path $skillDir '__pycache__/mod.pyc') -Value 'bytecode'
654
655 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
656 Set-Content -Path $vsixPath -Value 'fake-vsix'
657
658 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
659
660 $copiedSkillDir = Join-Path $script:extDir '.github/skills/my-skill'
661 # .venv and __pycache__ should not be present after full-mode copy
662 if (Test-Path $copiedSkillDir) {
663 Test-Path (Join-Path $copiedSkillDir '.venv') | Should -BeFalse
664 Test-Path (Join-Path $copiedSkillDir '__pycache__') | Should -BeFalse
665 Test-Path (Join-Path $copiedSkillDir 'SKILL.md') | Should -BeTrue
666 }
667 $result | Should -BeOfType [hashtable]
668 }
669
670 It 'Removes test directories from skills in full mode' {
671 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
672 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
673
674 $manifest = @{
675 name = 'test-ext'
676 version = '1.0.0'
677 publisher = 'test'
678 engines = @{ vscode = '^1.80.0' }
679 }
680 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
681
682 # Create skill with tests/ directory
683 $skillDir = Join-Path $script:repoRoot '.github/skills/my-skill'
684 New-Item -Path $skillDir -ItemType Directory -Force | Out-Null
685 Set-Content -Path (Join-Path $skillDir 'SKILL.md') -Value '# Skill'
686 New-Item -Path (Join-Path $skillDir 'tests') -ItemType Directory -Force | Out-Null
687 Set-Content -Path (Join-Path $skillDir 'tests/test_main.py') -Value 'def test(): pass'
688
689 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
690 Set-Content -Path $vsixPath -Value 'fake-vsix'
691
692 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
693
694 $copiedSkillDir = Join-Path $script:extDir '.github/skills/my-skill'
695 if (Test-Path $copiedSkillDir) {
696 Test-Path (Join-Path $copiedSkillDir 'tests') | Should -BeFalse
697 Test-Path (Join-Path $copiedSkillDir 'SKILL.md') | Should -BeTrue
698 }
699 $result | Should -BeOfType [hashtable]
700 }
701
702 It 'Returns success without VSIX creation when DryRun is specified' {
703 $manifest = @{
704 name = 'test-ext'
705 version = '1.0.0'
706 publisher = 'test'
707 engines = @{ vscode = '^1.80.0' }
708 }
709 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
710
711 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -DryRun
712
713 $result.Success | Should -BeTrue
714 $result.Version | Should -Be '1.0.0'
715 $result.OutputPath | Should -BeNullOrEmpty
716 }
717}
718
719Describe 'Test-PackagingInputsValid' {
720 BeforeAll {
721 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "pkg-inputs-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
722 $script:extDir = Join-Path $script:testRoot 'extension'
723 $script:repoRoot = Join-Path $script:testRoot 'repo'
724 }
725
726 BeforeEach {
727 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
728 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
729 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
730 New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null
731 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
732 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock'
733 Set-Content -Path (Join-Path $script:extDir 'package.json') -Value '{}'
734 }
735
736 AfterEach {
737 if (Test-Path $script:testRoot) {
738 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
739 }
740 }
741
742 It 'Returns valid when all paths exist' {
743 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
744 $result.IsValid | Should -BeTrue
745 $result.Errors | Should -BeNullOrEmpty
746 }
747
748 It 'Returns resolved paths in result' {
749 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
750 $result.PackageJsonPath | Should -BeLike '*package.json'
751 $result.GitHubDir | Should -BeLike '*.github'
752 $result.CIHelpersPath | Should -BeLike '*CIHelpers.psm1'
753 }
754
755 It 'Returns error when extension directory not found' {
756 $nonexistent = Join-Path ([System.IO.Path]::GetTempPath()) "nonexistent-$([guid]::NewGuid().ToString('N').Substring(0,8))"
757 $result = Test-PackagingInputsValid -ExtensionDirectory $nonexistent -RepoRoot $script:repoRoot
758 $result.IsValid | Should -BeFalse
759 # Function accumulates multiple errors; extension dir missing cascades to package.json missing
760 $result.Errors | Should -Match 'Extension directory not found|package.json not found'
761 }
762
763 It 'Returns error when package.json not found' {
764 Remove-Item -Path (Join-Path $script:extDir 'package.json') -Force
765 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
766 $result.IsValid | Should -BeFalse
767 $result.Errors | Should -Match 'package.json not found'
768 }
769
770 It 'Returns error when .github directory not found' {
771 Remove-Item -Path (Join-Path $script:repoRoot '.github') -Recurse -Force
772 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
773 $result.IsValid | Should -BeFalse
774 $result.Errors | Should -Match '.github directory not found'
775 }
776
777 It 'Returns error when CIHelpers.psm1 not found' {
778 Remove-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Force
779 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
780 $result.IsValid | Should -BeFalse
781 $result.Errors | Should -Match 'CIHelpers.psm1 not found'
782 }
783
784 It 'Collects multiple errors' {
785 Remove-Item -Path (Join-Path $script:extDir 'package.json') -Force
786 Remove-Item -Path (Join-Path $script:repoRoot '.github') -Recurse -Force
787 $result = Test-PackagingInputsValid -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
788 $result.IsValid | Should -BeFalse
789 $result.Errors.Count | Should -BeGreaterOrEqual 2
790 }
791}
792
793Describe 'Get-PackagingDirectorySpec' {
794 BeforeAll {
795 # Use platform-agnostic temp paths for cross-platform CI compatibility
796 $script:repoRoot = Join-Path ([System.IO.Path]::GetTempPath()) 'spec-repo'
797 $script:extDir = Join-Path ([System.IO.Path]::GetTempPath()) 'spec-ext'
798 }
799
800 It 'Returns array of 3 directory specifications' {
801 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
802 $result.Count | Should -Be 3
803 }
804
805 It 'Includes .github directory specification' {
806 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
807 $githubSpec = $result | Where-Object { $_.Source -like '*.github' }
808 $githubSpec | Should -Not -BeNullOrEmpty
809 $githubSpec.Destination | Should -BeLike '*.github'
810 $githubSpec.IsFile | Should -BeFalse
811 }
812
813 It 'Includes CIHelpers.psm1 file specification' {
814 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
815 $ciHelpersSpec = $result | Where-Object { $_.Source -like '*CIHelpers.psm1' }
816 $ciHelpersSpec | Should -Not -BeNullOrEmpty
817 $ciHelpersSpec.IsFile | Should -BeTrue
818 }
819
820 It 'Includes docs/templates directory specification' {
821 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
822 $templatesSpec = $result | Where-Object { $_.Source -like '*templates' }
823 $templatesSpec | Should -Not -BeNullOrEmpty
824 $templatesSpec.IsFile | Should -BeFalse
825 }
826
827 It 'Uses correct path joining for source and destination' {
828 $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir
829 foreach ($spec in $result) {
830 $spec.Source | Should -Not -BeNullOrEmpty
831 $spec.Destination | Should -Not -BeNullOrEmpty
832 }
833 }
834}
835
836Describe 'Invoke-VsceCommand' {
837 BeforeAll {
838 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "vsce-cmd-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
839 }
840
841 BeforeEach {
842 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
843 }
844
845 AfterEach {
846 if (Test-Path $script:testDir) {
847 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
848 }
849 }
850
851 It 'Returns hashtable with Success and ExitCode' {
852 $result = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 0') -WorkingDirectory $script:testDir
853 $result | Should -BeOfType [hashtable]
854 $result.Keys | Should -Contain 'Success'
855 $result.Keys | Should -Contain 'ExitCode'
856 }
857
858 It 'Returns Success true for zero exit code' {
859 $result = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 0') -WorkingDirectory $script:testDir
860 $result.Success | Should -BeTrue
861 $result.ExitCode | Should -Be 0
862 }
863
864 It 'Returns Success false for non-zero exit code' {
865 $result = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 42') -WorkingDirectory $script:testDir
866 $result.Success | Should -BeFalse
867 $result.ExitCode | Should -Be 42
868 }
869
870 It 'Restores working directory after execution' {
871 $originalDir = Get-Location
872 $null = Invoke-VsceCommand -Executable 'pwsh' -Arguments @('-Command', 'exit 0') -WorkingDirectory $script:testDir
873 (Get-Location).Path | Should -Be $originalDir.Path
874 }
875
876 It 'Uses cmd wrapper when UseWindowsWrapper specified with npx' -Skip:(-not $IsWindows) {
877 # Test that cmd wrapper path executes without error
878 # npx --help outputs text to the pipeline alongside the hashtable return value
879 $output = Invoke-VsceCommand -Executable 'npx' -Arguments @('--help') -WorkingDirectory $script:testDir -UseWindowsWrapper
880 # Filter for the hashtable return (command output also flows through pipeline)
881 $result = $output | Where-Object { $_ -is [hashtable] }
882 $result | Should -Not -BeNullOrEmpty
883 $result.Keys | Should -Contain 'Success'
884 }
885}
886
887Describe 'Remove-PackagingArtifacts' {
888 BeforeAll {
889 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "rm-artifacts-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
890 }
891
892 BeforeEach {
893 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
894 }
895
896 AfterEach {
897 if (Test-Path $script:testDir) {
898 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
899 }
900 }
901
902 It 'Removes existing directories' {
903 New-Item -Path (Join-Path $script:testDir '.github') -ItemType Directory -Force | Out-Null
904 New-Item -Path (Join-Path $script:testDir 'scripts') -ItemType Directory -Force | Out-Null
905
906 Remove-PackagingArtifacts -ExtensionDirectory $script:testDir
907
908 Test-Path (Join-Path $script:testDir '.github') | Should -BeFalse
909 Test-Path (Join-Path $script:testDir 'scripts') | Should -BeFalse
910 }
911
912 It 'Silently skips non-existent directories' {
913 { Remove-PackagingArtifacts -ExtensionDirectory $script:testDir } | Should -Not -Throw
914 }
915
916 It 'Uses custom directory names when specified' {
917 New-Item -Path (Join-Path $script:testDir 'custom1') -ItemType Directory -Force | Out-Null
918 New-Item -Path (Join-Path $script:testDir 'custom2') -ItemType Directory -Force | Out-Null
919
920 Remove-PackagingArtifacts -ExtensionDirectory $script:testDir -DirectoryNames @('custom1', 'custom2')
921
922 Test-Path (Join-Path $script:testDir 'custom1') | Should -BeFalse
923 Test-Path (Join-Path $script:testDir 'custom2') | Should -BeFalse
924 }
925
926 It 'Removes nested contents recursively' {
927 $nestedDir = Join-Path $script:testDir '.github/nested/deep'
928 New-Item -Path $nestedDir -ItemType Directory -Force | Out-Null
929 Set-Content -Path (Join-Path $nestedDir 'file.txt') -Value 'content'
930
931 Remove-PackagingArtifacts -ExtensionDirectory $script:testDir -DirectoryNames @('.github')
932
933 Test-Path (Join-Path $script:testDir '.github') | Should -BeFalse
934 }
935}
936
937Describe 'Restore-PackageJsonVersion' {
938 BeforeAll {
939 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "restore-ver-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
940 }
941
942 BeforeEach {
943 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
944 }
945
946 AfterEach {
947 if (Test-Path $script:testDir) {
948 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
949 }
950 }
951
952 It 'Restores original version to package.json' {
953 $packageJsonPath = Join-Path $script:testDir 'package.json'
954 $packageJson = @{ name = 'test'; version = '2.0.0' }
955 $packageJson | ConvertTo-Json | Set-Content -Path $packageJsonPath
956
957 $obj = Get-Content $packageJsonPath | ConvertFrom-Json
958 Restore-PackageJsonVersion -PackageJsonPath $packageJsonPath -PackageJson $obj -OriginalVersion '1.0.0'
959
960 $updated = Get-Content $packageJsonPath | ConvertFrom-Json
961 $updated.version | Should -Be '1.0.0'
962 }
963
964 It 'Returns early when OriginalVersion is null' {
965 $packageJsonPath = Join-Path $script:testDir 'package.json'
966 $packageJson = @{ name = 'test'; version = '2.0.0' }
967 $packageJson | ConvertTo-Json | Set-Content -Path $packageJsonPath
968
969 $obj = Get-Content $packageJsonPath | ConvertFrom-Json
970 { Restore-PackageJsonVersion -PackageJsonPath $packageJsonPath -PackageJson $obj -OriginalVersion $null } | Should -Not -Throw
971
972 $unchanged = Get-Content $packageJsonPath | ConvertFrom-Json
973 $unchanged.version | Should -Be '2.0.0'
974 }
975
976 It 'Returns early when PackageJson is null' {
977 $packageJsonPath = Join-Path $script:testDir 'package.json'
978 Set-Content -Path $packageJsonPath -Value '{"version": "2.0.0"}'
979
980 { Restore-PackageJsonVersion -PackageJsonPath $packageJsonPath -PackageJson $null -OriginalVersion '1.0.0' } | Should -Not -Throw
981
982 $unchanged = Get-Content $packageJsonPath | ConvertFrom-Json
983 $unchanged.version | Should -Be '2.0.0'
984 }
985
986 It 'Returns early when PackageJsonPath is null' {
987 $packageJson = @{ name = 'test'; version = '2.0.0' }
988 { Restore-PackageJsonVersion -PackageJsonPath $null -PackageJson $packageJson -OriginalVersion '1.0.0' } | Should -Not -Throw
989 }
990
991 It 'Handles write failure gracefully' {
992 Mock Write-Warning {}
993 $invalidPath = Join-Path $script:testDir 'nonexistent/package.json'
994 $packageJson = [PSCustomObject]@{ name = 'test'; version = '2.0.0' }
995
996 { Restore-PackageJsonVersion -PackageJsonPath $invalidPath -PackageJson $packageJson -OriginalVersion '1.0.0' } | Should -Not -Throw
997 }
998}
999
1000Describe 'Get-CollectionReadmePath' {
1001 BeforeAll {
1002 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "collection-readme-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
1003 $script:extDir = Join-Path $script:testDir 'extension'
1004 }
1005
1006 BeforeEach {
1007 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
1008 }
1009
1010 AfterEach {
1011 if (Test-Path $script:testDir) {
1012 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
1013 }
1014 }
1015
1016 It 'Returns null for hve-core collection' {
1017 $collectionPath = Join-Path $script:testDir 'collection.yml'
1018 @"
1019id: hve-core
1020name: core
1021"@ | Set-Content $collectionPath
1022
1023 $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir
1024 $result | Should -BeNullOrEmpty
1025 }
1026
1027 It 'Returns collection README path when file exists' {
1028 $collectionPath = Join-Path $script:testDir 'collection.yml'
1029 @"
1030id: developer
1031name: dev
1032"@ | Set-Content $collectionPath
1033
1034 $collectionReadme = Join-Path $script:extDir 'README.developer.md'
1035 Set-Content -Path $collectionReadme -Value '# Developer README'
1036
1037 $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir
1038 $result | Should -Be $collectionReadme
1039 }
1040
1041 It 'Returns null when collection README file does not exist' {
1042 $collectionPath = Join-Path $script:testDir 'collection.yml'
1043 @"
1044id: security
1045name: sec
1046"@ | Set-Content $collectionPath
1047
1048 $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir
1049 $result | Should -BeNullOrEmpty
1050 }
1051
1052 It 'Parses JSON collection file correctly' {
1053 $collectionPath = Join-Path $script:testDir 'collection.json'
1054 @{
1055 id = 'json-collection'
1056 name = 'json'
1057 } | ConvertTo-Json | Set-Content $collectionPath
1058
1059 $collectionReadme = Join-Path $script:extDir 'README.json-collection.md'
1060 Set-Content -Path $collectionReadme -Value '# JSON Collection README'
1061
1062 $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir
1063 $result | Should -Be $collectionReadme
1064 }
1065}
1066
1067Describe 'Set-CollectionReadme' {
1068 BeforeAll {
1069 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "set-readme-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
1070 }
1071
1072 BeforeEach {
1073 New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
1074 Set-Content -Path (Join-Path $script:testDir 'README.md') -Value '# Original README'
1075 }
1076
1077 AfterEach {
1078 if (Test-Path $script:testDir) {
1079 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
1080 }
1081 }
1082
1083 It 'Swaps README.md with collection README and creates backup' {
1084 $collectionReadmePath = Join-Path $script:testDir 'README.developer.md'
1085 Set-Content -Path $collectionReadmePath -Value '# Developer README'
1086
1087 Set-CollectionReadme -ExtensionDirectory $script:testDir -CollectionReadmePath $collectionReadmePath -Operation Swap
1088
1089 $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw
1090 $readmeContent | Should -Match 'Developer README'
1091
1092 Test-Path (Join-Path $script:testDir 'README.md.bak') | Should -BeTrue
1093 $backupContent = Get-Content -Path (Join-Path $script:testDir 'README.md.bak') -Raw
1094 $backupContent | Should -Match 'Original README'
1095 }
1096
1097 It 'Warns and returns early when no collection path for swap' {
1098 Mock Write-Warning {}
1099 Set-CollectionReadme -ExtensionDirectory $script:testDir -Operation Swap
1100
1101 Should -Invoke Write-Warning -Times 1
1102 $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw
1103 $readmeContent | Should -Match 'Original README'
1104 }
1105
1106 It 'Restores README.md from backup and removes backup file' {
1107 # Create backup state
1108 Set-Content -Path (Join-Path $script:testDir 'README.md.bak') -Value '# Original README'
1109 Set-Content -Path (Join-Path $script:testDir 'README.md') -Value '# Collection README'
1110
1111 Set-CollectionReadme -ExtensionDirectory $script:testDir -Operation Restore
1112
1113 $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw
1114 $readmeContent | Should -Match 'Original README'
1115 Test-Path (Join-Path $script:testDir 'README.md.bak') | Should -BeFalse
1116 }
1117
1118 It 'Restore is a no-op when no backup exists' {
1119 { Set-CollectionReadme -ExtensionDirectory $script:testDir -Operation Restore } | Should -Not -Throw
1120 $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw
1121 $readmeContent | Should -Match 'Original README'
1122 }
1123}
1124
1125Describe 'Copy-DirectoryFiltered' -Tag 'Unit' {
1126 BeforeAll {
1127 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "copy-dir-filtered-$([guid]::NewGuid().ToString('N').Substring(0,8))"
1128 }
1129
1130 AfterEach {
1131 if (Test-Path $script:testDir) {
1132 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
1133 }
1134 }
1135
1136 It 'Copies files at root level' {
1137 $src = Join-Path $script:testDir 'src'
1138 $dest = Join-Path $script:testDir 'dest'
1139 New-Item -Path $src -ItemType Directory -Force | Out-Null
1140 Set-Content -Path (Join-Path $src 'file.txt') -Value 'content'
1141
1142 Copy-DirectoryFiltered -Source $src -Destination $dest
1143
1144 Test-Path (Join-Path $dest 'file.txt') | Should -BeTrue
1145 Get-Content (Join-Path $dest 'file.txt') | Should -Be 'content'
1146 }
1147
1148 It 'Copies nested subdirectories recursively' {
1149 $src = Join-Path $script:testDir 'src'
1150 New-Item -Path (Join-Path $src 'sub/deep') -ItemType Directory -Force | Out-Null
1151 Set-Content -Path (Join-Path $src 'sub/deep/nested.md') -Value '# Nested'
1152 $dest = Join-Path $script:testDir 'dest'
1153
1154 Copy-DirectoryFiltered -Source $src -Destination $dest
1155
1156 Test-Path (Join-Path $dest 'sub/deep/nested.md') | Should -BeTrue
1157 }
1158
1159 It 'Excludes .venv directories' {
1160 $src = Join-Path $script:testDir 'src'
1161 New-Item -Path (Join-Path $src '.venv/lib') -ItemType Directory -Force | Out-Null
1162 Set-Content -Path (Join-Path $src '.venv/lib/site.py') -Value 'import os'
1163 Set-Content -Path (Join-Path $src 'SKILL.md') -Value '# Skill'
1164 $dest = Join-Path $script:testDir 'dest'
1165
1166 Copy-DirectoryFiltered -Source $src -Destination $dest
1167
1168 Test-Path (Join-Path $dest 'SKILL.md') | Should -BeTrue
1169 Test-Path (Join-Path $dest '.venv') | Should -BeFalse
1170 }
1171
1172 It 'Excludes __pycache__ directories' {
1173 $src = Join-Path $script:testDir 'src'
1174 New-Item -Path (Join-Path $src 'scripts/__pycache__') -ItemType Directory -Force | Out-Null
1175 Set-Content -Path (Join-Path $src 'scripts/__pycache__/mod.cpython-311.pyc') -Value 'bytecode'
1176 Set-Content -Path (Join-Path $src 'scripts/run.py') -Value 'print("hello")'
1177 $dest = Join-Path $script:testDir 'dest'
1178
1179 Copy-DirectoryFiltered -Source $src -Destination $dest
1180
1181 Test-Path (Join-Path $dest 'scripts/run.py') | Should -BeTrue
1182 Test-Path (Join-Path $dest 'scripts/__pycache__') | Should -BeFalse
1183 }
1184
1185 It 'Excludes .ruff_cache and .pytest_cache directories' {
1186 $src = Join-Path $script:testDir 'src'
1187 New-Item -Path (Join-Path $src '.ruff_cache') -ItemType Directory -Force | Out-Null
1188 New-Item -Path (Join-Path $src '.pytest_cache') -ItemType Directory -Force | Out-Null
1189 Set-Content -Path (Join-Path $src '.ruff_cache/data') -Value 'cache'
1190 Set-Content -Path (Join-Path $src '.pytest_cache/v') -Value 'cache'
1191 Set-Content -Path (Join-Path $src 'main.py') -Value 'pass'
1192 $dest = Join-Path $script:testDir 'dest'
1193
1194 Copy-DirectoryFiltered -Source $src -Destination $dest
1195
1196 Test-Path (Join-Path $dest 'main.py') | Should -BeTrue
1197 Test-Path (Join-Path $dest '.ruff_cache') | Should -BeFalse
1198 Test-Path (Join-Path $dest '.pytest_cache') | Should -BeFalse
1199 }
1200
1201 It 'Excludes node_modules directories' {
1202 $src = Join-Path $script:testDir 'src'
1203 New-Item -Path (Join-Path $src 'node_modules/pkg') -ItemType Directory -Force | Out-Null
1204 Set-Content -Path (Join-Path $src 'node_modules/pkg/index.js') -Value 'module.exports = {}'
1205 Set-Content -Path (Join-Path $src 'index.js') -Value 'require("pkg")'
1206 $dest = Join-Path $script:testDir 'dest'
1207
1208 Copy-DirectoryFiltered -Source $src -Destination $dest
1209
1210 Test-Path (Join-Path $dest 'index.js') | Should -BeTrue
1211 Test-Path (Join-Path $dest 'node_modules') | Should -BeFalse
1212 }
1213
1214 It 'Excludes deeply nested dev artifact directories' {
1215 $src = Join-Path $script:testDir 'src'
1216 New-Item -Path (Join-Path $src 'skills/powerpoint/.venv/lib') -ItemType Directory -Force | Out-Null
1217 Set-Content -Path (Join-Path $src 'skills/powerpoint/.venv/lib/pkg.py') -Value 'pass'
1218 Set-Content -Path (Join-Path $src 'skills/powerpoint/SKILL.md') -Value '# Skill'
1219 $dest = Join-Path $script:testDir 'dest'
1220
1221 Copy-DirectoryFiltered -Source $src -Destination $dest
1222
1223 Test-Path (Join-Path $dest 'skills/powerpoint/SKILL.md') | Should -BeTrue
1224 Test-Path (Join-Path $dest 'skills/powerpoint/.venv') | Should -BeFalse
1225 }
1226
1227 It 'Accepts custom ExcludePatterns' {
1228 $src = Join-Path $script:testDir 'src'
1229 New-Item -Path (Join-Path $src 'build') -ItemType Directory -Force | Out-Null
1230 New-Item -Path (Join-Path $src 'dist') -ItemType Directory -Force | Out-Null
1231 Set-Content -Path (Join-Path $src 'build/out.js') -Value 'compiled'
1232 Set-Content -Path (Join-Path $src 'dist/bundle.js') -Value 'bundled'
1233 Set-Content -Path (Join-Path $src 'src.js') -Value 'source'
1234 $dest = Join-Path $script:testDir 'dest'
1235
1236 Copy-DirectoryFiltered -Source $src -Destination $dest -ExcludePatterns @('build', 'dist')
1237
1238 Test-Path (Join-Path $dest 'src.js') | Should -BeTrue
1239 Test-Path (Join-Path $dest 'build') | Should -BeFalse
1240 Test-Path (Join-Path $dest 'dist') | Should -BeFalse
1241 }
1242
1243 It 'Creates destination directory if it does not exist' {
1244 $src = Join-Path $script:testDir 'src'
1245 New-Item -Path $src -ItemType Directory -Force | Out-Null
1246 Set-Content -Path (Join-Path $src 'file.txt') -Value 'content'
1247 $dest = Join-Path $script:testDir 'nonexistent/nested/dest'
1248
1249 Copy-DirectoryFiltered -Source $src -Destination $dest
1250
1251 Test-Path (Join-Path $dest 'file.txt') | Should -BeTrue
1252 }
1253
1254 It 'Handles empty source directory without error' {
1255 $src = Join-Path $script:testDir 'src'
1256 New-Item -Path $src -ItemType Directory -Force | Out-Null
1257 $dest = Join-Path $script:testDir 'dest'
1258
1259 { Copy-DirectoryFiltered -Source $src -Destination $dest } | Should -Not -Throw
1260
1261 Test-Path $dest | Should -BeTrue
1262 }
1263}
1264
1265Describe 'Copy-CollectionArtifacts' {
1266 BeforeAll {
1267 $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "copy-col-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
1268 $script:extDir = Join-Path $script:testDir 'extension'
1269 $script:repoRoot = Join-Path $script:testDir 'repo'
1270 }
1271
1272 BeforeEach {
1273 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
1274 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
1275 }
1276
1277 AfterEach {
1278 if (Test-Path $script:testDir) {
1279 Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue
1280 }
1281 }
1282
1283 It 'Copies agents from repo to extension directory' {
1284 # Create source agent
1285 $agentsSrc = Join-Path $script:repoRoot '.github/agents'
1286 New-Item -Path $agentsSrc -ItemType Directory -Force | Out-Null
1287 Set-Content -Path (Join-Path $agentsSrc 'task-planner.agent.md') -Value '# Agent'
1288
1289 # Create package.json with contributes referencing agents
1290 $pkgJson = @{
1291 contributes = @{
1292 chatAgents = @(
1293 @{ path = './.github/agents/task-planner.agent.md' }
1294 )
1295 }
1296 }
1297 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1298
1299 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1300
1301 Test-Path (Join-Path $script:extDir '.github/agents/task-planner.agent.md') | Should -BeTrue
1302 }
1303
1304 It 'Copies prompts from repo to extension directory' {
1305 # Create source prompt
1306 $promptsSrc = Join-Path $script:repoRoot '.github/prompts'
1307 New-Item -Path $promptsSrc -ItemType Directory -Force | Out-Null
1308 Set-Content -Path (Join-Path $promptsSrc 'my-prompt.prompt.md') -Value '# Prompt'
1309
1310 $pkgJson = @{
1311 contributes = @{
1312 chatPromptFiles = @(
1313 @{ path = './.github/prompts/my-prompt.prompt.md' }
1314 )
1315 }
1316 }
1317 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1318
1319 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1320
1321 Test-Path (Join-Path $script:extDir '.github/prompts/my-prompt.prompt.md') | Should -BeTrue
1322 }
1323
1324 It 'Copies instructions from repo to extension directory' {
1325 # Create source instruction
1326 $instrSrc = Join-Path $script:repoRoot '.github/instructions'
1327 New-Item -Path $instrSrc -ItemType Directory -Force | Out-Null
1328 Set-Content -Path (Join-Path $instrSrc 'commit-message.instructions.md') -Value '# Instructions'
1329
1330 $pkgJson = @{
1331 contributes = @{
1332 chatInstructions = @(
1333 @{ path = './.github/instructions/commit-message.instructions.md' }
1334 )
1335 }
1336 }
1337 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1338
1339 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1340
1341 Test-Path (Join-Path $script:extDir '.github/instructions/commit-message.instructions.md') | Should -BeTrue
1342 }
1343
1344 It 'Copies skills recursively from repo to extension directory' {
1345 # Create source skill with nested file
1346 $skillSrc = Join-Path $script:repoRoot '.github/skills/video-to-gif'
1347 New-Item -Path $skillSrc -ItemType Directory -Force | Out-Null
1348 Set-Content -Path (Join-Path $skillSrc 'SKILL.md') -Value '# Skill'
1349
1350 $pkgJson = @{
1351 contributes = @{
1352 chatSkills = @(
1353 @{ path = './.github/skills/video-to-gif' }
1354 )
1355 }
1356 }
1357 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1358
1359 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1360
1361 Test-Path (Join-Path $script:extDir '.github/skills/video-to-gif') | Should -BeTrue
1362 }
1363
1364 It 'Excludes .venv from copied skills' {
1365 $skillSrc = Join-Path $script:repoRoot '.github/skills/powerpoint'
1366 New-Item -Path $skillSrc -ItemType Directory -Force | Out-Null
1367 Set-Content -Path (Join-Path $skillSrc 'SKILL.md') -Value '# PPT Skill'
1368 New-Item -Path (Join-Path $skillSrc '.venv/lib') -ItemType Directory -Force | Out-Null
1369 Set-Content -Path (Join-Path $skillSrc '.venv/lib/site.py') -Value 'import os'
1370
1371 $pkgJson = @{
1372 contributes = @{
1373 chatSkills = @(
1374 @{ path = './.github/skills/powerpoint' }
1375 )
1376 }
1377 }
1378 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1379
1380 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1381
1382 Test-Path (Join-Path $script:extDir '.github/skills/powerpoint/SKILL.md') | Should -BeTrue
1383 Test-Path (Join-Path $script:extDir '.github/skills/powerpoint/.venv') | Should -BeFalse
1384 }
1385
1386 It 'Excludes __pycache__ and .ruff_cache from copied skills' {
1387 $skillSrc = Join-Path $script:repoRoot '.github/skills/my-skill'
1388 New-Item -Path $skillSrc -ItemType Directory -Force | Out-Null
1389 Set-Content -Path (Join-Path $skillSrc 'SKILL.md') -Value '# My Skill'
1390 New-Item -Path (Join-Path $skillSrc '__pycache__') -ItemType Directory -Force | Out-Null
1391 Set-Content -Path (Join-Path $skillSrc '__pycache__/mod.pyc') -Value 'bytecode'
1392 New-Item -Path (Join-Path $skillSrc '.ruff_cache') -ItemType Directory -Force | Out-Null
1393 Set-Content -Path (Join-Path $skillSrc '.ruff_cache/data') -Value 'cache'
1394
1395 $pkgJson = @{
1396 contributes = @{
1397 chatSkills = @(
1398 @{ path = './.github/skills/my-skill' }
1399 )
1400 }
1401 }
1402 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1403
1404 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1405
1406 Test-Path (Join-Path $script:extDir '.github/skills/my-skill/SKILL.md') | Should -BeTrue
1407 Test-Path (Join-Path $script:extDir '.github/skills/my-skill/__pycache__') | Should -BeFalse
1408 Test-Path (Join-Path $script:extDir '.github/skills/my-skill/.ruff_cache') | Should -BeFalse
1409 }
1410
1411 It 'Removes test directories from copied skills' {
1412 $skillSrc = Join-Path $script:repoRoot '.github/skills/my-skill'
1413 New-Item -Path $skillSrc -ItemType Directory -Force | Out-Null
1414 Set-Content -Path (Join-Path $skillSrc 'SKILL.md') -Value '# My Skill'
1415 New-Item -Path (Join-Path $skillSrc 'tests') -ItemType Directory -Force | Out-Null
1416 Set-Content -Path (Join-Path $skillSrc 'tests/test_main.py') -Value 'def test(): pass'
1417
1418 $pkgJson = @{
1419 contributes = @{
1420 chatSkills = @(
1421 @{ path = './.github/skills/my-skill' }
1422 )
1423 }
1424 }
1425 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1426
1427 Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{}
1428
1429 Test-Path (Join-Path $script:extDir '.github/skills/my-skill/SKILL.md') | Should -BeTrue
1430 Test-Path (Join-Path $script:extDir '.github/skills/my-skill/tests') | Should -BeFalse
1431 }
1432
1433 It 'Skips missing source files without error' {
1434 $pkgJson = @{
1435 contributes = @{
1436 chatAgents = @( @{ path = './.github/agents/nonexistent.agent.md' } )
1437 chatPromptFiles = @( @{ path = './.github/prompts/nonexistent.prompt.md' } )
1438 chatInstructions = @( @{ path = './.github/instructions/nonexistent.instructions.md' } )
1439 chatSkills = @( @{ path = './.github/skills/nonexistent' } )
1440 }
1441 }
1442 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1443
1444 { Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} } | Should -Not -Throw
1445 }
1446
1447 It 'Handles empty contributes sections' {
1448 $pkgJson = @{ contributes = @{} }
1449 $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1450
1451 { Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} } | Should -Not -Throw
1452 }
1453}
1454
1455Describe 'Invoke-PackageExtension - Collection mode' {
1456 BeforeAll {
1457 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "pkg-col-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
1458 $script:extDir = Join-Path $script:testRoot 'extension'
1459 $script:repoRoot = Join-Path $script:testRoot 'repo'
1460 }
1461
1462 BeforeEach {
1463 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
1464 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
1465 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
1466 New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null
1467 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
1468 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
1469 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
1470
1471 $manifest = @{
1472 name = 'test-ext'
1473 version = '1.0.0'
1474 publisher = 'test'
1475 engines = @{ vscode = '^1.80.0' }
1476 contributes = @{}
1477 }
1478 $manifest | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json')
1479 Set-Content -Path (Join-Path $script:extDir 'README.md') -Value '# Default README'
1480 }
1481
1482 AfterEach {
1483 if (Test-Path $script:testRoot) {
1484 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
1485 }
1486 }
1487
1488 It 'Uses collection-filtered artifact copy when Collection specified' {
1489 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1490 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1491
1492 $collectionPath = Join-Path $script:testRoot 'collection.yml'
1493 @"
1494id: developer
1495name: dev
1496displayName: Developer
1497items:
1498 - developer
1499"@ | Set-Content $collectionPath
1500
1501 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1502 Set-Content -Path $vsixPath -Value 'fake-vsix'
1503
1504 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -Collection $collectionPath
1505 $result | Should -BeOfType [hashtable]
1506 }
1507
1508 It 'Swaps collection README when collection has matching collection README' {
1509 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1510 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1511
1512 $collectionPath = Join-Path $script:testRoot 'collection.yml'
1513 @"
1514id: developer
1515name: dev
1516displayName: Developer
1517items:
1518 - developer
1519"@ | Set-Content $collectionPath
1520
1521 # Create collection README in extension directory
1522 Set-Content -Path (Join-Path $script:extDir 'README.developer.md') -Value '# Developer Collection'
1523
1524 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1525 Set-Content -Path $vsixPath -Value 'fake-vsix'
1526
1527 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -Collection $collectionPath
1528
1529 # README should be restored after packaging completes
1530 $readmeContent = Get-Content -Path (Join-Path $script:extDir 'README.md') -Raw
1531 $readmeContent | Should -Match 'Default README'
1532 $result | Should -BeOfType [hashtable]
1533 }
1534
1535 It 'Returns failure when no vsix file generated after successful vsce command' {
1536 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1537 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1538
1539 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1540
1541 $result.Success | Should -BeFalse
1542 $result.ErrorMessage | Should -Match 'No .vsix file found after packaging'
1543 }
1544}
1545
1546Describe 'CI Integration - Package-Extension' {
1547 BeforeAll {
1548 $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "ci-int-test-$([guid]::NewGuid().ToString('N').Substring(0,8))"
1549 $script:extDir = Join-Path $script:testRoot 'extension'
1550 $script:repoRoot = Join-Path $script:testRoot 'repo'
1551 }
1552
1553 AfterAll {
1554 if (Test-Path $script:testRoot) {
1555 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
1556 }
1557 }
1558
1559 Context 'GitHub Actions environment' {
1560 BeforeEach {
1561 Initialize-MockCIEnvironment
1562 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
1563 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
1564 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
1565 New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null
1566 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
1567 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
1568 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
1569
1570 $manifest = @{
1571 name = 'test-ext'
1572 version = '1.0.0'
1573 publisher = 'test'
1574 engines = @{ vscode = '^1.80.0' }
1575 }
1576 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
1577 }
1578
1579 AfterEach {
1580 Clear-MockCIEnvironment
1581 if (Test-Path $script:testRoot) {
1582 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
1583 }
1584 }
1585
1586 It 'Sets version output variable on successful package' {
1587 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1588 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1589
1590 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1591 Set-Content -Path $vsixPath -Value 'fake-vsix'
1592
1593 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1594
1595 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
1596 $outputContent | Should -Match 'version=1\.0\.0'
1597 }
1598
1599 It 'Sets vsix-file output variable on successful package' {
1600 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1601 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1602
1603 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1604 Set-Content -Path $vsixPath -Value 'fake-vsix'
1605
1606 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1607
1608 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
1609 $outputContent | Should -Match 'vsix-file=test-ext-1\.0\.0\.vsix'
1610 }
1611
1612 It 'Sets pre-release output variable when PreRelease specified' {
1613 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1614 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1615
1616 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1617 Set-Content -Path $vsixPath -Value 'fake-vsix'
1618
1619 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -PreRelease
1620
1621 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
1622 $outputContent | Should -Match 'pre-release=True'
1623 }
1624
1625 It 'Sets pre-release output variable to false when PreRelease not specified' {
1626 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1627 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1628
1629 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1630 Set-Content -Path $vsixPath -Value 'fake-vsix'
1631
1632 $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1633
1634 $outputContent = Get-Content $env:GITHUB_OUTPUT -Raw
1635 $outputContent | Should -Match 'pre-release=False'
1636 }
1637
1638 It 'Returns failure result when vsce command fails' {
1639 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1640 Mock Get-VscePackageCommand { return @{ Executable = 'pwsh'; Arguments = @('-Command', 'exit 1') } }
1641
1642 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1643
1644 $result.Success | Should -BeFalse
1645 $result.ErrorMessage | Should -Match 'vsce package command failed'
1646 }
1647 }
1648
1649 Context 'Local environment' {
1650 BeforeEach {
1651 Clear-MockCIEnvironment
1652
1653 New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null
1654 New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null
1655 New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null
1656 New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null
1657 New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null
1658 Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module'
1659 New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null
1660
1661 $manifest = @{
1662 name = 'test-ext'
1663 version = '1.0.0'
1664 publisher = 'test'
1665 engines = @{ vscode = '^1.80.0' }
1666 }
1667 $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json')
1668 }
1669
1670 AfterEach {
1671 if (Test-Path $script:testRoot) {
1672 Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
1673 }
1674 }
1675
1676 It 'Completes without error when not in CI environment' {
1677 Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } }
1678 Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } }
1679
1680 $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix'
1681 Set-Content -Path $vsixPath -Value 'fake-vsix'
1682
1683 $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot
1684
1685 $result.Success | Should -BeTrue
1686 }
1687 }
1688}
1689