_TESTS/PSLocalBase.WalMultiProcess.Tests.ps1

# Independent processes write to one WAL database file at the same time.
# Uses Windows PowerShell 5.1 and PowerShell 7 together when both are installed.
$ErrorActionPreference = 'Stop'
$modulePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'PSLocalBase.psd1'
Import-Module $modulePath -Force

$iterations = 40
$workDir = Join-Path ([System.IO.Path]::GetTempPath()) ("pslb-wal-" + [Guid]::NewGuid().ToString('N'))
[void][System.IO.Directory]::CreateDirectory($workDir)
$dbPath = Join-Path $workDir 'shared.db'
$goFile = Join-Path $workDir 'go'
$workerScript = Join-Path $workDir 'worker.ps1'

Set-Content -Path $workerScript -Encoding UTF8 -Value @'
param($ModulePath, $DbPath, $WorkerId, $Iterations, $GoFile)
$ErrorActionPreference = 'Stop'
Import-Module $ModulePath -Force
$db = New-LocalBase -Path $DbPath -Options @{ busyTimeout = 30000 }
try {
    $deadline = [DateTime]::UtcNow.AddSeconds(60)
    while (-not (Test-Path $GoFile)) {
        if ([DateTime]::UtcNow -gt $deadline) { throw 'Start signal timeout.' }
        Start-Sleep -Milliseconds 10
    }
    for ($i = 0; $i -lt $Iterations; $i++) {
        [void]$db.collection('events').add(@{ worker = $WorkerId; i = $i })
        $db.collection('shared').doc('members').update(@{ list = @{ '$arrayUnion' = @("$WorkerId/$i") } })
        # Read-modify-write inside an explicit transaction: lost updates would show as a short counter.
        $db.beginTransaction()
        try {
            $current = $db.collection('shared').doc('counter').get()
            $db.collection('shared').doc('counter').set(@{ n = ([int]$current.n + 1) })
            $db.commit()
        } catch {
            try { $db.rollback() } catch { }
            throw
        }
    }
} finally { $db.close() }
'@


$hosts = [System.Collections.Generic.List[string]]::new()
foreach ($candidate in @('pwsh.exe', 'powershell.exe')) {
    $cmd = Get-Command $candidate -ErrorAction SilentlyContinue
    if ($cmd) { $hosts.Add($cmd.Source) }
}
if ($hosts.Count -eq 1) { $hosts.Add($hosts[0]) }

$failed = $false
$processes = @()
try {
    $setup = New-LocalBase -Path $dbPath
    try {
        if ($setup.JournalMode -ne 'wal') { throw "Expected WAL journal mode, got $($setup.JournalMode)" }
        [void]$setup.collection('events').create()
        $setup.collection('shared').doc('counter').set(@{ n = 0 })
        $setup.collection('shared').doc('members').set(@{ list = @() })
    } finally { $setup.close() }

    for ($w = 0; $w -lt $hosts.Count; $w++) {
        $info = [System.Diagnostics.ProcessStartInfo]::new($hosts[$w])
        $info.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$workerScript`" -ModulePath `"$modulePath`" -DbPath `"$dbPath`" -WorkerId w$w -Iterations $iterations -GoFile `"$goFile`""
        $info.UseShellExecute = $false
        $info.RedirectStandardError = $true
        $info.RedirectStandardOutput = $true
        $info.CreateNoWindow = $true
        $process = [System.Diagnostics.Process]::Start($info)
        $processes += [pscustomobject]@{ Host = (Split-Path $hosts[$w] -Leaf); Process = $process; Stdout = $process.StandardOutput.ReadToEndAsync(); Stderr = $process.StandardError.ReadToEndAsync() }
    }
    Start-Sleep -Seconds 3
    Set-Content -Path $goFile -Value 'go'

    foreach ($p in $processes) {
        if (-not $p.Process.WaitForExit(240000)) { $p.Process.Kill(); throw "Worker $($p.Host) timed out." }
        $p.Process.WaitForExit()
        $stderr = $p.Stderr.Result
        if ($p.Process.ExitCode -ne 0 -or -not [string]::IsNullOrWhiteSpace($stderr)) {
            throw "Worker $($p.Host) failed (exit $($p.Process.ExitCode)): $stderr $($p.Stdout.Result)"
        }
        Write-Host "[OBSERVATION] worker $($p.Host) completed $iterations iterations."
    }

    $verify = New-LocalBase -Path $dbPath
    try {
        $expected = $iterations * $hosts.Count
        $events = $verify.collection('events').count()
        $members = @($verify.collection('shared').doc('members').get().list)
        $counter = [int]$verify.collection('shared').doc('counter').get().n
        $integrity = $verify.query('PRAGMA integrity_check;')[0].integrity_check
        if ($events -ne $expected) { throw "Lost add() writes: $events of $expected" }
        if ($members.Count -ne $expected -or @($members | Sort-Object -Unique).Count -ne $expected) { throw "Lost array mutations: $($members.Count) of $expected" }
        if ($counter -ne $expected) { throw "Lost transactional increments: $counter of $expected" }
        if ($integrity -ne 'ok') { throw "integrity_check returned $integrity" }
        Write-Host "[PASS] W01 - $($hosts.Count) processes ($((($processes | ForEach-Object Host) -join ', '))) wrote $expected adds, array unions and transactional increments to one WAL file without loss or lock errors."
    } finally { $verify.close() }
} catch {
    $failed = $true
    Write-Host "[FAIL] W01 - $($_.Exception.Message)"
} finally {
    foreach ($p in $processes) { if (-not $p.Process.HasExited) { $p.Process.Kill() }; $p.Process.Dispose() }
    Remove-Item $workDir -Recurse -Force -ErrorAction SilentlyContinue
}

if ($failed) { exit 1 }
Write-Host "WAL multi-process: passed=1; failed=0"