doc/v1-tests/Gain.Reference.ps1

#requires -Version 7.4
#requires -PSEdition Core
[CmdletBinding()]
param([string]$OutputPath)
$ErrorActionPreference='Stop'
[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)
$moduleRoot=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$testRoot=Join-Path ([IO.Path]::GetTempPath()) ('quietcli-gain-tests-'+[guid]::NewGuid().ToString('N'))
[void][IO.Directory]::CreateDirectory($testRoot)
$oldPath=$env:QUIETCLI_DB_PATH; $oldTracking=$env:QUIETCLI_TRACKING
$env:QUIETCLI_DB_PATH=Join-Path $testRoot 'initial.sqlite3'; $env:QUIETCLI_TRACKING='1'
Import-Module (Join-Path $moduleRoot 'FrenchExDev.QuietCli.PoSh.psd1') -ErrorAction Stop
$engine=(Get-Process -Id $PID).Path
$checks=[Collections.Generic.List[object]]::new()
function Assert([bool]$Condition,[string]$Message='Assertion failed') { if (!$Condition) { throw $Message } }
function Check([string]$Name,[scriptblock]$Body) {
    $env:QUIETCLI_DB_PATH=Join-Path $testRoot ([guid]::NewGuid().ToString('N')+'.sqlite3')
    $env:QUIETCLI_TRACKING='1'
    try { & $Body; $checks.Add(@{Name=$Name;Passed=$true}) }
    catch { $checks.Add(@{Name=$Name;Passed=$false;Message=$_.Exception.Message;Line=$_.InvocationInfo.ScriptLineNumber}) }
}
function Invoke-Fixture {
    param([string]$Code="[Console]::Out.Write(('x'*12000))",[switch]$Streaming,[int]$Timeout=10,[scriptblock]$Parse)
    $codePrefix='[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false);'
    $arguments=@('-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',
        [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($codePrefix+$Code)))
    if ($Streaming) {
        Invoke-QuietCli -FilePath $engine -ArgumentList $arguments -TimeoutSeconds $Timeout -State @{Records=0} `
            -OnRecord {param($r,$s) $s.Records++} -Complete {param($s,$c) [pscustomobject]@{Records=$s.Records}}
    } else {
        if (!$Parse) { $Parse={param($c) [pscustomobject]@{Bytes=[Text.Encoding]::UTF8.GetByteCount($c.Stdout)}} }
        Invoke-QuietCli -FilePath $engine -ArgumentList $arguments -TimeoutSeconds $Timeout -Parse $Parse
    }
}
function Seed-Delivery($Database,[string]$Id,[DateTimeOffset]$Timestamp,[string]$Command,[long]$InputTokens,[long]$OutputTokens) {
    $Database.Execute(@'
INSERT INTO quietcli_deliveries
(id,timestamp_ms,command,outcome,native_calls,measured,analysis_complete,truncated,input_bytes,output_bytes,input_tokens,output_tokens,saved_tokens,elapsed_ms)
VALUES (?,?,?,'Success',1,1,1,0,?,?,?,?,?,1)
'@
,[object[]]@($Id,$Timestamp.ToUnixTimeMilliseconds(),$Command,(4*$InputTokens),(4*$OutputTokens),$InputTokens,$OutputTokens,($InputTokens-$OutputTokens)))
}

try {
    Check 'Six public functions; empty history is readable without creating a database' {
        Assert (@(Get-Command -Module FrenchExDev.QuietCli.PoSh).Count -eq 6)
        $gain=QuietCliGain -View Object
        Assert ($gain.Totals.Deliveries -eq 0 -and $null -eq $gain.Totals.SavingsPercent)
        Assert (!(Test-Path -LiteralPath $env:QUIETCLI_DB_PATH))
        Assert ((QuietCliGain -View Text) -match 'No recorded deliveries')
    }
    Check 'UTF-8 counts include both streams and match RTK ceiling, including split surrogate pairs' {
        $native=Invoke-Fixture -Code "[Console]::Out.Write(('é😀'*5000));[Console]::Error.Write('err')"
        Assert ($native.Outcome -eq 'Success')
        Assert ($native.Data.Bytes -eq 30000) 'Fixture did not emit the expected Unicode bytes.'
        Assert ($native.Execution.StdoutBytes -eq 30000 -and $native.Execution.StderrBytes -eq 3) 'Unicode byte counter differs from a complete UTF-8 encoding.'
        Assert ((QuietCliGain -View Object).Totals.Deliveries -eq 0) 'An intermediate object was counted.'
        $json=ConvertTo-QuietJson $native
        $gain=QuietCliGain -View Object
        Assert ($gain.Totals.InputBytes -eq 30003 -and $gain.Totals.InputTokens -eq 7501)
        $expected=[long][Math]::Ceiling([Text.Encoding]::UTF8.GetByteCount($json)/4.0)
        Assert ($gain.Totals.OutputTokens -eq $expected -and $gain.Totals.SavedTokens -eq (7501-$expected))
        Assert (!$gain.LastTrackingError)
        $header=[IO.File]::ReadAllBytes($env:QUIETCLI_DB_PATH)
        Assert ([Text.Encoding]::ASCII.GetString($header,0,15) -eq 'SQLite format 3')
    }
    Check 'Repeated serialization counts the same result once and queries do not count themselves' {
        $native=Invoke-Fixture
        $first=ConvertTo-QuietJson $native -MaxOutputBytes 1024
        $second=ConvertTo-QuietJson $native -MaxOutputBytes 2048
        $one=QuietCliGain -View Object
        $two=QuietCliGain -View Json | ConvertFrom-Json
        Assert ($one.Totals.Deliveries -eq 1 -and $two.Totals.Deliveries -eq 1)
        Assert ($one.Totals.OutputBytes -eq [Text.Encoding]::UTF8.GetByteCount($first))
    }
    Check 'Nested tasks and assigned steps account for two native calls in one final delivery' {
        $json=Invoke-QuietTask -AsJson -MaxOutputBytes 1024 {
            $first=Invoke-QuietStep first {Invoke-Fixture -Code "[Console]::Out.Write(('a'*12000))"}
            $nested=Invoke-QuietTask -AsJson {
                $second=Invoke-QuietStep second {Invoke-Fixture -Code "[Console]::Out.Write(('b'*8000))"}
                [pscustomobject]@{Count=2}
            }
            [pscustomobject]@{Answer=2}
        }
        $gain=QuietCliGain -View Object
        Assert (($json|ConvertFrom-Json).Outcome -eq 'Success')
        Assert ($gain.Totals.Deliveries -eq 1 -and $gain.Totals.NativeCalls -eq 2)
        Assert ($gain.Totals.InputBytes -eq 20000 -and $gain.Totals.OutputBytes -eq [Text.Encoding]::UTF8.GetByteCount($json))
    }
    Check 'Previously delivered sources are not credited again by a new task' {
        $native=Invoke-Fixture
        $first=ConvertTo-QuietJson $native
        $before=QuietCliGain -View Object
        $taskJson=Invoke-QuietTask -AsJson {$step=Invoke-QuietStep reused {$native};[pscustomobject]@{Answer=1}}
        $after=QuietCliGain -View Object
        Assert ($after.Totals.Deliveries -eq 2 -and $after.Totals.UnmeasuredDeliveries -eq 1)
        Assert ($after.Totals.SavedTokens -eq $before.Totals.SavedTokens)
    }
    Check 'Streaming counts delimiters and all records without retaining the log' {
        $native=Invoke-Fixture -Code 'for($i=0;$i -lt 3000;$i++){[Console]::Out.Write("é😀`n")}' -Streaming
        Assert ($native.Outcome -eq 'Success' -and $native.Data.Records -eq 3000)
        Assert ($native.Execution.StdoutBytes -eq 21000)
        $json=ConvertTo-QuietJson $native
        Assert ((QuietCliGain -View Object).Totals.InputBytes -eq 21000)
    }
    Check 'Expansion is negative in objects, terminal charts and HTML' {
        $native=Invoke-Fixture -Code "[Console]::Out.Write('x')"
        $json=ConvertTo-QuietJson $native
        $gain=QuietCliGain -View Object
        Assert ($gain.Totals.InputTokens -eq 1 -and $gain.Totals.SavedTokens -lt 0)
        Assert ((QuietCliGain -View Chart) -match '\[-+\|')
        $htmlPath=QuietCliGain -View Html -OutputPath (Join-Path $testRoot 'negative.html')
        $html=Get-Content -LiteralPath $htmlPath -Raw
        Assert ($html -match '<svg' -and $html -match '#fb923c' -and $html -notmatch '<script')
    }
    Check 'The counter uses the final bounded JSON including an omitted projection' {
        $native=Invoke-Fixture -Parse {param($c) [pscustomobject]@{Large=('z'*6000)}}
        $json=ConvertTo-QuietJson $native -MaxOutputBytes 1024
        $decoded=$json|ConvertFrom-Json
        $gain=QuietCliGain -View Object
        Assert ($null -eq $decoded.Data -and $decoded.Truncation.Applied)
        Assert ($gain.Totals.OutputBytes -eq [Text.Encoding]::UTF8.GetByteCount($json) -and $gain.Totals.TruncatedDeliveries -eq 1)
    }
    Check 'Start failure and timeout are retained as unmeasured, not invented savings' {
        $missing=Invoke-QuietCli -FilePath '__quietcli_missing_gain_fixture__' -Parse {param($c) @{Never=$true}}
        $missingJson=ConvertTo-QuietJson $missing
        $timeout=Invoke-Fixture -Code "[Console]::Out.Write('partial');Start-Sleep -Seconds 5" -Timeout 1
        $timeoutJson=ConvertTo-QuietJson $timeout
        $gain=QuietCliGain -View Object
        Assert ($timeout.Outcome -eq 'Timeout')
        Assert ($gain.Totals.Deliveries -eq 2 -and $gain.Totals.UnmeasuredDeliveries -eq 2)
        Assert ($gain.Totals.SavedTokens -eq 0 -and $gain.Totals.FailedDeliveries -eq 2)
    }
    Check 'Parse failures preserve measured volume and failure counters' {
        $native=Invoke-Fixture -Parse {param($c) throw 'Unsupported format'}
        $json=ConvertTo-QuietJson $native
        $gain=QuietCliGain -View Object
        Assert ($native.Outcome -eq 'Failed')
        Assert ($gain.Totals.InputBytes -eq 12000 -and $gain.Totals.FailedDeliveries -eq 1 -and $gain.Totals.AnalysisIncompleteDeliveries -eq 1)
    }
    Check 'Tracking opt-out and runtime database path changes are respected immediately' {
        $env:QUIETCLI_TRACKING='0'
        $native=Invoke-Fixture; $json=ConvertTo-QuietJson $native
        Assert (!(Test-Path -LiteralPath $env:QUIETCLI_DB_PATH))
        Assert (!(QuietCliGain -View Object).TrackingEnabled)
        $env:QUIETCLI_TRACKING='1'
        $json=ConvertTo-QuietJson $native
        Assert ((QuietCliGain -View Object).Totals.Deliveries -eq 1)
        $env:QUIETCLI_DB_PATH=Join-Path $testRoot 'changed-path.sqlite3'
        $second=Invoke-Fixture; $json=ConvertTo-QuietJson $second
        Assert ((QuietCliGain -View Object).Totals.Deliveries -eq 1)
    }
    Check 'An unwritable database cannot change the business envelope and is observable' {
        $env:QUIETCLI_DB_PATH=$testRoot
        $native=Invoke-Fixture; $json=ConvertTo-QuietJson $native
        Assert (($json|ConvertFrom-Json).Outcome -eq 'Success')
        $report=QuietCliGain -View Object -DatabasePath (Join-Path $testRoot 'missing.sqlite3')
        Assert ([bool]$report.LastTrackingError)
        $failed=$false
        try { $ignored=QuietCliGain -View Object } catch {$failed=$true}
        Assert $failed 'An invalid database was silently reported as empty history.'
    }
    Check 'UTC boundaries, weighted totals, top groups and HTML escaping are correct' {
        $database=[QuietCliStack.GainV1.Database]::new($env:QUIETCLI_DB_PATH,$true)
        try {
            Seed-Delivery $database 'start' ([DateTimeOffset]'2026-01-01T00:00:00Z') 'tool<script>' 1000 100
            Seed-Delivery $database 'middle' ([DateTimeOffset]'2026-01-02T00:00:00Z') 'other' 100 200
            Seed-Delivery $database 'end' ([DateTimeOffset]'2026-01-03T00:00:00Z') 'excluded' 9000 1
        } finally {$database.Dispose()}
        $range=@{From=[DateTimeOffset]'2026-01-01T01:00:00+01:00';To=[DateTimeOffset]'2026-01-03T00:00:00Z'}
        $gain=QuietCliGain @range -View Object -MaxItems 1
        Assert ($gain.Totals.Deliveries -eq 2 -and $gain.Totals.SavedTokens -eq 800)
        Assert ($gain.Totals.SavingsPercent -eq 72.73 -and $gain.TotalGroups -eq 2 -and $gain.OmittedGroups -eq 1)
        Assert ($gain.Groups[0].Label -eq '2026-01-02') 'Day limit did not keep the latest group.'
        $byCommand=QuietCliGain @range -View Object -GroupBy Command
        Assert ($byCommand.Groups[0].Label -eq 'tool<script>')
        $htmlPath=QuietCliGain @range -View Html -GroupBy Command -OutputPath (Join-Path $testRoot 'safe.html')
        $html=Get-Content -LiteralPath $htmlPath -Raw
        Assert ($html -match 'tool&lt;script&gt;' -and $html -notmatch 'tool<script>')
    }
    Check 'Native child companions do not independently record the parent delivery' {
        $manifest=(Join-Path $moduleRoot 'FrenchExDev.QuietCli.PoSh.psd1').Replace("'","''")
        $childCode="Import-Module '$manifest'; Invoke-QuietTask -AsJson {`$s=Invoke-QuietStep native {Invoke-QuietCli -FilePath '__missing__' -Parse {param(`$c) @{}}};@{Done=`$true}}"
        $native=Invoke-Fixture -Code $childCode -Parse {param($c) $c.Stdout|ConvertFrom-Json}
        Assert ((QuietCliGain -View Object).Totals.Deliveries -eq 0)
        $json=ConvertTo-QuietJson $native
        Assert ((QuietCliGain -View Object).Totals.Deliveries -eq 1)
    }
    Check 'History persists across a fresh PowerShell process and contains no arguments or raw output' {
        $native=Invoke-Fixture -Code "[Console]::Out.Write(('PRIVATE_SENTINEL'*1000))"
        $json=ConvertTo-QuietJson $native
        $manifest=(Join-Path $moduleRoot 'FrenchExDev.QuietCli.PoSh.psd1').Replace("'","''")
        $fresh=Invoke-Fixture -Code "Import-Module '$manifest';QuietCliGain -View Json" -Parse {param($c) $c.Stdout|ConvertFrom-Json}
        Assert ($fresh.Outcome -eq 'Success' -and $fresh.Data.Totals.Deliveries -eq 1)
        $content=[Text.Encoding]::UTF8.GetString([IO.File]::ReadAllBytes($env:QUIETCLI_DB_PATH))
        Assert ($content -notmatch 'PRIVATE_SENTINEL|-EncodedCommand|StandardOutput')
    }
    Check 'Invalid periods and unsupported output combinations fail explicitly' {
        foreach($arguments in @(
            @{From='2026-02-02T00:00:00Z';To='2026-02-01T00:00:00Z'},
            @{View='Html'},
            @{View='Text';OutputPath='ignored.html'})) {
            $failed=$false
            try {$ignored=QuietCliGain @arguments} catch {$failed=$true}
            Assert $failed
        }
    }
    Check 'Browse opens only a saved HTML report, escapes its URI, and preserves the file on launch failure' {
        $gainModule=(Get-Command QuietCliGain).ScriptBlock.Module
        & $gainModule {
            $script:BrowserTest=@{Calls=[Collections.Generic.List[string]]::new();Fail=$false}
            function script:Start-Process {
                [CmdletBinding()]
                param([string]$FilePath)
                if (!(Test-Path -LiteralPath ([Uri]$FilePath).LocalPath)) { throw 'Browser launched before the report existed.' }
                $script:BrowserTest.Calls.Add($FilePath)
                if ($script:BrowserTest.Fail) { throw 'Browser unavailable in this test.' }
            }
        }
        try {
            $destination=Join-Path $testRoot 'gain é # report.html'
            $path=QuietCliGain -View Html -OutputPath $destination
            Assert ((& $gainModule {$script:BrowserTest.Calls.Count}) -eq 0) 'HTML opened without an explicit Browse switch.'
            $path=QuietCliGain -View Html -OutputPath $destination -Browse
            $calls=@(& $gainModule {$script:BrowserTest.Calls.ToArray()})
            Assert ($calls.Count -eq 1 -and $path -eq [IO.Path]::GetFullPath($destination))
            $uri=[Uri]$calls[0]
            Assert ($uri.IsFile -and $uri.LocalPath -eq $path -and $calls[0] -match '%23' -and $calls[0] -match '%20')
            foreach ($view in @('Text','Chart','Object','Json')) {
                $rejected=$false
                try {$ignored=QuietCliGain -View $view -Browse} catch {$rejected=$_.Exception.Message -match '-Browse'}
                Assert $rejected ('Browse was accepted with '+$view)
            }
            $rejected=$false
            try {$ignored=QuietCliGain -View Html -OutputPath $testRoot -Browse} catch {$rejected=$true}
            Assert ($rejected -and (& $gainModule {$script:BrowserTest.Calls.Count}) -eq 1) 'Browser launched after HTML generation failed.'
            & $gainModule {$script:BrowserTest.Fail=$true}
            $failure=$null
            try {$ignored=QuietCliGain -View Html -OutputPath $destination -Browse} catch {$failure=$_.Exception.Message}
            Assert ($failure -and $failure.Contains($path) -and (Test-Path -LiteralPath $path))
        } finally {
            & $gainModule {
                Remove-Item -LiteralPath 'Function:script:Start-Process' -Force
                Remove-Variable BrowserTest -Scope Script
            }
        }
    }
    Check 'An unrelated SQLite database is preserved rather than initialized as gain history' {
        $database=[QuietCliStack.GainV1.Database]::new($env:QUIETCLI_DB_PATH,$true)
        try {
            $database.Execute('DROP TABLE quietcli_deliveries',[object[]]@())
            $database.Execute('PRAGMA user_version=0',[object[]]@())
            $database.Execute('CREATE TABLE preserve_me(value TEXT)',[object[]]@())
            $database.Execute('INSERT INTO preserve_me VALUES (?)',[object[]]@('original'))
            $native=Invoke-Fixture; $json=ConvertTo-QuietJson $native
            Assert (($json|ConvertFrom-Json).Outcome -eq 'Success')
            Assert ($database.Query('PRAGMA user_version',[object[]]@())[0]['user_version'] -eq 0)
            Assert ($database.Query('SELECT value FROM preserve_me',[object[]]@())[0]['value'] -eq 'original')
        } finally {$database.Dispose()}
    }
    Check 'Concurrent writers initialize and update one real SQLite history without lost rows' {
        $manifest=Join-Path $moduleRoot 'FrenchExDev.QuietCli.PoSh.psd1'
        $sharedPath=$env:QUIETCLI_DB_PATH
        $writerErrors=@(1..4 | ForEach-Object -Parallel {
            $ErrorActionPreference='Stop'
            $connection=$null
            try {
                Import-Module $using:manifest -ErrorAction Stop
                $connection=[QuietCliStack.GainV1.Database]::new($using:sharedPath,$true)
                $connection.Execute(@'
INSERT INTO quietcli_deliveries
(id,timestamp_ms,command,outcome,native_calls,measured,analysis_complete,truncated,input_bytes,output_bytes,input_tokens,output_tokens,saved_tokens,elapsed_ms)
VALUES (?,?,'concurrent','Success',1,1,1,0,4000,400,1000,100,900,1)
'@
,[object[]]@([guid]::NewGuid().ToString('N'),[DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()))
            } catch {$_.Exception.Message}
            finally {if($connection){$connection.Dispose()}}
        } -ThrottleLimit 4)
        Assert ($writerErrors.Count -eq 0) ($writerErrors -join '; ')
        $gain=QuietCliGain -View Object
        Assert ($gain.Totals.Deliveries -eq 4 -and $gain.Totals.SavedTokens -eq 3600)
    }
} finally {
    $env:QUIETCLI_DB_PATH=$oldPath; $env:QUIETCLI_TRACKING=$oldTracking
}
$failed=@($checks|Where-Object {!$_.Passed}).Count
$report=[pscustomobject]@{Outcome=$(if($failed){'Failed'}else{'Success'});Passed=($checks.Count-$failed);Failed=$failed;
    Checks=$checks.ToArray();PowerShell=$PSVersionTable.PSVersion.ToString();ExecutedAtUtc=[DateTime]::UtcNow.ToString('o')}
$json=ConvertTo-Json -InputObject $report -Depth 6 -Compress
if (!$failed) {
    $resolvedRoot=(Get-Item -LiteralPath $testRoot).FullName
    $tempParent=[IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd([IO.Path]::DirectorySeparatorChar)
    if ([IO.Path]::GetDirectoryName($resolvedRoot) -eq $tempParent -and
        [IO.Path]::GetFileName($resolvedRoot) -like 'quietcli-gain-tests-*' -and
        !((Get-Item -LiteralPath $resolvedRoot).Attributes -band [IO.FileAttributes]::ReparsePoint)) {
        Remove-Item -LiteralPath $resolvedRoot -Recurse -Force
    }
}
if($OutputPath){[IO.File]::WriteAllText([IO.Path]::GetFullPath($OutputPath),$json,[Text.UTF8Encoding]::new($false))}
$json
if($failed){exit 1}