Files
LinkPort/scripts/start-dev.test.ps1
T

224 lines
16 KiB
PowerShell

# Isolated startup-script regression: never starts a service or reads project data.
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent
$fixture = Join-Path ([IO.Path]::GetTempPath()) ('linkport-start-test-' + [Guid]::NewGuid().ToString('N') + ' with spaces')
$callerDirectory = (Get-Location).Path
$previousPause = $env:LINKPORT_START_NO_PAUSE
$previousEnvironment = @{}
$heldProcesses = @()
foreach ($name in @('LINKPORT_DATA', 'LINKPORT_START_TEST_DATA_PATH', 'LINKPORT_START_TEST_RESOLVE_FAILURE')) {
$previousEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process')
}
function Assert-StartTest([bool]$Condition, [string]$Message) { if (!$Condition) { throw $Message } }
try {
. (Join-Path $PSScriptRoot 'version.ps1')
$beforeBuildTime = [DateTime]::UtcNow.AddSeconds(-1)
$developmentInfo = Get-linkportBuildInfo -Development
$releaseInfo = Get-linkportBuildInfo
Assert-StartTest ($developmentInfo.BuildTime -eq 'unknown') 'Development timestamp invalidates the Go executable cache'
Assert-StartTest ($developmentInfo.Version -eq $releaseInfo.Version -and $developmentInfo.Commit -eq $releaseInfo.Commit) 'Development build lost version or commit metadata'
$releaseTime = [DateTimeOffset]::Parse($releaseInfo.BuildTime).UtcDateTime
Assert-StartTest ($releaseTime -ge $beforeBuildTime -and $releaseTime -le [DateTime]::UtcNow) 'Release build lost its actual build timestamp'
New-Item -ItemType Directory -Path (Join-Path $fixture 'scripts'), (Join-Path $fixture 'data') -Force | Out-Null
Copy-Item -LiteralPath (Join-Path $repo 'start-dev.cmd') -Destination $fixture
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'start-dev.ps1') -Destination (Join-Path $fixture 'scripts')
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'build-lock.ps1') -Destination (Join-Path $fixture 'scripts')
. (Join-Path $fixture 'scripts/build-lock.ps1')
$firstLock = Enter-LinkPortBuildLock
try {
$rejected = $false
try { $secondLock = Enter-LinkPortBuildLock; $secondLock.Dispose() } catch { $rejected = $true }
Assert-StartTest $rejected 'Concurrent build was not rejected'
} finally { $firstLock.Dispose() }
$nextLock = Enter-LinkPortBuildLock
$nextLock.Dispose()
[IO.File]::WriteAllText((Join-Path $fixture 'data/sentinel'), 'fixture data must stay unchanged')
$fakeBuild = @'
param([switch]$SkipTests, [switch]$RebuildWeb, [string]$OutputPath, [switch]$Development)
$root = Split-Path $PSScriptRoot -Parent
@{skipTests=[bool]$SkipTests;rebuildWeb=[bool]$RebuildWeb;development=[bool]$Development;output=$OutputPath;workingDirectory=(Get-Location).Path} | ConvertTo-Json | Set-Content (Join-Path $root 'arguments.json')
if (Test-Path (Join-Path $root 'fail-build')) { exit 9 }
New-Item -ItemType Directory -Force (Split-Path $OutputPath -Parent) | Out-Null
if (Test-Path -LiteralPath (Join-Path $root 'fixture.exe')) {
Copy-Item -LiteralPath (Join-Path $root 'fixture.exe') -Destination $OutputPath -Force
} else {
[IO.File]::WriteAllText($OutputPath, 'fixture executable placeholder')
}
exit 0
'@
[IO.File]::WriteAllText((Join-Path $fixture 'scripts/build.ps1'), $fakeBuild)
$env:LINKPORT_START_NO_PAUSE = '1'
$launcher = Join-Path $fixture 'start-dev.cmd'
& $launcher -BuildOnly
Assert-StartTest ($LASTEXITCODE -eq 0) 'Default build-only failed'
$arguments = Get-Content -Raw (Join-Path $fixture 'arguments.json') | ConvertFrom-Json
Assert-StartTest ($arguments.skipTests -and !$arguments.rebuildWeb -and $arguments.development) 'Fast flags not forwarded'
Assert-StartTest ($arguments.workingDirectory -eq $fixture) 'CMD did not use the entry directory'
Assert-StartTest ((Get-Location).Path -eq $callerDirectory) 'CMD changed the caller directory'
Assert-StartTest ($arguments.output -eq (Join-Path $fixture 'bin/dev-build/linkport.exe')) 'Build output not isolated'
Assert-StartTest (!(Test-Path (Join-Path $fixture 'bin/linkport.exe'))) 'Build-only installed a binary'
Assert-StartTest (!(Test-Path (Join-Path $fixture 'bin/backups'))) 'Build-only touched backups'
& $launcher -BuildOnly -FullCheck -RebuildWeb
Assert-StartTest ($LASTEXITCODE -eq 0) 'Full build-only failed'
$arguments = Get-Content -Raw (Join-Path $fixture 'arguments.json') | ConvertFrom-Json
Assert-StartTest (!$arguments.skipTests -and $arguments.rebuildWeb -and $arguments.development) 'Full-check/force flags not forwarded'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $fixture 'scripts/start-dev.ps1') -BuildOnly
Assert-StartTest ($LASTEXITCODE -eq 0) 'Direct PowerShell build-only failed'
$arguments = Get-Content -Raw (Join-Path $fixture 'arguments.json') | ConvertFrom-Json
Assert-StartTest ($arguments.workingDirectory -eq $fixture) 'PowerShell did not use the entry directory'
Assert-StartTest ((Get-Location).Path -eq $callerDirectory) 'PowerShell changed the caller directory'
[IO.File]::WriteAllText((Join-Path $fixture 'fail-build'), 'simulate failure with old staged executable still present')
$ErrorActionPreference = 'Continue'
& $launcher -BuildOnly 2>$null | Out-Null
$failedExit = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
Assert-StartTest ($failedExit -ne 0) 'Failed build reused old staged executable'
Assert-StartTest ((Get-Location).Path -eq $callerDirectory) 'Failed launch changed the caller directory'
Assert-StartTest (!(Test-Path (Join-Path $fixture 'bin/backups'))) 'Failed build touched backups'
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $fixture 'data/sentinel'))) -eq 'fixture data must stay unchanged') 'Fixture data changed'
Remove-Item -LiteralPath (Join-Path $fixture 'fail-build')
$fakeProgram = @'
using System;
using System.IO;
public static class LinkPortStartFixture {
public static int Main(string[] arguments) {
if (arguments.Length == 1 && arguments[0] == "hold") {
File.WriteAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ready.txt"), "ready");
System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
return 0;
}
if (arguments.Length == 1 && arguments[0] == "data-path") {
if (Environment.GetEnvironmentVariable("LINKPORT_START_TEST_RESOLVE_FAILURE") == "1") return 9;
Console.WriteLine(Environment.GetEnvironmentVariable("LINKPORT_START_TEST_DATA_PATH"));
return 0;
}
File.WriteAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "launched-data.txt"), Environment.GetEnvironmentVariable("LINKPORT_DATA") ?? "<unset>");
return 0;
}
}
'@
Add-Type -TypeDefinition $fakeProgram -OutputAssembly (Join-Path $fixture 'fixture.exe') -OutputType ConsoleApplication
$legacyDirectory = Join-Path $fixture 'legacy data'
New-Item -ItemType Directory -Path (Join-Path $legacyDirectory 'audit'), (Join-Path $legacyDirectory 'logs'), (Join-Path $legacyDirectory 'tls') -Force | Out-Null
$backupFiles = @('linkport.sqlite', 'linkport.sqlite-wal', 'linkport.sqlite-shm', 'linkport.sqlite-journal', 'master-key', 'master-key.dpapi', 'tls\key.pem', 'settings.json')
$excludedFiles = @('audit\day.sqlite', 'audit\day.sqlite-wal', 'linkport.log', 'linkport.log.1', 'logs\runtime.txt')
foreach ($relativePath in ($backupFiles + $excludedFiles)) {
[IO.File]::WriteAllText((Join-Path $legacyDirectory $relativePath), "fixture $relativePath")
}
$env:LINKPORT_DATA = ''
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $legacyDirectory 'linkport.sqlite'
$foreignDirectory = Join-Path $fixture 'unowned'
New-Item -ItemType Directory -Path $foreignDirectory, (Join-Path $fixture 'bin') -Force | Out-Null
$heldPaths = @((Join-Path $fixture 'linkport.exe'), (Join-Path $fixture 'bin/linkport.exe'), (Join-Path $foreignDirectory 'linkport.exe'))
foreach ($heldPath in $heldPaths) {
Copy-Item -LiteralPath (Join-Path $fixture 'fixture.exe') -Destination $heldPath
$heldProcesses += Start-Process -FilePath $heldPath -ArgumentList 'hold' -WindowStyle Hidden -PassThru
$readyPath = Join-Path (Split-Path $heldPath -Parent) 'ready.txt'
$readyDeadline = [DateTime]::UtcNow.AddSeconds(10)
while (!(Test-Path -LiteralPath $readyPath) -and [DateTime]::UtcNow -lt $readyDeadline) { Start-Sleep -Milliseconds 50 }
Assert-StartTest (Test-Path -LiteralPath $readyPath) 'Fixture process did not start'
}
& $launcher
Assert-StartTest ($LASTEXITCODE -eq 0) 'Source launch failed'
Assert-StartTest ($heldProcesses[0].WaitForExit(1000) -and $heldProcesses[1].WaitForExit(1000)) 'Launcher did not stop both owned executable paths'
Assert-StartTest (!$heldProcesses[2].HasExited) 'Launcher stopped an unowned linkport executable'
$launchRecord = Join-Path $fixture 'bin/launched-data.txt'
Assert-StartTest (([IO.File]::ReadAllText($launchRecord)) -eq '<unset>') 'Launcher forced a database path'
$backups = @(Get-ChildItem -LiteralPath (Join-Path $fixture 'bin/backups') -Directory)
Assert-StartTest ($backups.Count -eq 1) 'Source launch did not create exactly one backup'
foreach ($relativePath in $backupFiles) {
$backupFile = Join-Path $backups[0].FullName (Join-Path 'legacy data' $relativePath)
Assert-StartTest (([IO.File]::ReadAllText($backupFile)) -eq "fixture $relativePath") 'Selected non-log data was not backed up'
}
foreach ($relativePath in $excludedFiles) {
Assert-StartTest (!(Test-Path -LiteralPath (Join-Path $backups[0].FullName (Join-Path 'legacy data' $relativePath)))) 'Log data was included in the startup backup'
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $legacyDirectory $relativePath))) -eq "fixture $relativePath") 'Original log data changed'
}
foreach ($excludedDirectory in @('audit', 'logs')) {
Assert-StartTest (!(Test-Path -LiteralPath (Join-Path $backups[0].FullName (Join-Path 'legacy data' $excludedDirectory)))) 'Excluded log directory was traversed or copied'
}
Assert-StartTest (!(Test-Path -LiteralPath (Join-Path $backups[0].FullName 'data'))) 'Launcher backed up the unselected local directory'
$env:LINKPORT_DATA = '.\custom data\selected.sqlite'
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $fixture 'custom data/selected.sqlite'
New-Item -ItemType Directory -Path (Split-Path $env:LINKPORT_START_TEST_DATA_PATH -Parent) -Force | Out-Null
[IO.File]::WriteAllText($env:LINKPORT_START_TEST_DATA_PATH, 'explicit database fixture')
& $launcher
Assert-StartTest ($LASTEXITCODE -eq 0) 'Explicit-path launch failed'
Assert-StartTest (([IO.File]::ReadAllText($launchRecord)) -eq $env:LINKPORT_DATA) 'Launcher replaced the explicit database path'
$backups = @(Get-ChildItem -LiteralPath (Join-Path $fixture 'bin/backups') -Directory)
Assert-StartTest ($backups.Count -eq 2) 'Explicit-path launch did not create a backup'
$env:LINKPORT_START_TEST_RESOLVE_FAILURE = '1'
$ErrorActionPreference = 'Continue'
& $launcher 2>$null | Out-Null
$failedExit = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
Assert-StartTest ($failedExit -ne 0) 'Database resolution failure was ignored'
$env:LINKPORT_START_TEST_RESOLVE_FAILURE = ''
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $fixture 'linkport.sqlite'
$ErrorActionPreference = 'Continue'
& $launcher 2>$null | Out-Null
$failedExit = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
Assert-StartTest ($failedExit -ne 0) 'Recursive backup destination was accepted'
$backups = @(Get-ChildItem -LiteralPath (Join-Path $fixture 'bin/backups') -Directory)
Assert-StartTest ($backups.Count -eq 2) 'Rejected launch created a backup'
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $legacyDirectory 'linkport.sqlite'
$hashProvider = [Security.Cryptography.SHA256]::Create()
try {
$lockHash = ([BitConverter]::ToString($hashProvider.ComputeHash([Text.Encoding]::UTF8.GetBytes(([IO.Path]::GetFullPath($env:LINKPORT_START_TEST_DATA_PATH)).ToLowerInvariant())))).Replace('-', '').ToLowerInvariant()
} finally { $hashProvider.Dispose() }
$busyLock = New-Object Threading.Mutex($false, "Local\linkport-$lockHash")
try {
$ErrorActionPreference = 'Continue'
& $launcher 2>$null | Out-Null
$failedExit = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
Assert-StartTest ($failedExit -ne 0) 'Launcher backed up data held by another local instance'
$backups = @(Get-ChildItem -LiteralPath (Join-Path $fixture 'bin/backups') -Directory)
Assert-StartTest ($backups.Count -eq 2) 'Busy data directory created a backup'
} finally {
$busyLock.Dispose()
}
$releaseDirectory = Join-Path $fixture 'release with spaces'
New-Item -ItemType Directory -Path (Join-Path $releaseDirectory 'scripts') -Force | Out-Null
Copy-Item -LiteralPath (Join-Path $repo 'start-dev.cmd') -Destination $releaseDirectory
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'start-dev.ps1') -Destination (Join-Path $releaseDirectory 'scripts')
Copy-Item -LiteralPath (Join-Path $fixture 'fixture.exe') -Destination (Join-Path $releaseDirectory 'linkport.exe')
$env:LINKPORT_DATA = ''
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $legacyDirectory 'linkport.sqlite'
& (Join-Path $releaseDirectory 'start-dev.cmd')
Assert-StartTest ($LASTEXITCODE -eq 0) 'Release launch failed'
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $releaseDirectory 'launched-data.txt'))) -eq '<unset>') 'Release launcher forced a database path'
$releaseBackups = @(Get-ChildItem -LiteralPath (Join-Path $releaseDirectory 'bin/backups') -Directory)
Assert-StartTest ($releaseBackups.Count -eq 1) 'Release launch did not back up the selected directory'
foreach ($relativePath in $backupFiles) {
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $releaseBackups[0].FullName (Join-Path 'legacy data' $relativePath)))) -eq "fixture $relativePath") 'Release startup did not preserve non-log backup data'
}
foreach ($relativePath in $excludedFiles) {
Assert-StartTest (!(Test-Path -LiteralPath (Join-Path $releaseBackups[0].FullName (Join-Path 'legacy data' $relativePath)))) 'Release startup backed up log data'
}
Assert-StartTest ((Get-Location).Path -eq $callerDirectory) 'Launch changed the caller directory'
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $fixture 'data/sentinel'))) -eq 'fixture data must stay unchanged') 'Unselected local data changed'
Write-Host 'Startup script tests passed: relative entry paths, caller-directory preservation, build locking, build flags/isolation, owned-process stopping and unowned-process preservation, unchanged data override, selected-directory backups excluding logs while preserving source logs, source/release startup and resolution/recursive-backup/busy-data failure protection.'
} finally {
foreach ($heldProcess in $heldProcesses) {
try {
if (!$heldProcess.HasExited) {
$heldPath = [IO.Path]::GetFullPath($heldProcess.Path)
$fixturePrefix = [IO.Path]::GetFullPath($fixture).TrimEnd('\') + '\'
if (!$heldPath.StartsWith($fixturePrefix, [StringComparison]::OrdinalIgnoreCase)) { throw 'Unexpected fixture process path' }
$heldProcess.Kill()
if (!$heldProcess.WaitForExit(10000)) { throw 'Fixture process did not exit' }
}
} finally { $heldProcess.Dispose() }
}
$env:LINKPORT_START_NO_PAUSE = $previousPause
foreach ($name in $previousEnvironment.Keys) {
[Environment]::SetEnvironmentVariable($name, $previousEnvironment[$name], 'Process')
}
$resolved = [IO.Path]::GetFullPath($fixture)
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
if (!$resolved.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -or (Split-Path $resolved -Leaf) -notlike 'linkport-start-test-*') { throw 'Unexpected test cleanup path' }
if (Test-Path -LiteralPath $resolved) { Remove-Item -LiteralPath $resolved -Recurse -Force }
}