diff --git a/.github/actions/setup-powershell-modules/action.yml b/.github/actions/setup-powershell-modules/action.yml new file mode 100644 index 0000000..80bc265 --- /dev/null +++ b/.github/actions/setup-powershell-modules/action.yml @@ -0,0 +1,29 @@ +name: Setup PowerShell modules +description: Installs pinned versions of Pester and PSScriptAnalyzer for the CI jobs. + +inputs: + shell: + description: Shell to run the installation in ('pwsh' or 'powershell') + required: false + default: pwsh + +runs: + using: composite + steps: + - name: Install Pester 5.7.1 + shell: ${{ inputs.shell }} + run: | + $ErrorActionPreference = 'Stop' + Install-Module -Name Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Repository PSGallery -Force -SkipPublisherCheck + + - name: Install PSScriptAnalyzer 1.25.0 + shell: ${{ inputs.shell }} + run: | + $ErrorActionPreference = 'Stop' + Install-Module -Name PSScriptAnalyzer -RequiredVersion 1.25.0 -Scope CurrentUser -Repository PSGallery -Force -SkipPublisherCheck + + - name: Install PSSQLite 1.1.0 + shell: ${{ inputs.shell }} + run: | + $ErrorActionPreference = 'Stop' + Install-Module -Name PSSQLite -RequiredVersion 1.1.0 -Scope CurrentUser -Repository PSGallery -Force -SkipPublisherCheck diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8cfcca7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +# Basic `dependabot.yml` file with +# minimum configuration for three package managers + +version: 2 +updates: + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + # Workflow files stored in the default location of `.github/workflows` + # You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`. + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" + commit-message: + prefix: "⬆️ chore(ci)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..40457af --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint-and-test-ps51: + name: Lint & Test (PowerShell 5.1, Windows 2022) + runs-on: windows-2022 + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - name: Install pinned modules (PowerShell 5.1) + uses: ./.github/actions/setup-powershell-modules + with: + shell: powershell + + # Build.ps1 spawns isolated test processes and prefers pwsh when available, + # so modules must also be present for pwsh. + - name: Install pinned modules (pwsh for isolated test processes) + uses: ./.github/actions/setup-powershell-modules + with: + shell: pwsh + + - name: Run Build (PSScriptAnalyzer + Pester) + shell: powershell + # Force the Windows PowerShell 5.1 engine for isolated Pester test processes, + # because Build.ps1 prefers pwsh when it is available. + run: .\Build.ps1 -PowerShellExecutable powershell + + lint-and-test-ps7: + name: Lint & Test (PowerShell 7, ${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + runner: [windows-2022, windows-2025] + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - name: Install pinned PowerShell modules + uses: ./.github/actions/setup-powershell-modules + with: + shell: pwsh + + - name: Run Build (PSScriptAnalyzer + Pester) + shell: pwsh + # Force the PowerShell Core engine for isolated Pester test processes. + run: .\Build.ps1 -PowerShellExecutable pwsh + + psscriptanalyzer: + name: PSScriptAnalyzer SARIF + runs-on: windows-2025 + permissions: + contents: read + security-events: write + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - name: Run PSScriptAnalyzer and save SARIF + uses: microsoft/psscriptanalyzer-action@v1.1 + with: + path: . + recurse: true + settings: PSScriptAnalyzerSettings.psd1 + output: analysis-results.sarif + + - name: Upload SARIF to GitHub code scanning + uses: github/codeql-action/upload-sarif@9e3211c9a3b9311dfe05da2ed48eea3386f042dd + with: + sarif_file: analysis-results.sarif + category: "/language:powershell" + + dependency-review: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - name: Dependency Review + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 + with: + fail-on-severity: high + allow-licenses: GPL-3.0-or-later, MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, Unlicense diff --git a/Build.ps1 b/Build.ps1 index bbd4f38..e35f1fa 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -10,15 +10,30 @@ PS C:\> .\Build.ps1 Runs all Pester tests and analyzes all PowerShell scripts in the project. +.EXAMPLE + PS C:\> .\Build.ps1 -PowerShellExecutable pwsh + Forces the PowerShell Core engine (pwsh) for isolated Pester test processes. + +.EXAMPLE + PS C:\> .\Build.ps1 -PowerShellExecutable powershell + Forces the Windows PowerShell 5.1 engine (powershell.exe) for isolated Pester test processes. + +.PARAMETER PowerShellExecutable + The PowerShell engine to use for isolated Pester test processes. + Valid values: 'auto' (default, picks pwsh if available, otherwise powershell), 'pwsh', or 'powershell'. + .NOTES - Version: 1.1.0 + Version: 1.2.0 Author: chriskyfung, Gemini License: GNU GPLv3 license Creation Date: 2025-08-02 - Last Modified: 2025-09-08 + Last Modified: 2026-08-07 #> -param() +param( + [ValidateSet('auto', 'pwsh', 'powershell')] + [string]$PowerShellExecutable = 'auto' +) $ErrorActionPreference = "Stop" @@ -42,15 +57,30 @@ try { # Determine the correct PowerShell executable to use for isolated processes $executable = '' - if (Get-Command -Name 'pwsh' -ErrorAction SilentlyContinue) { + if ($PowerShellExecutable -eq 'pwsh') { + if (Get-Command -Name 'pwsh' -ErrorAction SilentlyContinue) { + $executable = 'pwsh' + } + else { + throw "Requested 'pwsh' for isolated Pester tests, but it is not available." + } + } + elseif ($PowerShellExecutable -eq 'powershell') { + if (Get-Command -Name 'powershell' -ErrorAction SilentlyContinue) { + $executable = 'powershell' + } + else { + throw "Requested 'powershell' for isolated Pester tests, but it is not available." + } + } + elseif (Get-Command -Name 'pwsh' -ErrorAction SilentlyContinue) { $executable = 'pwsh' } elseif (Get-Command -Name 'powershell' -ErrorAction SilentlyContinue) { $executable = 'powershell' } else { - Write-Error "Could not find 'pwsh' or 'powershell' executable to run isolated Pester tests." - exit 1 + throw "Could not find 'pwsh' or 'powershell' executable to run isolated Pester tests." } Write-Host "Using '$executable' for isolated test execution." @@ -80,8 +110,7 @@ try { } if ($overallResult.FailedCount -gt 0) { - Write-Error "$($overallResult.FailedCount) test file(s) contained failures." - exit 1 + throw "$($overallResult.FailedCount) test file(s) contained failures." } } else { diff --git a/Tests/Bluestacks/Optimize-BluestacksVEthernet.Tests.ps1 b/Tests/Bluestacks/Optimize-BluestacksVEthernet.Tests.ps1 index 38fc11a..331ed1d 100644 --- a/Tests/Bluestacks/Optimize-BluestacksVEthernet.Tests.ps1 +++ b/Tests/Bluestacks/Optimize-BluestacksVEthernet.Tests.ps1 @@ -3,14 +3,16 @@ Tests for the Optimize-BluestacksVEthernet.ps1 script. #> -Describe "Optimize-BluestacksVEthernet" -Tags "CI" { +# Must be top-level: Pester Discovery evaluates -Skip: before BeforeAll runs. +$script:SkipAll = ($PSEdition -eq 'Core') -or [bool]$env:CI +Describe "Optimize-BluestacksVEthernet" -Tags "CI", "DesktopOnly" { BeforeAll { # Get the absolute path to the script under test $script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\Bluestacks\Optimize-BluestacksVEthernet.ps1" } - It "Should run without errors" -Skip:(-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + It "Should run without errors" -Skip:($script:SkipAll -or -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { # Code that requires admin permissions Write-Host "Running test with administrative privileges..." -ForegroundColor Green Mock Get-NetAdapter { diff --git a/Tests/OneNote/Find-OneNotePages.Tests.ps1 b/Tests/OneNote/Find-OneNotePages.Tests.ps1 index 443ad88..2ca76d4 100644 --- a/Tests/OneNote/Find-OneNotePages.Tests.ps1 +++ b/Tests/OneNote/Find-OneNotePages.Tests.ps1 @@ -3,15 +3,17 @@ Tests for Find-OneNotePages.ps1 #> -Describe "Find-OneNotePages.ps1" { +# Must be top-level: Pester Discovery evaluates -Skip: before BeforeAll runs. +$script:SkipAll = [bool]$env:CI +Describe "Find-OneNotePages.ps1" -Tag "Integration" { BeforeAll { # Set the path to the script under test. $script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\OneNote\Find-OneNotePages.ps1" } # This is an integration test that requires a running OneNote instance. - It "should return formatted output when pages are found" -Tag 'Integration' { + It "should return formatted output when pages are found" -Skip:$script:SkipAll { $output = (& $script:ScriptPath -Query "MyNote" | Out-String).Trim() $output | Should -Match "Test Notebook" $output | Should -Match " > Test Section" @@ -23,7 +25,7 @@ Describe "Find-OneNotePages.ps1" { $output | Should -Match "URI : " } - It "should return a warning when no pages are found" { + It "should return a warning when no pages are found" -Skip:$script:SkipAll { $output = (& $script:ScriptPath -Query "NonExistentPage" | Out-String).Trim() $output | Should -BeNullOrEmpty $output = (& $script:ScriptPath -Query "NonExistentPage" 3>&1 | Out-String).Trim() diff --git a/Tests/OneNote/Out-OneNoteSections.Tests.ps1 b/Tests/OneNote/Out-OneNoteSections.Tests.ps1 index 99f1cc0..fe3a062 100644 --- a/Tests/OneNote/Out-OneNoteSections.Tests.ps1 +++ b/Tests/OneNote/Out-OneNoteSections.Tests.ps1 @@ -3,15 +3,17 @@ Tests for Out-OneNoteSections.ps1 #> -Describe "Out-OneNoteSections.ps1" { +# Must be top-level: Pester Discovery evaluates -Skip: before BeforeAll runs. +$script:SkipAll = [bool]$env:CI +Describe "Out-OneNoteSections.ps1" -Tag "Integration" { BeforeAll { # Set the path to the script under test. $script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\OneNote\Out-OneNoteSections.ps1" } Context "When OneNote has notebooks" { - It "should list all notebooks and their sections" { + It "should list all notebooks and their sections" -Skip:$script:SkipAll { $output = (& $script:ScriptPath | Out-String).Trim() $output | Should -Match "Archive" $output | Should -Match "### Ideas" diff --git a/Tests/VSCode/Export-VSCodeExtensionList.Tests.ps1 b/Tests/VSCode/Export-VSCodeExtensionList.Tests.ps1 index a6a679d..ccbfed8 100644 --- a/Tests/VSCode/Export-VSCodeExtensionList.Tests.ps1 +++ b/Tests/VSCode/Export-VSCodeExtensionList.Tests.ps1 @@ -85,7 +85,7 @@ Describe "Export-VSCodeExtensionList.ps1" { It "writes a header with generation timestamp and machine name" { $content = Get-Content -Path $script:ExpectedOutputFile -Raw - $content | Should -Match "VS Code Profile & Extension Export" + $content | Should -Match "VS Code Profile and Extension Export" $content | Should -Match "Generated:" $content | Should -Match "Machine:\s+$env:COMPUTERNAME" } diff --git a/Tests/VSCode/Export-VSCodeProfiles.Tests.ps1 b/Tests/VSCode/Export-VSCodeProfiles.Tests.ps1 index 4f5c1df..5a093cb 100644 --- a/Tests/VSCode/Export-VSCodeProfiles.Tests.ps1 +++ b/Tests/VSCode/Export-VSCodeProfiles.Tests.ps1 @@ -14,8 +14,11 @@ extension export, settings backup, manifest generation, and WhatIf behavior. #> #Requires -Version 5.0 +#Requires -PSEdition Desktop #Requires -Module Pester +$script:SkipAll = $PSEdition -eq 'Core' + Describe "Export-VSCodeProfiles.ps1" { BeforeAll { # Use a temp directory to avoid polluting the user's Documents folder @@ -55,26 +58,26 @@ echo ext2.vscode' -Encoding ASCII } Context "Parameter validation" { - It "accepts a custom OutputDirectory" { + It "accepts a custom OutputDirectory" -Skip:$script:SkipAll { { & $script:ScriptPath -OutputDirectory (Join-Path $script:TestRoot "out") -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath -WhatIf } | Should -Not -Throw } - It "accepts a custom VSCodeUserDataPath" { + It "accepts a custom VSCodeUserDataPath" -Skip:$script:SkipAll { { & $script:ScriptPath -OutputDirectory (Join-Path $script:TestRoot "out2") -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath -WhatIf } | Should -Not -Throw } - It "throws for invalid OutputDirectory characters" { + It "throws for invalid OutputDirectory characters" -Skip:$script:SkipAll { { & $script:ScriptPath -OutputDirectory "C:\bad|path" -WhatIf } | Should -Throw "*OutputDirectory contains invalid path characters*" } } Context "Prerequisite checks" { - It "throws when VSCodeUserDataPath does not exist" { + It "throws when VSCodeUserDataPath does not exist" -Skip:$script:SkipAll { { & $script:ScriptPath -VSCodeUserDataPath "C:\nonexistent\path" -WhatIf } | Should -Throw "VS Code user data directory not found:*" } - It "throws when storage.json is missing" { + It "throws when storage.json is missing" -Skip:$script:SkipAll { Remove-Item -Path (Join-Path $script:TestRoot "User\globalStorage\storage.json") -Force try { { & $script:ScriptPath -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -WhatIf } | Should -Throw "VS Code storage.json not found*" @@ -84,7 +87,7 @@ echo ext2.vscode' -Encoding ASCII } } - It "throws when 'code' CLI is not in PATH" { + It "throws when 'code' CLI is not in PATH" -Skip:$script:SkipAll { { & $script:ScriptPath -VSCodeUserDataPath (Join-Path $script:TestRoot "User") ` -CodeCommand "nonexistent_code_binary_xyz" -WhatIf } | Should -Throw "VS Code CLI*not available*" @@ -92,7 +95,7 @@ echo ext2.vscode' -Encoding ASCII } Context "Profile discovery" { - It "discovers profiles from storage.json" { + It "discovers profiles from storage.json" -Skip:$script:SkipAll { $outputDir = Join-Path $script:TestRoot "out_discover" $output = (& $script:ScriptPath -OutputDirectory $outputDir -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath -WhatIf 6>&1) | Out-String @@ -102,7 +105,7 @@ echo ext2.vscode' -Encoding ASCII } Context "Extension export" { - It "exports extensions and manifest using a custom CodeCommand" { + It "exports extensions and manifest using a custom CodeCommand" -Skip:$script:SkipAll { $outputDir = Join-Path $script:TestRoot "out_export" & $script:ScriptPath -OutputDirectory $outputDir -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath @@ -115,7 +118,7 @@ echo ext2.vscode' -Encoding ASCII } Context "WhatIf behavior" { - It "does not create output files when -WhatIf is specified" { + It "does not create output files when -WhatIf is specified" -Skip:$script:SkipAll { $outputDir = Join-Path $script:TestRoot "out_whatif" { & $script:ScriptPath -OutputDirectory $outputDir -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath -WhatIf } | Should -Not -Throw $outputDir | Should -Not -Exist diff --git a/Tests/Windows/Get-DiskReliabilityCounter.Tests.ps1 b/Tests/Windows/Get-DiskReliabilityCounter.Tests.ps1 index ceb6079..4e54001 100644 --- a/Tests/Windows/Get-DiskReliabilityCounter.Tests.ps1 +++ b/Tests/Windows/Get-DiskReliabilityCounter.Tests.ps1 @@ -3,6 +3,9 @@ Tests for Get-DiskReliabilityCounter.ps1 script. #> +#Requires -PSEdition Desktop +$script:SkipAll = $PSEdition -eq 'Core' + Describe "Get-DiskReliabilityCounter Script" -Tag "CI" { BeforeAll { @@ -50,7 +53,7 @@ Describe "Get-DiskReliabilityCounter Script" -Tag "CI" { } Context "Execution" { - It "Should execute without throwing" -Skip:(-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + It "Should execute without throwing" -Skip:($script:SkipAll -or -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { # Code that requires admin permissions { & $script:ScriptPath } | Should -Not -Throw } diff --git a/Tests/Windows/Optimize-DockerDesktopVHD.Tests.ps1 b/Tests/Windows/Optimize-DockerDesktopVHD.Tests.ps1 index eed77ed..9d912a5 100644 --- a/Tests/Windows/Optimize-DockerDesktopVHD.Tests.ps1 +++ b/Tests/Windows/Optimize-DockerDesktopVHD.Tests.ps1 @@ -3,6 +3,9 @@ Tests for Optimize-DockerDesktopVHD.ps1 script. #> +#Requires -PSEdition Desktop +$script:SkipAll = $PSEdition -eq 'Core' + Describe "Optimize-DockerDesktopVHD Script" -Tag "CI" { BeforeAll { @@ -126,7 +129,7 @@ Describe "Optimize-DockerDesktopVHD Script" -Tag "CI" { } } - It "Should use default VHDX path when not specified" { + It "Should use default VHDX path when not specified" -Skip:$script:SkipAll { Mock Test-Path { $true } -ParameterFilter { $Path -like "*docker_data.vhdx*" } Mock Get-Item { [PSCustomObject]@{ Length = 1048576 } } Mock Get-Process { $null } -ParameterFilter { $Name -eq "Docker Desktop" } @@ -135,7 +138,7 @@ Describe "Optimize-DockerDesktopVHD Script" -Tag "CI" { { & $script:ScriptPath -VhdPath $script:TestVhdPath } | Should -Not -Throw } - It "Should accept custom VHDX path" { + It "Should accept custom VHDX path" -Skip:$script:SkipAll { Mock Test-Path { $true } -ParameterFilter { $Path -eq $script:TestVhdPath } Mock Get-Item { [PSCustomObject]@{ Length = 1048576 } } -ParameterFilter { $Path -eq $script:TestVhdPath } Mock Get-Process { $null } @@ -155,7 +158,7 @@ Describe "Optimize-DockerDesktopVHD Script" -Tag "CI" { } } - It "Should call Optimize-VHD with correct parameters" { + It "Should call Optimize-VHD with correct parameters" -Skip:$script:SkipAll { Mock Test-Path { $true } Mock Get-Item { [PSCustomObject]@{ Length = 1048576 } } Mock Get-Process { $null } @@ -166,7 +169,7 @@ Describe "Optimize-DockerDesktopVHD Script" -Tag "CI" { Assert-MockCalled Optimize-VHD -ParameterFilter { $Mode -eq 'Full' } -Scope It } - It "Should not prompt to stop Docker when not running" { + It "Should not prompt to stop Docker when not running" -Skip:$script:SkipAll { Mock Get-Process { $null } -ParameterFilter { $Name -eq "Docker Desktop" } Mock Test-Path { $true } Mock Get-Item { [PSCustomObject]@{ Length = 1048576 } } diff --git a/Tests/Windows/Optimize-WslDistroVHD.Tests.ps1 b/Tests/Windows/Optimize-WslDistroVHD.Tests.ps1 index 81d6909..8f08366 100644 --- a/Tests/Windows/Optimize-WslDistroVHD.Tests.ps1 +++ b/Tests/Windows/Optimize-WslDistroVHD.Tests.ps1 @@ -3,9 +3,14 @@ Tests for Optimize-WslDistroVHD.ps1 script. #> +#Requires -PSEdition Desktop +$script:SkipAll = $PSEdition -eq 'Core' + Describe "Optimize-WslDistroVHD Script" -Tag "CI" { BeforeAll { + if ($script:SkipAll) { return } + # Get the absolute path to the script under test $script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\Windows\Optimize-WslDistroVHD.ps1" @@ -22,17 +27,17 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { } Context "Script Structure" { - It "Should exist" { + It "Should exist" -Skip:$script:SkipAll { Test-Path -Path $script:ScriptPath -PathType Leaf | Should -Be $true } - It "Should contain valid PowerShell syntax" { + It "Should contain valid PowerShell syntax" -Skip:$script:SkipAll { $errors = $null $null = [System.Management.Automation.PSParser]::Tokenize((Get-Content -Path $script:ScriptPath -Raw), [ref]$errors) $errors.Count | Should -Be 0 } - It "Should have Requires-Version 5.1 or higher" { + It "Should have Requires-Version 5.1 or higher" -Skip:$script:SkipAll { $content = Get-Content -Path $script:ScriptPath -Raw $content | Should -Match "#Requires\s+-Version\s+5\.[1-9]" } @@ -43,23 +48,23 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { $scriptContent = Get-Content -Path $script:ScriptPath -Raw } - It "Should have a SYNOPSIS section" { + It "Should have a SYNOPSIS section" -Skip:$script:SkipAll { $scriptContent | Should -Match "\.SYNOPSIS" } - It "Should have a DESCRIPTION section" { + It "Should have a DESCRIPTION section" -Skip:$script:SkipAll { $scriptContent | Should -Match "\.DESCRIPTION" } - It "Should have at least one EXAMPLE section" { + It "Should have at least one EXAMPLE section" -Skip:$script:SkipAll { $scriptContent | Should -Match "\.EXAMPLE" } - It "Should have a NOTES section" { + It "Should have a NOTES section" -Skip:$script:SkipAll { $scriptContent | Should -Match "\.NOTES" } - It "Should have a LINK section" { + It "Should have a LINK section" -Skip:$script:SkipAll { $scriptContent | Should -Match "\.LINK" } } @@ -69,27 +74,27 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { $scriptContent = Get-Content -Path $script:ScriptPath -Raw } - It "Should have DistroName parameter" { + It "Should have DistroName parameter" -Skip:$script:SkipAll { $scriptContent | Should -Match '\[string\]\$DistroName' } - It "Should have VhdPath parameter" { + It "Should have VhdPath parameter" -Skip:$script:SkipAll { $scriptContent | Should -Match '\[string\]\$VhdPath' } - It "Should have Mode parameter" { + It "Should have Mode parameter" -Skip:$script:SkipAll { $scriptContent | Should -Match '\[string\]\$Mode' } - It "Should have ValidateSet for Mode parameter with Full, Retain, None" { + It "Should have ValidateSet for Mode parameter with Full, Retain, None" -Skip:$script:SkipAll { $scriptContent | Should -Match "ValidateSet\('Full',\s*'Retain',\s*'None'\)" } - It "Should default Mode to Full" { + It "Should default Mode to Full" -Skip:$script:SkipAll { $scriptContent | Should -Match '\[string\]\$Mode\s*=\s*''Full''' } - It "Should support ShouldProcess" { + It "Should support ShouldProcess" -Skip:$script:SkipAll { $scriptContent | Should -Match "SupportsShouldProcess\s*=\s*\`$true" } } @@ -99,23 +104,23 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { $scriptContent = Get-Content -Path $script:ScriptPath -Raw } - It "Should define Get-WslDistroList function" { + It "Should define Get-WslDistroList function" -Skip:$script:SkipAll { $scriptContent | Should -Match "function\s+Get-WslDistroList" } - It "Should define Get-WslDistroVhdPath function" { + It "Should define Get-WslDistroVhdPath function" -Skip:$script:SkipAll { $scriptContent | Should -Match "function\s+Get-WslDistroVhdPath" } - It "Should define Select-WslDistro function" { + It "Should define Select-WslDistro function" -Skip:$script:SkipAll { $scriptContent | Should -Match "function\s+Select-WslDistro" } - It "Should define Stop-WslDistro function" { + It "Should define Stop-WslDistro function" -Skip:$script:SkipAll { $scriptContent | Should -Match "function\s+Stop-WslDistro" } - It "Should use ShouldContinue in Stop-WslDistro for user confirmation" { + It "Should use ShouldContinue in Stop-WslDistro for user confirmation" -Skip:$script:SkipAll { $scriptContent | Should -Match "ShouldContinue" } } @@ -142,7 +147,7 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { } } - It "Should optimize a VHDX when -VhdPath is provided directly" { + It "Should optimize a VHDX when -VhdPath is provided directly" -Skip:$script:SkipAll { Mock Get-Item { [PSCustomObject]@{ Length = 1048576 } } Mock Test-Path { $true } Mock Optimize-VHD { } @@ -161,7 +166,7 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { Assert-MockCalled Optimize-VHD -Times 1 -Scope It } - It "Should accept custom VHDX path" { + It "Should accept custom VHDX path" -Skip:$script:SkipAll { Mock Test-Path { $true } -ParameterFilter { $Path -eq $script:TestVhdPath } Mock Get-Item { [PSCustomObject]@{ Length = 1048576 } } -ParameterFilter { $Path -eq $script:TestVhdPath } Mock Optimize-VHD { } @@ -180,7 +185,7 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { Assert-MockCalled Optimize-VHD -Times 1 -Scope It } - It "Should call Optimize-VHD with correct parameters" { + It "Should call Optimize-VHD with correct parameters" -Skip:$script:SkipAll { Mock Test-Path { $true } Mock Get-Item { [PSCustomObject]@{ Length = 1048576 } } Mock Optimize-VHD { } @@ -192,7 +197,7 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { Assert-MockCalled Optimize-VHD -ParameterFilter { $Mode -eq 'Full' } -Scope It } - It "Should not stop WSL distro when not running" { + It "Should not stop WSL distro when not running" -Skip:$script:SkipAll { Mock Test-Path { $true } Mock Get-Item { [PSCustomObject]@{ Length = 1048576 } } Mock Optimize-VHD { } @@ -208,12 +213,12 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { $env:LOCALAPPDATA = $env:TEMP } - It "Should throw when VHDX file not found" { + It "Should throw when VHDX file not found" -Skip:$script:SkipAll { $nonExistentPath = Join-Path $env:TEMP "non-existent.vhdx" { Start-WslDistroVhdOptimization -VhdPath $nonExistentPath } | Should -Throw } - It "Should throw when file is not .vhdx extension" { + It "Should throw when file is not .vhdx extension" -Skip:$script:SkipAll { $invalidPath = Join-Path $env:TEMP "invalid.txt" "" | Set-Content -Path $invalidPath { Start-WslDistroVhdOptimization -VhdPath $invalidPath } | Should -Throw @@ -230,12 +235,12 @@ Describe "Optimize-WslDistroVHD Script" -Tag "CI" { Mock Read-Host { "1" } } - It "Should support WhatIf parameter" { + It "Should support WhatIf parameter" -Skip:$script:SkipAll { $scriptContent = Get-Content -Path $script:ScriptPath -Raw $scriptContent | Should -Match "SupportsShouldProcess\s*=\s*\`$true" } - It "Should not call Optimize-VHD when -WhatIf is used" { + It "Should not call Optimize-VHD when -WhatIf is used" -Skip:$script:SkipAll { Start-WslDistroVhdOptimization -VhdPath $script:TestVhdPath -Mode Full -WhatIf Should -Invoke Optimize-VHD -Times 0 -Scope It } diff --git a/Tests/theBrain/Format-TheBrainNotesYouTubeThumbnail.Tests.ps1 b/Tests/theBrain/Format-TheBrainNotesYouTubeThumbnail.Tests.ps1 index 46628f1..47a1dfb 100644 --- a/Tests/theBrain/Format-TheBrainNotesYouTubeThumbnail.Tests.ps1 +++ b/Tests/theBrain/Format-TheBrainNotesYouTubeThumbnail.Tests.ps1 @@ -1,9 +1,14 @@ # Test for Format-TheBrainNotesYouTubeThumbnail.ps1 # Requires -Modules Pester -BeforeAll { - # Path to the script being tested - $script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\theBrain\Format-TheBrainNotesYouTubeThumbnail.ps1" +# NOTE: Must be top-level (not inside BeforeAll) so Pester Discovery phase +# can evaluate -Skip: expressions before BeforeAll runs. +$script:SkipAll = $PSEdition -eq 'Core' + + + BeforeAll { + # Path to the script being tested + $script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\theBrain\Format-TheBrainNotesYouTubeThumbnail.ps1" # Set up a temporary file system structure to simulate TheBrain's data $script:TestDrive = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "Test-FormatTheBrainYouTubeThumbnail") -Force @@ -24,8 +29,6 @@ BeforeAll { $script:OriginalContent = '[![Test Alt Text](.data/md-images/thumbnail123.jpg)](https://youtu.be/VIDEO123)' Set-Content -Path $script:NotesFile -Value $script:OriginalContent -Encoding UTF8 - # Mock the external script dependency to return a temporary path - Mock Invoke-SqliteQuery { return [PSCustomObject]@{ Value = """$script:TestDrive""" } } -Verifiable } AfterAll { @@ -33,7 +36,7 @@ AfterAll { Remove-Item -Path $script:TestDrive.FullName -Recurse -Force } -Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' { +Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' -Tag "DesktopOnly" { BeforeEach { # Reset all mocks before each test to ensure isolation @@ -49,7 +52,7 @@ Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' { Mock Convert-Path { return $Path } -Verifiable } - It 'should find, back up, and replace a YouTube thumbnail link' { + It 'should find, back up, and replace a YouTube thumbnail link' -Skip:$script:SkipAll { # Arrange # This object simulates the output of Select-String with a found match $MatchObject = @( @@ -75,7 +78,7 @@ Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' { Mock New-Item { return [pscustomobject]@{ FullName = $Path[0] } } -Verifiable # Act - . $script:ScriptPath + . $script:ScriptPath -DataDirectory $script:BrainFolder # Assert $ExpectedNewString = '[![Test Alt Text](https://img.youtube.com/vi/VIDEO123/maxresdefault.jpg)](https://www.youtube.com/watch?v=VIDEO123)' @@ -101,14 +104,14 @@ Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' { } } - It 'should do nothing if no matching links are found' { + It 'should do nothing if no matching links are found' -Skip:$script:SkipAll { # Arrange # Mock Select-String to return no matches Mock Get-ChildItem -Verifiable Mock Select-String { return $null } -Verifiable # Act - . $script:ScriptPath + . $script:ScriptPath -DataDirectory $script:BrainFolder # Assert # Ensure no file operations were attempted @@ -122,7 +125,7 @@ Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' { } } - It 'should handle errors during file operations' { + It 'should handle errors during file operations' -Skip:$script:SkipAll { # Arrange # Simulate a match being found, same as the happy path test $MatchObject = @( diff --git a/Tests/theBrain/Get-TheBrainDataDirectory.Tests.ps1 b/Tests/theBrain/Get-TheBrainDataDirectory.Tests.ps1 index 7e52d25..a29d24c 100644 --- a/Tests/theBrain/Get-TheBrainDataDirectory.Tests.ps1 +++ b/Tests/theBrain/Get-TheBrainDataDirectory.Tests.ps1 @@ -66,4 +66,22 @@ Describe 'Get-TheBrainDataDirectory' { $result | Should -Be (Join-Path 'C:\Users\TestUser\Documents' -ChildPath 'Brains') } } + + Context 'When -DataDirectory is provided' { + It 'should return the provided directory without querying the database' { + $testDir = Join-Path $env:TEMP 'TestBrainDataDir' + New-Item -ItemType Directory -Path $testDir -Force | Out-Null + + Mock Test-Path { $true } -ParameterFilter { $Path -eq $testDir } + Mock Invoke-SqliteQuery { throw 'Should not be called' } -Verifiable + + $result = & $script:ScriptPath -DataDirectory $testDir + $result | Should -Be $testDir + + # Should not have called Invoke-SqliteQuery since -DataDirectory bypasses it + Should -Not -Invoke Invoke-SqliteQuery + + Remove-Item -Path $testDir -Recurse -Force + } + } } diff --git a/Tests/theBrain/Get-TheBrainNotesLinks.Tests.ps1 b/Tests/theBrain/Get-TheBrainNotesLinks.Tests.ps1 index 681ad0b..0e221a6 100644 --- a/Tests/theBrain/Get-TheBrainNotesLinks.Tests.ps1 +++ b/Tests/theBrain/Get-TheBrainNotesLinks.Tests.ps1 @@ -2,51 +2,59 @@ # # To run these tests, run `Invoke-Pester` in the root of the repository. -Describe "Get-TheBrainNotesLinks.ps1" { +# NOTE: These flags must be top-level (not inside BeforeAll) so the Pester +# Discovery phase can evaluate -Skip: expressions before BeforeAll runs. +$script:SkipAll = $PSEdition -eq 'Core' + +Describe "Get-TheBrainNotesLinks.ps1" -Tag "DesktopOnly" { BeforeAll { # Path to the script being tested $script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\theBrain\Get-TheBrainNotesLinks.ps1" # # Create a temporary directory structure for testing - $tempDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "Test-GetTheBrainLinks") -Force - $thought1Dir = New-Item -Path (Join-Path $tempDir "Thought1") -ItemType Directory - $thought2Dir = New-Item -Path (Join-Path $tempDir "Thought2") -ItemType Directory - $backupDir = New-Item -ItemType Directory -Path (Join-Path $tempDir "Backup") -Force - $thought3Dir = New-Item -Path (Join-Path $backupDir "Thought3") -ItemType Directory + # NOTE: These must be script-scoped so they are visible in the It blocks, + # because Pester v5 BeforeAll runs in a separate scope. + $script:tempDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "Test-GetTheBrainLinks") -Force + $script:thought1Dir = New-Item -Path (Join-Path $script:tempDir "Thought1") -ItemType Directory + $script:thought2Dir = New-Item -Path (Join-Path $script:tempDir "Thought2") -ItemType Directory + $script:backupDir = New-Item -ItemType Directory -Path (Join-Path $script:tempDir "Backup") -Force + $script:thought3Dir = New-Item -Path (Join-Path $script:backupDir "Thought3") -ItemType Directory # # Create dummy Notes.md files - Set-Content -Path (Join-Path $thought1Dir "Notes.md") -Value "This note contains a [valid link](https://www.google.com). This is not a link: [invalid link](htp://invalid-url)." - Set-Content -Path (Join-Path $thought2Dir "Notes.md") -Value "This note has no links." - Set-Content -Path (Join-Path $thought3Dir "Notes.md") -Value "This note is in a backup folder and should be ignored: [backup link](https://www.yahoo.com)." + Set-Content -Path (Join-Path $script:thought1Dir "Notes.md") -Value "This note contains a [valid link](https://www.google.com). This is not a link: [invalid link](htp://invalid-url)." + Set-Content -Path (Join-Path $script:thought2Dir "Notes.md") -Value "This note has no links." + Set-Content -Path (Join-Path $script:thought3Dir "Notes.md") -Value "This note is in a backup folder and should be ignored: [backup link](https://www.yahoo.com)." # Mock Format-List to prevent UI from showing during tests Mock Format-List { return @( $_ ) } -Verifiable } AfterAll { - # Clean up the temporary directory - Remove-Item -Path $tempDir -Recurse -Force + # Clean up the temporary directory (guard against a failed BeforeAll) + if ($script:tempDir -and (Test-Path -LiteralPath $script:tempDir)) { + Remove-Item -Path $script:tempDir -Recurse -Force + } } Context "When searching for links" { - It "should find 1 link in Notes.md files" { - $results = & $script:ScriptPath -Path $tempDir + It "should find 1 link in Notes.md files" -Skip:$script:SkipAll { + $results = & $script:ScriptPath -Path $script:tempDir $results | Should -Not -BeNullOrEmpty $results.Count | Should -BeNullOrEmpty $results[0].LinkText | Should -Be "valid link" $results[0].URL | Should -Be "https://www.google.com" } - It "should find 3 links in Notes.md files" { + It "should find 3 links in Notes.md files" -Skip:$script:SkipAll { # Update the Notes.md in Thought1 to have another valid link - Set-Content -Path (Join-Path $thought1Dir "Notes.md") -Value "This note contains a [valid link to Google](https://www.google.com) and a [valid link to Bing](https://www.bing.com). This is not a link: [invalid link](htp://invalid-url)." + Set-Content -Path (Join-Path $script:thought1Dir "Notes.md") -Value "This note contains a [valid link to Google](https://www.google.com) and a [valid link to Bing](https://www.bing.com). This is not a link: [invalid link](htp://invalid-url)." # Add a valid link to Thought2 - Set-Content -Path (Join-Path $thought2Dir "Notes.md") -Value "This note contains a [valid link to Facebook](https://www.facebook.com)." + Set-Content -Path (Join-Path $script:thought2Dir "Notes.md") -Value "This note contains a [valid link to Facebook](https://www.facebook.com)." - $results = & $script:ScriptPath -Path $tempDir + $results = & $script:ScriptPath -Path $script:tempDir $results | Should -Not -BeNullOrEmpty $results.Count | Should -Be 3 - $results | ForEach-Object { $_.Path } | Should -Not -Contain (Join-Path $thought3Dir "Notes.md") + $results | ForEach-Object { $_.Path } | Should -Not -Contain (Join-Path $script:thought3Dir "Notes.md") $results[0].LinkText | Should -Be "valid link to Google" $results[0].URL | Should -Be "https://www.google.com" $results[1].LinkText | Should -Be "valid link to Bing" @@ -55,18 +63,18 @@ Describe "Get-TheBrainNotesLinks.ps1" { $results[2].URL | Should -Be "https://www.facebook.com" # Revert changes - Set-Content -Path (Join-Path $thought1Dir "Notes.md") -Value "This note contains a [valid link](https://www.google.com). This is not a link: [invalid link](htp://invalid-url)." - Set-Content -Path (Join-Path $thought2Dir "Notes.md") -Value "This note has no links." + Set-Content -Path (Join-Path $script:thought1Dir "Notes.md") -Value "This note contains a [valid link](https://www.google.com). This is not a link: [invalid link](htp://invalid-url)." + Set-Content -Path (Join-Path $script:thought2Dir "Notes.md") -Value "This note has no links." } - It "should ignore the 'Backup' directory" { - $results = & $script:ScriptPath -Path $tempDir + It "should ignore the 'Backup' directory" -Skip:$script:SkipAll { + $results = & $script:ScriptPath -Path $script:tempDir $results | Should -Not -BeNullOrEmpty - $results | ForEach-Object { $_.Path } | Should -Not -Contain (Join-Path $thought3Dir "Notes.md") + $results | ForEach-Object { $_.Path } | Should -Not -Contain (Join-Path $script:thought3Dir "Notes.md") $results | ForEach-Object { $_.URL } | Should -Not -Contain "https://www.yahoo.com" } - It "should return an empty result if no links are found" { + It "should return an empty result if no links are found" -Skip:$script:SkipAll { $emptyTempDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "EmptyTestBrain") -Force $emptyThoughtDir = New-Item -Path (Join-Path $emptyTempDir "EmptyThought") -ItemType Directory Set-Content -Path (Join-Path $emptyThoughtDir "Notes.md") -Value "No links here." @@ -79,9 +87,9 @@ Describe "Get-TheBrainNotesLinks.ps1" { } Context "With -OutputPath parameter" { - It "should export the results to a CSV file" { - $outputCsv = Join-Path $tempDir "links.csv" - & $script:ScriptPath -Path $tempDir -OutputPath $outputCsv + It "should export the results to a CSV file" -Skip:$script:SkipAll { + $outputCsv = Join-Path $script:tempDir "links.csv" + & $script:ScriptPath -Path $script:tempDir -OutputPath $outputCsv Test-Path $outputCsv | Should -Be $true $csvContent = Import-Csv -Path $outputCsv @@ -90,48 +98,39 @@ Describe "Get-TheBrainNotesLinks.ps1" { Remove-Item -Path $outputCsv -Force } - It "should sanitize fields to prevent CSV injection" { + It "should sanitize fields to prevent CSV injection" -Skip:($script:SkipAll) { $maliciousLinkText = '=HYPERLINK("cmd.exe","/c dir")' $maliciousURL = '+A1+B1' $maliciousContent = "This note contains a [$maliciousLinkText]($maliciousURL)." - $maliciousNotesDir = New-Item -ItemType Directory -Path (Join-Path $tempDir "ThoughtMalicious") -Force + $maliciousNotesDir = New-Item -ItemType Directory -Path (Join-Path $script:tempDir "ThoughtMalicious") -Force $maliciousNotesFile = Join-Path $maliciousNotesDir "Notes.md" Set-Content -Path $maliciousNotesFile -Value $maliciousContent # Mock Get-ChildItem - Mock Get-ChildItem { - param($Path, $Filter, $Recurse, $Directory, $Exclude) - if ($Filter -eq 'Notes.md') { - # This is the call that searches for Notes.md files - return @(Get-Item $maliciousNotesFile) - } - if ($Directory) { - # This is the call that gets the base directory for thebrain notes - return New-Item -ItemType Directory -Path (Join-Path $tempDir "ThoughtMalicious") -Force - } - return $null # Default for other Get-ChildItem calls - } + Mock Get-ChildItem { return @(Get-Item $maliciousNotesFile) } -ParameterFilter { $Filter -eq 'Notes.md' } -Verifiable + Mock Get-ChildItem { return @(Get-Item $maliciousNotesDir) } -ParameterFilter { $Directory.IsPresent } -Verifiable + Mock Get-ChildItem { return $null } -Verifiable # Default for other Get-ChildItem calls # Mock Select-String to return the malicious link Mock Select-String { - [PSCustomObject]@{ - Path = $maliciousNotesFile - LineNumber = 1 - Matches = @( - [PSCustomObject]@{ # This is a single 'Match' object - Groups = @( - [PSCustomObject]@{ Value = "$maliciousLinkText($maliciousURL)" }, # Group 0 (full match, approximate) - [PSCustomObject]@{ Value = $maliciousLinkText }, # Group 1 - [PSCustomObject]@{ Value = $maliciousURL } # Group 2 - ) - } - ) + [PSCustomObject]@{ + Path = $maliciousNotesFile + LineNumber = 1 + Matches = @( + [PSCustomObject]@{ # This is a single 'Match' object + Groups = @( + [PSCustomObject]@{ Value = "$maliciousLinkText($maliciousURL)" }, # Group 0 (full match, approximate) + [PSCustomObject]@{ Value = $maliciousLinkText }, # Group 1 + [PSCustomObject]@{ Value = $maliciousURL } # Group 2 + ) } - } -ParameterFilter { $_.FullName -eq $maliciousNotesFile } + ) + } + } -ParameterFilter { $_.FullName -eq $maliciousNotesFile } -Verifiable - $outputCsv = Join-Path $tempDir "malicious_links.csv" - & $script:ScriptPath -Path $tempDir -OutputPath $outputCsv + $outputCsv = Join-Path $script:tempDir "malicious_links.csv" + & $script:ScriptPath -Path $script:tempDir -OutputPath $outputCsv Test-Path $outputCsv | Should -Be $true $importedCsv = Import-Csv -Path $outputCsv @@ -149,17 +148,15 @@ Describe "Get-TheBrainNotesLinks.ps1" { } Context "Without -Path parameter" { - It "should call Get-TheBrainDataDirectory.ps1 to get the default path" { - # Mock the dependency script - Mock Invoke-SqliteQuery { return [PSCustomObject]@{ Value = """$tempDir""" } } -Verifiable - - & $script:ScriptPath | Out-Null - Should -Invoke Invoke-SqliteQuery -Times 1 -Exactly + It "should accept -DataDirectory as an alternative to -Path" -Skip:$script:SkipAll { + $results = & $script:ScriptPath -DataDirectory $script:tempDir + $results | Should -Not -BeNullOrEmpty + $results[0].LinkText | Should -Be "valid link" } } Context "Error Handling" { - It "should throw an error for an invalid path" { + It "should throw an error for an invalid path" -Skip:$script:SkipAll { $invalidPath = "Z:\Invalid\Path\That\Does\Not\Exist" { & $script:ScriptPath -Path $invalidPath } | Should -Throw } diff --git a/Tests/theBrain/Open-TheBrainNodeFolder.Tests.ps1 b/Tests/theBrain/Open-TheBrainNodeFolder.Tests.ps1 index ce2b30e..9765710 100644 --- a/Tests/theBrain/Open-TheBrainNodeFolder.Tests.ps1 +++ b/Tests/theBrain/Open-TheBrainNodeFolder.Tests.ps1 @@ -10,10 +10,9 @@ # Version: 1.0.0 # -BeforeAll { - # Resolve full paths to the scripts at the start - $script:ScriptPath = Resolve-Path -Path "$PSScriptRoot/../../theBrain/Open-TheBrainNodeFolder.ps1" - $script:GetDataDirectoryScriptPath = Resolve-Path -Path "$PSScriptRoot/../../theBrain/Get-TheBrainDataDirectory.ps1" + BeforeAll { + # Resolve full paths to the scripts at the start + $script:ScriptPath = Resolve-Path -Path "$PSScriptRoot/../../theBrain/Open-TheBrainNodeFolder.ps1" # Create a temporary directory to simulate TheBrain's data folder structure $script:TempDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "Test-OpenTheBrainNode") @@ -38,12 +37,6 @@ AfterAll { Describe "Open-TheBrainNodeFolder.ps1" { BeforeEach { - # Mock the Get-TheBrainDataDirectory.ps1 script to return our temp path. - # This is the correct Pester v5 syntax for mocking a script that is dot-sourced. - Mock $script:GetDataDirectoryScriptPath { - return $script:fakeBrainDataDir - } -Verifiable - # Mock explorer.exe to prevent it from actually opening a window Mock explorer.exe { # This space intentionally left blank @@ -62,7 +55,7 @@ Describe "Open-TheBrainNodeFolder.ps1" { Mock Get-ChildItem { return @(Get-Item $script:fakeNodeFolderPath) } -Verifiable # Run the script with the NodeId of the folder we created - . $script:ScriptPath -NodeId $script:fakeNodeId + . $script:ScriptPath -NodeId $script:fakeNodeId -DataDirectory $script:fakeBrainDataDir # Verify that explorer.exe was called exactly once with the correct full path Should -Invoke "explorer.exe" -Times 1 -Exactly -ParameterFilter { $script:fakeNodeFolderPath } @@ -76,7 +69,7 @@ Describe "Open-TheBrainNodeFolder.ps1" { $nonExistentNodeId = "xyz-789-uvw-101" # Run the script with a NodeId that does not correspond to any folder - . $script:ScriptPath -NodeId $nonExistentNodeId + . $script:ScriptPath -NodeId $nonExistentNodeId -DataDirectory $script:fakeBrainDataDir # Verify that Write-Warning was called exactly once Should -Invoke "Write-Warning" -Exactly 1 @@ -89,19 +82,15 @@ Describe "Open-TheBrainNodeFolder.ps1" { } Context "when an error occurs" { - It "should call Write-Error when Get-TheBrainDataDirectory fails" { - # Mock Get-Module to simulate that PSSQLite is not found, causing Get-TheBrainDataDirectory to fail - Mock Get-Module { - throw "Failed to find TheBrain data directory" - } -ParameterFilter { - $Name -eq "PSSQLite" - } -Verifiable + It "should call Write-Error when Get-ChildItem fails" { + # Mock Get-ChildItem to throw an exception to simulate an error + Mock Get-ChildItem { throw "Simulated Get-ChildItem error" } -Verifiable # Mock Write-Error to verify it's called Mock Write-Error # Run the script. The script's try/catch should handle the error and not throw. - { . $script:ScriptPath -NodeId $script:fakeNodeId } | Should -Not -Throw + { . $script:ScriptPath -NodeId $script:fakeNodeId -DataDirectory $script:fakeBrainDataDir } | Should -Not -Throw # Verify that Write-Error was called because the catch block should execute Should -Invoke "Write-Error" -Times 1 -Exactly @@ -114,7 +103,7 @@ Describe "Open-TheBrainNodeFolder.ps1" { $invalidNodeId = "node-id-with-@!#" # Expect a ParameterBindingValidationException because the input does not match the pattern - { . $script:ScriptPath -NodeId $invalidNodeId } | Should -Throw -ExceptionType ([System.Management.Automation.ParameterBindingException]) + { . $script:ScriptPath -NodeId $invalidNodeId -DataDirectory $script:fakeBrainDataDir } | Should -Throw -ExceptionType ([System.Management.Automation.ParameterBindingException]) } } } diff --git a/Tests/theBrain/Resize-TheBrainNotesYouTubeThumbnail.Tests.ps1 b/Tests/theBrain/Resize-TheBrainNotesYouTubeThumbnail.Tests.ps1 index 16f345a..120a8b6 100644 --- a/Tests/theBrain/Resize-TheBrainNotesYouTubeThumbnail.Tests.ps1 +++ b/Tests/theBrain/Resize-TheBrainNotesYouTubeThumbnail.Tests.ps1 @@ -7,7 +7,6 @@ Describe 'Resize-TheBrainNotesYouTubeThumbnail.ps1' { BeforeAll { # Path to the script being tested, resolved relative to this test script's location $script:ScriptPath = Resolve-Path -Path "$PSScriptRoot\..\..\theBrain\Resize-TheBrainNotesYouTubeThumbnail.ps1" - $script:GetDataDirectoryScriptPath = Resolve-Path -Path "$PSScriptRoot\..\..\theBrain\Get-TheBrainDataDirectory.ps1" # Set up a temporary directory to simulate TheBrain's data folder $script:TestDrive = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "Test-ResizeTheBrainNotesYouTubeThumbnail") -Force @@ -17,8 +16,6 @@ Describe 'Resize-TheBrainNotesYouTubeThumbnail.ps1' { $script:TestNotesFile = Join-Path $script:TestThoughtDir 'Notes.md' New-Item -Path $script:TestThoughtDir -ItemType Directory -Force | Out-Null - # Mock the external script dependency to return a temporary path - Mock Invoke-SqliteQuery { return [PSCustomObject]@{ Value = """$script:TestDrive""" } } -Verifiable } AfterAll { @@ -43,7 +40,7 @@ Describe 'Resize-TheBrainNotesYouTubeThumbnail.ps1' { Set-Content -Path $script:TestNotesFile -Value $content -Encoding UTF8 # Act - . $script:ScriptPath + . $script:ScriptPath -DataDirectory $script:TestBrainDataDir # Assert $newContent = Get-Content -Path $script:TestNotesFile -Encoding UTF8 @@ -56,7 +53,7 @@ Describe 'Resize-TheBrainNotesYouTubeThumbnail.ps1' { Set-Content -Path $script:TestNotesFile -Value $content -Encoding UTF8 # Act - . $script:ScriptPath -NewWidth 75 + . $script:ScriptPath -NewWidth 75 -DataDirectory $script:TestBrainDataDir # Assert $newContent = Get-Content -Path $script:TestNotesFile -Encoding UTF8 @@ -69,7 +66,7 @@ Describe 'Resize-TheBrainNotesYouTubeThumbnail.ps1' { Set-Content -Path $script:TestNotesFile -Value $content -Encoding UTF8 # Act - . $script:ScriptPath + . $script:ScriptPath -DataDirectory $script:TestBrainDataDir # Assert $backupDirForThought = Join-Path $script:TestBackupDir 'TestThought' @@ -86,7 +83,7 @@ Describe 'Resize-TheBrainNotesYouTubeThumbnail.ps1' { Set-Content -Path $script:TestNotesFile -Value $content -Encoding UTF8 # Act - . $script:ScriptPath -ImageType resized -CurrentWidth 30 -NewWidth 80 + . $script:ScriptPath -ImageType resized -CurrentWidth 30 -NewWidth 80 -DataDirectory $script:TestBrainDataDir # Assert $newContent = Get-Content -Path $script:TestNotesFile -Encoding UTF8 @@ -99,7 +96,7 @@ Describe 'Resize-TheBrainNotesYouTubeThumbnail.ps1' { Set-Content -Path $script:TestNotesFile -Value $content -Encoding UTF8 # Act - . $script:ScriptPath -ImageType resized -CurrentWidth 30 -NewWidth 80 + . $script:ScriptPath -ImageType resized -CurrentWidth 30 -NewWidth 80 -DataDirectory $script:TestBrainDataDir # Assert $newContent = Get-Content -Path $script:TestNotesFile -Encoding UTF8 @@ -116,7 +113,7 @@ Describe 'Resize-TheBrainNotesYouTubeThumbnail.ps1' { Set-Content -Path $script:TestNotesFile -Value $content -Encoding UTF8 # Act - . $script:ScriptPath + . $script:ScriptPath -DataDirectory $script:TestBrainDataDir # Assert (Get-Content -Path $script:TestNotesFile -Encoding UTF8) | Should -Be $content @@ -126,23 +123,21 @@ Describe 'Resize-TheBrainNotesYouTubeThumbnail.ps1' { } Context 'Error Handling' { - It 'should call Write-Error when Get-TheBrainDataDirectory.ps1 fails' { + It 'should call Write-Error when an error occurs' { # Arrange # This mock will cause the script's try/catch block to fail - Mock Get-Module { return $null } -ParameterFilter { $Name -eq 'PSSQLite' } -Verifiable + Mock Get-ChildItem { throw "Simulated error" } -Verifiable # Mock Write-Error to verify the catch block is executed Mock Write-Error -Verifiable # Act # The script should not throw an unhandled exception because it has a catch block - { . $script:ScriptPath } | Should -Not -Throw + { . $script:ScriptPath -DataDirectory $script:TestBrainDataDir } | Should -Not -Throw # Assert # Verify that the script's catch block called Write-Error - Should -Invoke Write-Error -ParameterFilter { - $Message -eq "An error occurred while trying to get TheBrain data directory: The 'PSSQLite' module is required but not installed. Please run 'Install-Module -Name PSSQLite'." - } + Should -Invoke Write-Error -Times 1 -Exactly } } } diff --git a/VSCode/Export-VSCodeExtensionList.ps1 b/VSCode/Export-VSCodeExtensionList.ps1 index 2e9b185..257aae7 100644 --- a/VSCode/Export-VSCodeExtensionList.ps1 +++ b/VSCode/Export-VSCodeExtensionList.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Exports all VS Code profiles and their extensions to a text file. @@ -63,12 +63,12 @@ try { $json = Get-Content $StorageJson -Raw | ConvertFrom-Json $VSCodeProfiles = @("Default") + ($json.userDataProfiles | Select-Object -ExpandProperty name) - Write-Host "[✓] Found profiles: $($VSCodeProfiles -join ', ')`n" + Write-Host "[OK] Found profiles: $($VSCodeProfiles -join ', ')`n" # --- Build all content in memory, write once (no intermediate files) --- $lines = [System.Collections.Generic.List[string]]::new() - $lines.Add("VS Code Profile & Extension Export") + $lines.Add("VS Code Profile and Extension Export") $lines.Add("Generated: $(Get-Date)") $lines.Add("Machine: $env:COMPUTERNAME") $lines.Add("==================================================") @@ -106,7 +106,7 @@ try { } } $lines | Out-File -FilePath $OutputFile -Encoding UTF8 - Write-Host "[✓] Export complete: $OutputFile`n" + Write-Host "[OK] Export complete: $OutputFile`n" } else { Write-Host "[i] WhatIf: Output file would be written to $OutputFile`n" diff --git a/VSCode/README.md b/VSCode/README.md index 9c2a2e6..7d0083a 100644 --- a/VSCode/README.md +++ b/VSCode/README.md @@ -44,7 +44,7 @@ Exports all VS Code user profiles and their installed extensions to a timestampe The script generates a text file named `vscode-profiles-export-YYYY-MM-DD.txt` in your **My Documents** folder by default. You can override this with the `-OutputDirectory` parameter. ```plaintext -VS Code Profile & Extension Export +VS Code Profile and Extension Export Generated: 2026-06-01 Machine: my-machine ================================================== diff --git a/theBrain/Format-TheBrainNotesYouTubeThumbnail.ps1 b/theBrain/Format-TheBrainNotesYouTubeThumbnail.ps1 index c2319bb..0695a5b 100644 --- a/theBrain/Format-TheBrainNotesYouTubeThumbnail.ps1 +++ b/theBrain/Format-TheBrainNotesYouTubeThumbnail.ps1 @@ -26,12 +26,18 @@ #Requires -Version 2.0 [CmdletBinding()] +param( + [string]$DataDirectory +) $ErrorActionPreference = "Stop" try { - # Look up the Notes.md files that locate under the Brain data folder and contain the YouTube thumbnail URLs. - $BrainFolder = . "$PSScriptRoot\Get-TheBrainDataDirectory.ps1" + if ($DataDirectory) { + $BrainFolder = $DataDirectory + } else { + $BrainFolder = . "$PSScriptRoot\Get-TheBrainDataDirectory.ps1" + } $SubFolders = Get-ChildItem -Directory -Path $BrainFolder -Exclude 'Backup' $BackupFolder = Join-Path $BrainFolder 'Backup' diff --git a/theBrain/Get-TheBrainDataDirectory.ps1 b/theBrain/Get-TheBrainDataDirectory.ps1 index 1e9d90c..05c13cb 100644 --- a/theBrain/Get-TheBrainDataDirectory.ps1 +++ b/theBrain/Get-TheBrainDataDirectory.ps1 @@ -23,29 +23,37 @@ #> #Requires -Version 2.0 -#Requires -Modules PSSQLite + +param( + [string]$DataDirectory +) $ErrorActionPreference = "Stop" try { - # Check if PSSQLite module is available - if (-not (Get-Module -ListAvailable -Name PSSQLite)) { - throw "The 'PSSQLite' module is required but not installed. Please run 'Install-Module -Name PSSQLite'." - } + if ($DataDirectory) { + # Use the provided directory directly (skip PSSQLite check and DB query) + $brainDataDirectory = $DataDirectory + } else { + # Require PSSQLite only when actually querying the database + if (-not (Get-Module -ListAvailable -Name PSSQLite)) { + throw "The 'PSSQLite' module is required but not installed. Please run 'Install-Module -Name PSSQLite'." + } - $theBrainMetaDatabase = Join-Path $env:LOCALAPPDATA "TheBrain\MetaDB\TheBrainMeta.db" - if (-not (Test-Path -Path $theBrainMetaDatabase)) { - throw "TheBrain metadata database was not found at '$theBrainMetaDatabase'." - } + $theBrainMetaDatabase = Join-Path $env:LOCALAPPDATA "TheBrain\MetaDB\TheBrainMeta.db" + if (-not (Test-Path -Path $theBrainMetaDatabase)) { + throw "TheBrain metadata database was not found at '$theBrainMetaDatabase'." + } - $query = "SELECT Value FROM MetaSettings WHERE Name='preferences.userbraindatadirectory'" + $query = "SELECT Value FROM MetaSettings WHERE Name='preferences.userbraindatadirectory'" - $result = Invoke-SqliteQuery -DataSource $theBrainMetaDatabase -Query $query - $brainDataDirectory = $result.Value.Trim('"').Replace('\\', '\') + $result = Invoke-SqliteQuery -DataSource $theBrainMetaDatabase -Query $query + $brainDataDirectory = $result.Value.Trim('"').Replace('\\', '\') - if ($null -eq $brainDataDirectory -or $brainDataDirectory -eq '') { - # Default to 'Brains' folder in 'My Documents' if no value is found - $brainDataDirectory = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath 'Brains' + if ($null -eq $brainDataDirectory -or $brainDataDirectory -eq '') { + # Default to 'Brains' folder in 'My Documents' if no value is found + $brainDataDirectory = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath 'Brains' + } } # Check if the folder exists diff --git a/theBrain/Get-TheBrainNotesLinks.ps1 b/theBrain/Get-TheBrainNotesLinks.ps1 index 71b47b0..c8821a5 100644 --- a/theBrain/Get-TheBrainNotesLinks.ps1 +++ b/theBrain/Get-TheBrainNotesLinks.ps1 @@ -36,7 +36,8 @@ param( [string]$Path, - [string]$OutputPath + [string]$OutputPath, + [string]$DataDirectory ) $ErrorActionPreference = "Stop" @@ -44,7 +45,11 @@ $ErrorActionPreference = "Stop" try { # If no path is specified, get the default TheBrain data directory if (-not $Path) { - $Path = . "$PSScriptRoot\Get-TheBrainDataDirectory.ps1" + if ($DataDirectory) { + $Path = $DataDirectory + } else { + $Path = . "$PSScriptRoot\Get-TheBrainDataDirectory.ps1" + } } $PathToSearch = Get-ChildItem -Directory -Path $Path -Exclude 'Backup' diff --git a/theBrain/Open-TheBrainNodeFolder.ps1 b/theBrain/Open-TheBrainNodeFolder.ps1 index 6820bf7..2dd8e99 100644 --- a/theBrain/Open-TheBrainNodeFolder.ps1 +++ b/theBrain/Open-TheBrainNodeFolder.ps1 @@ -29,13 +29,19 @@ param( [Parameter(Mandatory = $true)] [ValidatePattern('^[a-zA-Z0-9-]+$')] - [string]$NodeId + [string]$NodeId, + + [string]$DataDirectory ) $ErrorActionPreference = "Stop" try { - $BrainFolder = . "$PSScriptRoot\Get-TheBrainDataDirectory.ps1" + if ($DataDirectory) { + $BrainFolder = $DataDirectory + } else { + $BrainFolder = . "$PSScriptRoot\Get-TheBrainDataDirectory.ps1" + } $PathToSearch = Get-ChildItem -Directory -Path $BrainFolder -Exclude 'Backup' $Folder = Get-ChildItem -Path $PathToSearch -Directory -Filter $NodeId -Recurse diff --git a/theBrain/Resize-TheBrainNotesYouTubeThumbnail.ps1 b/theBrain/Resize-TheBrainNotesYouTubeThumbnail.ps1 index d04b2af..d09f040 100644 --- a/theBrain/Resize-TheBrainNotesYouTubeThumbnail.ps1 +++ b/theBrain/Resize-TheBrainNotesYouTubeThumbnail.ps1 @@ -55,13 +55,19 @@ param( [Parameter(Mandatory = $false)] [ValidateRange(1, 100)] - [int]$CurrentWidth = 30 + [int]$CurrentWidth = 30, + + [string]$DataDirectory ) $ErrorActionPreference = "Stop" try { - $BrainFolder = . "$PSScriptRoot\Get-TheBrainDataDirectory.ps1" + if ($DataDirectory) { + $BrainFolder = $DataDirectory + } else { + $BrainFolder = . "$PSScriptRoot\Get-TheBrainDataDirectory.ps1" + } $SubFolders = Get-ChildItem -Directory -Path $BrainFolder -Exclude 'Backup' $BackupFolder = Join-Path $BrainFolder 'Backup'