test/V2.Acceptance.Tests.ps1

#requires -Version 7.6
BeforeAll {
    $script:QuietRoot=Split-Path -Parent $PSScriptRoot
    $script:Manifest=Join-Path $QuietRoot 'FrenchExDev.QuietCli.PoSh.psd1'
    $env:QUIETCLI_DB_PATH=Join-Path $TestDrive 'history.sqlite3'
    $env:QUIETCLI_CONFIG_PATH=Join-Path $TestDrive 'settings.json'
    $env:QUIETCLI_TRACKING='1';$env:QUIETCLI_RECALL='1'
    Import-Module $Manifest -DisableNameChecking
    $script:Pwsh=(Get-Process -Id $PID).Path
    $script:GitContract=Join-Path $QuietRoot 'examples/contracts/Git.Status.ps1'
    function Native {
        param([string]$Code,[hashtable]$Extra=@{})
        $encoded=[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Code))
        Invoke-QuietCli -FilePath $Pwsh -ArgumentList @('-NoProfile','-EncodedCommand',$encoded) @Extra
    }
    function Git {
        param([string]$Repo,[string[]]$Words)
        $r=pump -AsObject -WorkingDirectory $Repo -OnOutput {param($r,$s)} git @Words
        if($r.Outcome -ne 'Success'){throw "Git fixture failed: $($Words -join ' ') exit=$($r.Execution.ExitCode)"}
    }
    function Repo {
        param([string]$Name,[switch]$Commit)
        $p=Join-Path $TestDrive $Name;[void][IO.Directory]::CreateDirectory($p)
        Git $p @('init','-q','--initial-branch=main')
        Git $p @('config','user.name','QuietCli Test');Git $p @('config','user.email','quietcli@example.invalid')
        if($Commit){[IO.File]::WriteAllText((Join-Path $p 'tracked.txt'),'initial');Git $p @('add','tracked.txt');Git $p @('-c','commit.gpgsign=false','commit','-qm','initial')}
        $p
    }
    function Status {
        param([string]$Repo,[switch]$Record)
        pump -AsObject -WorkingDirectory $Repo -ContractPath $GitContract -Record:$Record git status
    }
}
Describe 'QuietCli V2 local values and delivery' {
    It 'retains all 1500 local items and limits only the delivery' {
        $r=New-QuietResult -Data @(1..1500)
        $r.Data.Count|Should -Be 1500
        $d=$r|ConvertTo-QuietJson -MaxItems 3|ConvertFrom-Json
        @($d.Data).Count|Should -Be 3
        $d.Truncation.Omitted|Should -Be 1497
        $r.Data.Count|Should -Be 1500
    }
    It 'keeps complete data when the final byte budget drops the projection' {
        $r=New-QuietResult -Data @{Text=('x'*10000)}
        $text=$r|ConvertTo-QuietJson -MaxOutputBytes 512
        [Text.Encoding]::UTF8.GetByteCount($text)|Should -BeLessOrEqual 512
        ($text|ConvertFrom-Json).Truncation.Applied|Should -BeTrue
        $r.Data.Text.Length|Should -Be 10000
    }
    It 'projects domain values without modifying the original object' {
        $r=New-QuietResult -Data @{Values=@(1..2000)} -Projection {param($d,$s) @{Total=$d.Values.Count}}
        ($r|ConvertTo-QuietJson|ConvertFrom-Json).Data.Total|Should -Be 2000
        $r.Data.Values.Count|Should -Be 2000
    }
    It 'preserves empty singleton and nested arrays' {
        $r=New-QuietResult -Data @{Empty=@();One=@(7);Nested=@(@{Values=@(1,2)})}
        $d=$r|ConvertTo-QuietJson|ConvertFrom-Json
        @($d.Data.Empty).Count|Should -Be 0
        @($d.Data.One).Count|Should -Be 1
        @($d.Data.Nested[0].Values).Count|Should -Be 2
    }
    It 'masks secrets only at delivery' {
        $r=New-QuietResult -Data @{Password='private-value';Message='token=abcdefghijk'}
        $text=$r|ConvertTo-QuietJson
        $text|Should -Not -Match 'private-value|abcdefghijk'
        $r.Data.Password|Should -Be 'private-value'
    }
    It 'supports YAML without serializing capture or internal metadata' {
        $r=New-QuietResult -Data @{Number=3;Items=@('a','b')}
        $text=$r|Out-QuietResult -AsYaml
        $text|Should -Match '"SchemaVersion": 2'
        $text|Should -Not -Match 'Internal|Capture'
    }
}
Describe 'Native execution and composition' {
    It 'drains both streams and parses a complete document' {
        $r=Native '[Console]::Write("alpha");[Console]::Error.Write("beta")' @{Parse={param($c) @{Out=$c.Stdout;Err=$c.Stderr}}}
        $r.Outcome|Should -Be Success;$r.Data.Out|Should -Be alpha;$r.Data.Err|Should -Be beta
        $r.Completeness.Source|Should -BeTrue
    }
    It 'preserves Unicode stdin and environment overrides' {
        $r=Native '[Console]::Write([Console]::In.ReadToEnd()+$env:QUIET_TEST)' @{InputText='été 🌍';Environment=@{QUIET_TEST='ok'};Parse={param($c) $c.Stdout}}
        $r.Data|Should -Be 'été 🌍ok'
    }
    It 'handles NUL framing and embedded newlines' {
        $s=@{Items=[Collections.Generic.List[string]]::new()}
        $r=Native '[Console]::Write("a"+[char]10+"b"+[char]0+"été"+[char]0)' @{RecordDelimiter=[char]0;State=$s;OnOutput={param($r,$s) $s.Items.Add($r.Text)};Complete={param($s,$c) ,$s.Items.ToArray()}}
        $r.Outcome|Should -Be Success
        $r.Data.Count|Should -Be 2
        $r.Data[0]|Should -Be ("a"+[char]10+"b")
    }
    It 'counts an assigned failed native call exactly once through an explicit step' {
        $r=Invoke-QuietTask {
            $step=Invoke-QuietStep named { Native 'exit 7' @{Parse={param($c) @{Seen=$true}}} }
            @{Done=$true}
        }
        $r.Outcome|Should -Be Failed;$r.Execution.Steps|Should -Be 1;$r.Execution.Issues[0].ExitCode|Should -Be 7
    }
    It 'counts an assigned failed native call without an explicit step' {
        $r=Invoke-QuietTask {$step=Native 'exit 8' @{Parse={param($c) $null}};@{Done=$true}}
        $r.Outcome|Should -Be Failed;$r.Execution.Steps|Should -Be 1
    }
    It 'continues streaming analysis after its saved capture fills' {
        $s=@{Count=0}
        $r=Native '1..200 | ForEach-Object {[Console]::WriteLine("record-$_")}' @{MaxCaptureBytes=64;State=$s;OnOutput={param($r,$s) $s.Count++};Complete={param($s,$c) @{Count=$s.Count}}}
        $r.Outcome|Should -Be Success;$r.Data.Count|Should -Be 200
        $r.Completeness.Analysis|Should -BeTrue;$r.Completeness.Capture|Should -BeFalse
    }
    It 'rejects incomplete document captures' {
        $r=Native '[Console]::Write("x"*1000)' @{MaxCaptureBytes=64;Parse={param($c) $c.Stdout}}
        $r.Outcome|Should -Be Failed;$r.Error.Code|Should -Be CaptureLimit
    }
    It 'reports oversized records while fully draining the source' {
        $r=Native '[Console]::WriteLine("x"*2000)' @{MaxRecordCharacters=1024;OnOutput={param($r,$s)};Complete={param($s,$c) @{Done=$true}}}
        $r.Error.Code|Should -Be RecordLimit;$r.Completeness.Source|Should -BeTrue
    }
    It 'retains callback failure and still drains stdout and stderr' {
        $r=Native '1..50|ForEach-Object {[Console]::WriteLine($_);[Console]::Error.WriteLine($_)}' @{OnOutput={param($r,$s) throw 'bad parser'};Complete={param($s,$c) @{Drained=$c.SourceComplete}}}
        $r.Error.Code|Should -Be ParserFailed;$r.Data.Drained|Should -BeTrue
    }
    It 'reports native failure separately from finalizer failure' {
        $r=Native 'exit 4' @{OnOutput={param($r,$s)};Complete={throw 'finalizer failed'}}
        $r.Error.Code|Should -Be NativeFailed;$r.Execution.FinalizerError.Code|Should -Be FinalizerFailed
    }
    It 'times out a quiet child' {
        $r=Native 'Start-Sleep -Seconds 5' @{TimeoutSeconds=1;Parse={param($c) $null}}
        $r.Outcome|Should -Be Timeout
    }
    It 'cancels before native startup' {
        $signal=[Threading.CancellationTokenSource]::new();$signal.Cancel()
        $marker=Join-Path $TestDrive 'must-not-start'
        $r=Native ("[IO.File]::WriteAllText('"+$marker+"','bad')") @{CancellationToken=$signal.Token;Parse={param($c) $null}}
        $r.Outcome|Should -Be Cancelled
        Test-Path $marker|Should -BeFalse
        $signal.Dispose()
    }
    It 'cancels a running quiet child' {
        $signal=[Threading.CancellationTokenSource]::new();$signal.CancelAfter(1000)
        try{$r=Native 'Start-Sleep -Seconds 5' @{CancellationToken=$signal.Token;Parse={param($c) $null}};$r.Outcome|Should -Be Cancelled}finally{$signal.Dispose()}
    }
    It 'does not run an uncovered executable even with RECORD' {
        $r=pump -AsObject -Record $Pwsh -NoProfile -Command 'throw "must not run"'
        $r.Outcome|Should -Be Unsupported;$r.Error.Code|Should -Be ContractNotFound
    }
}
Describe 'Documented Git status contracts' {
    It 'reports an unborn clean repository' {
        $p=Repo unborn;$r=Status $p
        $r.Outcome|Should -Be Success;$r.Data.Unborn|Should -BeTrue;$r.Data.Clean|Should -BeTrue;$r.Data.Branch|Should -Be main
    }
    It 'changes its observation with the repository and time' {
        $p=Repo dynamic -Commit;$clean=Status $p
        [IO.File]::WriteAllText((Join-Path $p 'tracked.txt'),'changed')
        [IO.File]::WriteAllText((Join-Path $p 'new file.txt'),'new')
        $changed=Status $p
        $clean.Data.Clean|Should -BeTrue;$changed.Data.Clean|Should -BeFalse
        $changed.Data.Counts.Unstaged|Should -Be 1;$changed.Data.Counts.Untracked|Should -Be 1
        Git $p @('add','tracked.txt')
        (Status $p).Data.Counts.Staged|Should -Be 1
    }
    It 'retains original and destination rename paths' {
        $p=Repo renamed -Commit;Git $p @('mv','tracked.txt','renamed file.txt')
        $r=Status $p
        $r.Data.Counts.Renamed|Should -Be 1;$r.Data.Entries[0].OriginalPath|Should -Be tracked.txt
        $r.Data.Entries[0].Path|Should -Be 'renamed file.txt'
    }
    It 'handles detached HEAD' {
        $p=Repo detached -Commit;Git $p @('checkout','--detach','-q')
        $r=Status $p;$r.Data.Detached|Should -BeTrue;$r.Data.Branch|Should -BeNullOrEmpty
    }
    It 'handles newline Unicode and leading dash pathnames' {
        $p=Repo unicode
        $name="été "+[char]10+'-file'
        [IO.File]::WriteAllText((Join-Path $p $name),'hello')
        $r=Status $p;$r.Data.Entries[0].Path|Should -Be $name
    }
    It 'rejects unsupported arguments before native execution' {
        $r=pump -AsObject -ContractPath $GitContract git status --verbose
        $r.Outcome|Should -Be Unsupported
    }
    It 'replays the stored observation without observing later repository changes' {
        $p=Repo replay -Commit;[IO.File]::WriteAllText((Join-Path $p 'new.txt'),'one')
        $original=Status $p -Record
        $original.RecordId|Should -Not -BeNullOrEmpty
        [IO.File]::WriteAllText((Join-Path $p 'later.txt'),'two')
        $replayed=Invoke-QuietRecord -Id $original.RecordId -ContractPath $GitContract -AsObject
        $replayed.Outcome|Should -Be Success;$replayed.Data.Counts.Untracked|Should -Be 1
        (Status $p).Data.Counts.Untracked|Should -Be 2
        (Get-QuietRecord -Id $original.RecordId -AsObject).Promotable|Should -BeTrue
    }
}
Describe 'RECORD modules and measurement' {
    It 'generates a module validates independent assertions and installs explicitly' {
        $p=Repo packaged
        [IO.File]::WriteAllText((Join-Path $p 'proof.txt'),'one')
        $observed=Status $p -Record
        $assert=Join-Path $TestDrive 'Independent.Assertions.ps1'
        [IO.File]::WriteAllText($assert,'param($Cases) if($Cases.Count -ne 1 -or $Cases[0].Actual.Data.Counts.Untracked -ne 1 -or $Cases[0].Actual.Data.Entries[0].Path -ne "proof.txt"){throw "Expected the independently prepared one-file unborn fixture."}')
        $target=Join-Path $TestDrive 'generated'
        $generated=New-QuietModule -Command git -Name FrenchExDev.QuietCli.Git.PoSh -ContractPath $GitContract -RecordId $observed.RecordId -AssertionPath $assert -OutputPath $target
        $check=Test-QuietModule -Path $generated.Path -AsObject
        $check.Data.Success|Should -BeTrue -Because ($check.Data.Failures -join '; ')
        $installed=Install-QuietModule -Path $generated.Path -InstallRoot (Join-Path $TestDrive 'installed')
        $code="Import-Module '"+$Manifest+"' -DisableNameChecking; Set-Location '"+$p+"'; "+'$r=pump -AsObject git status; @{Outcome=$r.Outcome;Count=$r.Data.Counts.Untracked}|ConvertTo-Json -Compress'
        $fresh=Native $code @{Parse={param($c) $c.Stdout|ConvertFrom-Json}}
        $fresh.Outcome|Should -Be Success;$fresh.Data.Outcome|Should -Be Success;$fresh.Data.Count|Should -Be 1
        $second=Join-Path $TestDrive 'generated-again'
        $again=New-QuietModule -Command git -Name FrenchExDev.QuietCli.Git.PoSh -ContractPath $GitContract -RecordId $observed.RecordId -AssertionPath $assert -OutputPath $second
        $again.Hash|Should -Be $generated.Hash
    }
    It 'does not promote inline callback closures without explicit source files' {
        $r=Native '"hello"' @{Parse={param($c) @{Text=$c.Stdout}};Record=$true}
        (Get-QuietRecord -Id $r.RecordId -AsObject).Promotable|Should -BeFalse
    }
    It 'rejects replay that tries to execute another native command' {
        $r=Native '"saved"' @{Parse={param($c) $c.Stdout};Record=$true}
        $contract=New-QuietContract -Id forbidden -FilePath $Pwsh -Parse {param($c) Invoke-QuietCli -FilePath git -ArgumentList @('--version') -Parse {param($c) $c.Stdout}}
        $replay=Invoke-QuietRecord -Id $r.RecordId -Contract $contract -AsObject
        $replay.Outcome|Should -Be Failed;$replay.Error.Code|Should -Be ReplayFailed
    }
    It 'counts the final delivery once even when serialized twice' {
        $r=Native '"measured"' @{Parse={param($c) $c.Stdout}}
        $before=(QuietCliGain -View Object).Totals.Deliveries
        $null=$r|ConvertTo-QuietJson;$null=$r|ConvertTo-QuietJson
        (QuietCliGain -View Object).Totals.Deliveries|Should -Be ($before+1)
    }
    It 'keeps an invalid encoding visible and its exact token baseline unknown' {
        $r=Native '[Console]::OpenStandardOutput().Write([byte[]](255,10))' @{OnOutput={param($r,$s)};Complete={param($s,$c) @{Seen=$true}}}
        $r.Capture.Lossy|Should -BeTrue
        $null=$r|ConvertTo-QuietJson
        $r.Measurement.Status|Should -Be CounterUnavailable
    }
    It 'preserves business success when storage is unavailable' {
        $previous=$env:QUIETCLI_DB_PATH
        try{$env:QUIETCLI_DB_PATH='/proc/quietcli-impossible/history.sqlite3';$r=Native '"okay"' @{Parse={param($c) $c.Stdout};Record=$true};$r.Outcome|Should -Be Success;$null=$r|ConvertTo-QuietJson;$r.Measurement.Status|Should -Be StorageUnavailable}
        finally{$env:QUIETCLI_DB_PATH=$previous}
    }
    It 'installs hooks only on request and preserves unrelated settings' {
        $path=Join-Path $TestDrive 'claude-settings.json'
        [IO.File]::WriteAllText($path,'{"custom":42,"hooks":{"PreToolUse":[{"matcher":"Read","hooks":[]}]}}')
        Install-QuietHook -SettingsPath $path
        $settings=Get-Content $path -Raw|ConvertFrom-Json
        $settings.custom|Should -Be 42;$settings.hooks.PreToolUse.Count|Should -Be 2
        Uninstall-QuietHook -SettingsPath $path
        (Get-Content $path -Raw|ConvertFrom-Json).hooks.PreToolUse.Count|Should -Be 1
    }
}

Describe 'Additional V2 qualification' {
    It 'replays the observed interleaving of stdout and stderr' {
        $state=@{Trace=[Collections.Generic.List[string]]::new()}
        $read={param($r,$s) $s.Trace.Add($r.Stream+':'+$r.Text)}
        $finish={param($s,$c) ,$s.Trace.ToArray()}
        $live=Native '[Console]::WriteLine("first");Start-Sleep -Milliseconds 40;[Console]::Error.WriteLine("second");Start-Sleep -Milliseconds 40;[Console]::WriteLine("third")' @{OnOutput=$read;State=$state;Complete=$finish;Record=$true}
        $contract=New-QuietContract -Id ordered -FilePath $Pwsh -State @{Trace=[Collections.Generic.List[string]]::new()} -OnOutput $read -Complete $finish
        $replayed=Invoke-QuietRecord -Id $live.RecordId -Contract $contract -AsObject
        $replayed.Outcome|Should -Be Success
        ($replayed.Data -join '|')|Should -Be ($live.Data -join '|')
    }
    It 'replays an explicit multi-call task and its original aggregation' {
        $path=Join-Path $TestDrive 'Composed.Contract.ps1'
        $code=@'
param($Invocation)
New-QuietContract -Id composed -SourcePath @($PSCommandPath) -Documentation @('fixture:two independent integers') -Execute {
    param($call)
    $first=Invoke-QuietCli -FilePath $call.FilePath -ArgumentList @('-NoProfile','-Command','[Console]::Write(12)') -Parse {param($c) [int]$c.Stdout}
    $second=Invoke-QuietCli -FilePath $call.FilePath -ArgumentList @('-NoProfile','-Command','[Console]::Write(30)') -Parse {param($c) [int]$c.Stdout}
    @{Total=$first.Data+$second.Data}
}
'@

        [IO.File]::WriteAllText($path,$code)
        $live=pump -AsObject -Record -ContractPath $path $Pwsh composed
        $live.Outcome|Should -Be Success;$live.Data.Total|Should -Be 42;$live.Execution.Steps|Should -Be 2
        $replayed=Invoke-QuietRecord -Id $live.RecordId -ContractPath $path -AsObject
        $replayed.Outcome|Should -Be Success;$replayed.Data.Total|Should -Be 42;$replayed.Execution.Steps|Should -Be 2
    }
    It 'reports a real Git merge conflict' {
        $p=Repo conflict -Commit;Git $p @('checkout','-qb','other')
        [IO.File]::WriteAllText((Join-Path $p 'tracked.txt'),'other');Git $p @('add','tracked.txt');Git $p @('-c','commit.gpgsign=false','commit','-qm','other')
        Git $p @('checkout','-q','main');[IO.File]::WriteAllText((Join-Path $p 'tracked.txt'),'main');Git $p @('add','tracked.txt');Git $p @('-c','commit.gpgsign=false','commit','-qm','main')
        $merge=Invoke-QuietCli -FilePath git -WorkingDirectory $p -ArgumentList @('merge','other') -SuccessExitCodes @(0,1) -Parse {param($c) @{Code=$c.ExitCode}}
        $merge.Data.Code|Should -Be 1
        $r=Status $p;$r.Data.Counts.Conflicted|Should -Be 1;$r.Data.Clean|Should -BeFalse
    }
    It 'reports divergence against local upstream refs without fetching' {
        $p=Repo upstream -Commit;Git $p @('branch','reference');Git $p @('branch','--set-upstream-to=reference','main')
        [IO.File]::WriteAllText((Join-Path $p 'tracked.txt'),'next');Git $p @('add','tracked.txt');Git $p @('-c','commit.gpgsign=false','commit','-qm','next')
        $r=Status $p;$r.Data.Upstream|Should -Be reference;$r.Data.Ahead|Should -Be 1;$r.Data.Behind|Should -Be 0
    }
    It 'migrates a V1 database and keeps estimator totals separate from exact profiles' {
        $previous=$env:QUIETCLI_DB_PATH;$env:QUIETCLI_DB_PATH=Join-Path $TestDrive 'legacy.sqlite3'
        try{
            $db=[QuietCliStack.GainV2.Database]::new($env:QUIETCLI_DB_PATH,$true)
            $db.Execute('DROP TABLE quietcli_v2_events',[object[]]@());$db.Execute('PRAGMA user_version=1',[object[]]@())
            $db.Execute('INSERT INTO quietcli_deliveries VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)',[object[]]@('old',[DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds(),'legacy','Success',1,1,1,0,400,40,100,10,90,5));$db.Dispose()
            $r=Native '"new exact output"' @{Parse={param($c) $c.Stdout}};$null=$r|ConvertTo-QuietJson
            $gain=QuietCliGain -View Object
            $gain.Totals.Deliveries|Should -Be 2;$gain.Methods.Count|Should -Be 2
            $gain.Totals.SavedTokens|Should -BeNullOrEmpty
            @($gain.Methods|Where-Object Label -Match estimate)[0].Totals.SavedTokens|Should -Be 90
        }finally{$env:QUIETCLI_DB_PATH=$previous}
    }
    It 'keeps literal native argv values including spaces and shell-looking text' {
        $r=Invoke-QuietCli -FilePath $Pwsh -ArgumentList @('-NoProfile','-CommandWithArgs','[Console]::Write(($args|ConvertTo-Json -Compress))','a space','--literal','$(not executed)') -Parse {param($c) ,@($c.Stdout|ConvertFrom-Json)}
        $r.Outcome|Should -Be Success
        $r.Data[0]|Should -Be 'a space';$r.Data[1]|Should -Be '--literal';$r.Data[2]|Should -Be '$(not executed)'
    }
    It 'keeps masked JSON captures parseable on replay' {
        $r=Native '[Console]::Write(''{"token":"unshared-secret","count":2}'')' @{Parse={param($c) $c.Stdout|ConvertFrom-Json};Record=$true}
        $contract=New-QuietContract -Id json -FilePath $Pwsh -Parse {param($c) $c.Stdout|ConvertFrom-Json}
        $replayed=Invoke-QuietRecord -Id $r.RecordId -Contract $contract -AsObject
        $replayed.Outcome|Should -Be Success;$replayed.Data.count|Should -Be 2
        $replayed.Data.token|Should -Be '[REDACTED]'
    }
}

Describe 'Packaging boundaries and presentation' {
    It 'preserves a literal separator and pathspec through pump' {
        $p=Repo pathspec;[IO.File]::WriteAllText((Join-Path $p 'one file.txt'),'one');[IO.File]::WriteAllText((Join-Path $p 'other.txt'),'two')
        $r=pump -AsObject -ContractPath $GitContract -WorkingDirectory $p -- git status -- 'one file.txt'
        $r.Outcome|Should -Be Success;$r.Data.Counts.Untracked|Should -Be 1;$r.Data.Entries[0].Path|Should -Be 'one file.txt'
    }
    It 'keeps late failure diagnostics after the capture file fills' {
        $r=Native '1..100|ForEach-Object {[Console]::WriteLine("long output $_")};[Console]::Error.WriteLine("late diagnostic");exit 9' @{MaxCaptureBytes=64;OnOutput={param($r,$s)}}
        $r.Outcome|Should -Be Failed
        $d=$r|ConvertTo-QuietJson|ConvertFrom-Json
        ($d.Excerpt.stderr -join ' ')|Should -Match 'late diagnostic'
        $r.Completeness.Capture|Should -BeFalse
    }
    It 'preserves declared relative source dependencies in the generated tree' {
        $codeRoot=Join-Path $TestDrive 'source-tree';[void][IO.Directory]::CreateDirectory((Join-Path $codeRoot 'factory'));[void][IO.Directory]::CreateDirectory((Join-Path $codeRoot 'lib'))
        $helper=Join-Path $codeRoot 'lib/Value.ps1'
        [IO.File]::WriteAllText($helper,'function Read-BusinessValue { param($c) @{Number=[int]$c.Stdout;Unit="items"} }')
        $factory=Join-Path $codeRoot 'factory/Count.ps1'
        $content=@'
param($Invocation)
$helper=Join-Path $PSScriptRoot '../lib/Value.ps1'
. $helper
$reader=Get-Command Read-BusinessValue -CommandType Function
$parse={param($c) & $reader $c}.GetNewClosure()
New-QuietContract -Id count -FilePath $Invocation.FilePath -ArgumentList @('-NoProfile','-Command','[Console]::Write(42)') -Parse $parse -SourcePath @($PSCommandPath,$helper) -Documentation @('fixture:42 independent items')
'@

        [IO.File]::WriteAllText($factory,$content)
        $r=pump -AsObject -Record -ContractPath $factory $Pwsh count
        $r.Outcome|Should -Be Success;$r.Data.Number|Should -Be 42
        $assert=Join-Path $TestDrive 'Count.Assertions.ps1'
        [IO.File]::WriteAllText($assert,'param($Cases) if($Cases[0].Actual.Data.Number -ne 42 -or $Cases[0].Actual.Data.Unit -ne "items"){throw "Expected 42 items from the fixture."}')
        $module=New-QuietModule -Command pwsh -ContractPath $factory -RecordId $r.RecordId -AssertionPath $assert -SourceRoot $codeRoot -OutputPath (Join-Path $TestDrive 'tree-module')
        Test-Path (Join-Path (Split-Path $module.Path) 'contracts/lib/Value.ps1')|Should -BeTrue
        $check=Test-QuietModule -Path $module.Path -AsObject
        $check.Outcome|Should -Be Success -Because ($check.Data.Failures -join ';')
    }
    It 'reports unknown tokens and known bytes separately when counting is off' {
        $previous=$env:QUIETCLI_DB_PATH;$counter=$env:QUIETCLI_TOKENIZER
        try{
            $env:QUIETCLI_DB_PATH=Join-Path $TestDrive 'bytes-only.sqlite3';$env:QUIETCLI_TOKENIZER='off'
            $r=Native '"known bytes"' @{Parse={param($c) $c.Stdout}};$null=$r|ConvertTo-QuietJson
            $report=QuietCliGain -View Object
            $report.Totals.InputBytes|Should -BeGreaterThan 0
            $report.Totals.SavedTokens|Should -BeNullOrEmpty
            $report.Totals.ByteMeasuredDeliveries|Should -Be 1
        }finally{$env:QUIETCLI_DB_PATH=$previous;$env:QUIETCLI_TOKENIZER=$counter}
    }
}

Describe 'Agent hook decisions' {
    It 'detects an unpumped command in a mixed invocation' {
        $eventInput=@{tool_name='PowerShell';tool_input=@{command='pump git status; git log -1'}}
        $decision=Invoke-QuietHook -Event $eventInput -Awareness deny
        $decision.hookSpecificOutput.permissionDecision|Should -Be deny
    }
    It 'asks approval for a self-contained rewrite without invoking the factory' {
        Register-QuietCommand -Command fixturehook -ModuleId hook-fixture -Factory {throw 'The hook must not execute the contract factory.'}
        $decision=Invoke-QuietHook -Event @{tool_name='Bash';tool_input=@{command='fixturehook status'}} -Awareness rewrite
        $decision.hookSpecificOutput.permissionDecision|Should -Be ask
        $line=$decision.hookSpecificOutput.updatedInput.command
        $line|Should -Match '^pwsh -NoProfile -EncodedCommand '
        $encoded=($line -split ' ')[-1]
        $body=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($encoded))
        $body|Should -Match 'Import-Module'
        $body|Should -Match 'quiet -- fixturehook status'
    }
}

Describe 'YAML semantic verification' {
    It 'round trips empty maps arrays Unicode and nested objects through an independent parser' {
        Import-Module powershell-yaml -ErrorAction Stop
        $r=New-QuietResult -Data @{EmptyMap=@{};EmptyArray=@();Nested=@(@{},@{Name="été";Values=@(1,2)});Unknown=$null}
        $yaml=$r|Out-QuietResult -AsYaml
        $parsed=ConvertFrom-Yaml $yaml
        $parsed.SchemaVersion|Should -Be 2
        $parsed.Data.EmptyMap.Count|Should -Be 0
        @($parsed.Data.EmptyArray).Count|Should -Be 0
        $parsed.Data.Nested[1].Name|Should -Be 'été'
        @($parsed.Data.Nested[1].Values).Count|Should -Be 2
        $parsed.Data.Unknown|Should -BeNullOrEmpty
    }
}

Describe 'Factory construction boundaries' {
    It 'does not treat a declined candidate as a failed execution of the enclosing task' {
        Register-QuietCommand -Command fixturecandidate -ModuleId factory-test -Factory {
            $declined=New-QuietResult -Outcome Unsupported
            New-QuietContract -Id selected -FilePath (Get-Process -Id $PID).Path -ArgumentList @('-NoProfile','-Command','[Console]::Write(42)') -Parse {param($c) [int]$c.Stdout}
        }
        $r=Invoke-QuietTask {$selected=pump -AsObject fixturecandidate run;@{Answer=$selected.Data}}
        $r.Outcome|Should -Be Success;$r.Data.Answer|Should -Be 42;$r.Execution.Steps|Should -Be 1
    }
    It 'keeps native launches out of contract selection' {
        Register-QuietCommand -Command fixtureselection -ModuleId factory-test -Factory {
            Invoke-QuietCli -FilePath (Get-Process -Id $PID).Path -ArgumentList @('-NoProfile','-Command','throw "must not launch"') -Parse {param($c) $c.Stdout}
        }
        $r=pump -AsObject fixtureselection run
        $r.Outcome|Should -Be Failed
        $r.Error.Message|Should -Match 'Factories construct contracts'
    }
}

Describe 'Quiet command and output callback public interfaces' {
    It 'exports both public functions and exposes only the new streaming parameter' {
        foreach($name in @('quiet','pump')){
            (Get-Command $name -Module FrenchExDev.QuietCli.PoSh).CommandType|Should -Be Function
        }
        foreach($name in @('Invoke-QuietCli','New-QuietContract')){
            $command=Get-Command $name -Module FrenchExDev.QuietCli.PoSh
            $command.Parameters.ContainsKey('OnOutput')|Should -BeTrue
            $command.Parameters.ContainsKey('OnRecord')|Should -BeFalse
            @($command.Parameters.Values|Where-Object {$_.Aliases -contains 'OnRecord'}).Count|Should -Be 0
        }
        $contract=New-QuietContract -Id output.fixture -FilePath $Pwsh -OnOutput {param($output,$state)}
        $contract.OnOutput|Should -BeOfType scriptblock
        $contract.PSObject.Properties['OnRecord']|Should -BeNullOrEmpty
    }
    It 'passes flags colon values spaces empty values and native array splatting identically' {
        $child=Join-Path $TestDrive 'arguments.ps1'
        [IO.File]::WriteAllText($child,'[pscustomobject]@{Arguments=@($args)}|ConvertTo-Json -Compress')
        $tail=@('-c','-v','-o','-name:value','a file with spaces','','--','-path')
        $expected=@('-NoProfile','-File',$child)+$tail
        $a=quiet -AsObject -Parse {param($capture) $capture.Stdout|ConvertFrom-Json} -- $Pwsh -NoProfile -File $child @tail
        $b=pump -AsObject -Parse {param($capture) $capture.Stdout|ConvertFrom-Json} -- $Pwsh -NoProfile -File $child @tail
        foreach($result in @($a,$b)){
            $result.Outcome|Should -Be Success
            $source=@($result.Internal.Sources.Values)[0]
            $source.Request.ArgumentList|Should -Be $expected
        }
        $a.Data.Arguments|Should -Be $b.Data.Arguments
    }
    It 'handles literal native separators through both module-qualified functions' {
        $child=Join-Path $TestDrive 'qualified-arguments.ps1'
        [IO.File]::WriteAllText($child,'[pscustomobject]@{Arguments=@($args)}|ConvertTo-Json -Compress')
        $a=FrenchExDev.QuietCli.PoSh\quiet -AsObject -Parse {param($capture) $capture.Stdout|ConvertFrom-Json} -- $Pwsh -NoProfile -File $child -- -literal
        $b=FrenchExDev.QuietCli.PoSh\pump -AsObject -Parse {param($capture) $capture.Stdout|ConvertFrom-Json} -- $Pwsh -NoProfile -File $child -- -literal
        $expected=@('-NoProfile','-File',$child,'--','-literal')
        foreach($result in @($a,$b)){
            $result.Outcome|Should -Be Success
            (@($result.Internal.Sources.Values)[0]).Request.ArgumentList|Should -Be $expected
        }
    }
    It 'rejects a removed native separator before execution for all public spellings' {
        $marker=Join-Path $TestDrive 'separator-must-not-run'
        $child=Join-Path $TestDrive 'separator-child.ps1'
        [IO.File]::WriteAllText($child,("[IO.File]::WriteAllText('"+$marker+"','started')"))
        foreach($name in @('quiet','pump','FrenchExDev.QuietCli.PoSh\quiet','FrenchExDev.QuietCli.PoSh\pump')){
            $call=[scriptblock]::Create($name+' -AsObject -OnOutput {} $Pwsh -NoProfile -File $child -- -literal')
            {& $call}|Should -Throw '*PowerShell removed*'
        }
        Test-Path $marker|Should -BeFalse
    }
    It 'delivers stdout stderr and the same caller state to the output callback' {
        $state=@{Identity='caller-state';Items=[Collections.Generic.List[object]]::new()}
        $code='[Console]::WriteLine("business");[Console]::Error.WriteLine("diagnostic")'
        $r=quiet -AsObject -State $state -OnOutput {
            param($output,$state)
            $state.Items.Add([pscustomobject]@{Stream=$output.Stream;Text=$output.Text})
        } -Complete {param($state,$capture) @{Identity=$state.Identity;Items=$state.Items.ToArray();Drained=$capture.SourceComplete}} $Pwsh -NoProfile -Command $code
        $r.Outcome|Should -Be Success
        $r.Data.Identity|Should -BeExactly 'caller-state'
        $r.Data.Drained|Should -BeTrue
        @($r.Data.Items|Where-Object {$_.Stream -eq 'stdout' -and $_.Text -eq 'business'}).Count|Should -Be 1
        @($r.Data.Items|Where-Object {$_.Stream -eq 'stderr' -and $_.Text -eq 'diagnostic'}).Count|Should -Be 1
        $state.Items.Count|Should -Be 2
    }
    It 'rejects the retired parameter without starting a native process' {
        $marker=Join-Path $TestDrive 'retired-must-not-run'
        $code="[IO.File]::WriteAllText('"+$marker+"','started')"
        {Invoke-QuietCli -FilePath $Pwsh -ArgumentList @('-NoProfile','-Command',$code) -Parse {} -OnRecord {}}|Should -Throw '*OnRecord*'
        {New-QuietContract -Id retired -FilePath $Pwsh -Parse {} -OnRecord {}}|Should -Throw '*OnRecord*'
        {quiet -AsObject -OnRecord {} $Pwsh -NoProfile -Command $code}|Should -Throw '*OnRecord*'
        {pump -AsObject -OnRecord {} $Pwsh -NoProfile -Command $code}|Should -Throw '*OnRecord*'
        Test-Path $marker|Should -BeFalse
    }
    It 'preserves callback failure diagnostics and drains both streams through quiet' {
        $code='1..20|ForEach-Object{[Console]::WriteLine($_);[Console]::Error.WriteLine($_)}'
        $r=quiet -AsObject -OnOutput {param($output,$state) throw 'output callback failed'} -Complete {param($state,$capture) @{Drained=$capture.SourceComplete}} $Pwsh -NoProfile -Command $code
        $r.Outcome|Should -Be Failed
        $r.Error.Code|Should -Be ParserFailed
        $r.Completeness.Source|Should -BeTrue
        $r.Completeness.Analysis|Should -BeFalse
        $r.Data.Drained|Should -BeTrue
    }
    It 'preserves quiet timeouts and pre-start cancellation' {
        $r=quiet -AsObject -TimeoutSeconds 1 -OnOutput {} $Pwsh -NoProfile -Command 'Start-Sleep -Seconds 5'
        $r.Outcome|Should -Be Timeout
        $signal=[Threading.CancellationTokenSource]::new()
        $signal.Cancel()
        try {
            $marker=Join-Path $TestDrive 'cancelled-quiet-must-not-run'
            $code="[IO.File]::WriteAllText('"+$marker+"','started')"
            $r=quiet -AsObject -CancellationToken $signal.Token -OnOutput {} $Pwsh -NoProfile -Command $code
            $r.Outcome|Should -Be Cancelled
            Test-Path $marker|Should -BeFalse
        }finally{$signal.Dispose()}
    }
}
Describe 'Output callback replay and agent recognition' {
    It 'replays a source-declared output contract without relaunching its native producer' {
        $marker=Join-Path $TestDrive 'streaming-launches.txt'
        $factory=Join-Path $TestDrive 'Streaming.Output.ps1'
        $factoryLines=@(
            'param($Invocation)'
            '$state=@{Items=[Collections.Generic.List[object]]::new()}'
            'New-QuietContract -Id streaming.output -FilePath $Invocation.FilePath -ArgumentList $Invocation.ArgumentList -State $state -OnOutput {param($output,$state) $state.Items.Add([pscustomobject]@{Stream=$output.Stream;Text=$output.Text})} -Complete {param($state,$capture) @{Frames=$state.Items.ToArray()}} -SourcePath @($PSCommandPath) -Documentation @("fixture: one stdout line, one stderr line, producer launch marker")'
        )
        [IO.File]::WriteAllText($factory,($factoryLines -join [Environment]::NewLine))
        $code="[IO.File]::AppendAllText('"+$marker+"','launch'+[char]10);[Console]::WriteLine('saved-out');[Console]::Error.WriteLine('saved-err')"
        $encoded=[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($code))
        $recorded=quiet -AsObject -Record -ContractPath $factory $Pwsh -NoProfile -EncodedCommand $encoded
        $recorded.Outcome|Should -Be Success
        $replayed=Invoke-QuietRecord -Id $recorded.RecordId -ContractPath $factory -AsObject
        $replayed.Outcome|Should -Be Success
        $replayed.Execution.Replay|Should -BeTrue
        @($replayed.Data.Frames).Count|Should -Be 2
        ($replayed.Data.Frames|ConvertTo-Json -Compress)|Should -BeExactly ($recorded.Data.Frames|ConvertTo-Json -Compress)
        @([IO.File]::ReadAllLines($marker)).Count|Should -Be 1
    }
    It 'recognizes both entry points in hook decisions and measured adoption' {
        foreach($line in @('quiet git status','pump git status','FrenchExDev.QuietCli.PoSh\quiet git status','FrenchExDev.QuietCli.PoSh\pump git status')){
            (Invoke-QuietHook -Event @{tool_input=@{command=$line}} -Awareness deny).Count|Should -Be 0
        }
        $transcript=Join-Path $TestDrive 'entrypoint-session.jsonl'
        $lines=@(foreach($line in @('quiet git status','pump git status','FrenchExDev.QuietCli.PoSh\quiet git status','git status')){
            @{input=@{command=$line}}|ConvertTo-Json -Compress
        })
        [IO.File]::WriteAllLines($transcript,[string[]]$lines)
        $audit=Get-QuietSession -Path $transcript -AsObject
        $audit.Data[0].Calls|Should -Be 4
        $audit.Data[0].Pumped|Should -Be 3
        $audit.Data[0].UnpumpedOrUnknown|Should -Be 1
    }
}
Describe 'Graphical gain HTML' {
    BeforeAll {
        function Add-GainHtmlSample {
            param([hashtable]$Values=@{})
            $row=@{Id=[Guid]::NewGuid().ToString('N');TimestampUtc=[DateTimeOffset]::UtcNow.ToString('o')
                Command='sample';Outcome='Success';NativeCalls=1;Measured=$true;ByteMeasured=$true
                Method='exact';Profile='o200k_base';Version='fixture/1';InputTokens=1000;OutputTokens=200;SavedTokens=800
                InputBytes=4000;OutputBytes=800;AnalysisComplete=$true;Truncated=$false;Fingerprint=$null}
            foreach($key in $Values.Keys){$row[$key]=$Values[$key]}
            & (Get-Module -All QuietCli) {param($row) Save-QuietEvent $row.Id delivery $row.Command $row} $row
        }
        function Read-GainHtmlSvg {
            param([string]$Html)
            foreach($match in [regex]::Matches($Html,'(?s)<svg\b.*?</svg>')){[xml]$match.Value}
        }
    }
    BeforeEach {
        $script:PreviousHtmlDatabase=$env:QUIETCLI_DB_PATH
        $env:QUIETCLI_DB_PATH=Join-Path $TestDrive ("html-"+[Guid]::NewGuid().ToString('N')+'.sqlite3')
        $script:HtmlPath=Join-Path $TestDrive 'gain.html'
    }
    AfterEach {$env:QUIETCLI_DB_PATH=$script:PreviousHtmlDatabase}
    It 'renders measured token bars to scale and marks increased output' {
        Add-GainHtmlSample @{Command='a'}
        Add-GainHtmlSample @{Command='b';InputTokens=100;OutputTokens=250;SavedTokens=-150}
        $path=QuietCliGain -From ([DateTimeOffset]::UtcNow.AddDays(-30)) -To ([DateTimeOffset]::UtcNow.AddMinutes(1)) -GroupBy Command -View Html -OutputPath $HtmlPath
        $path|Should -Be $HtmlPath
        $html=[IO.File]::ReadAllText($path)
        $charts=@(Read-GainHtmlSvg $html)
        $charts.Count|Should -Be 1
        $bars=$charts[0].SelectNodes('//*[local-name()="rect"]')
        $bars.Count|Should -Be 4
        $bars[0].GetAttribute('data-value')|Should -Be '1000'
        $bars[0].height|Should -Be '200'
        $bars[1].height|Should -Be '40'
        $bars[2].height|Should -Be '20'
        $bars[3].height|Should -Be '50'
        $bars[3].class|Should -Be 'loss'
        $html|Should -Match "class='value positive'>650</span>"
        $html|Should -Not -Match '<script\b|<link\b|<canvas\b'
        $html|Should -Match 'aria-labelledby='
    }
    It 'keeps historical estimates and different token profiles in separate charts' {
        Add-GainHtmlSample
        Add-GainHtmlSample @{Method='estimate';Profile='utf8-bytes/4';Version='1';InputTokens=400;OutputTokens=40;SavedTokens=360}
        Add-GainHtmlSample @{Profile='cl100k_base';InputTokens=300;OutputTokens=30;SavedTokens=270}
        $null=QuietCliGain -From ([DateTimeOffset]::UtcNow.AddDays(-30)) -To ([DateTimeOffset]::UtcNow.AddMinutes(1)) -View Html -OutputPath $HtmlPath
        $html=[IO.File]::ReadAllText($HtmlPath)
        $charts=@(Read-GainHtmlSvg $html)
        $charts.Count|Should -Be 3
        foreach($chart in $charts){$chart.SelectNodes('//*[local-name()="rect"]').Count|Should -Be 2}
        $html|Should -Match "data-profile='estimate, utf8-bytes/4, 1'"
        $html|Should -Match "data-profile='exact, o200k_base, fixture/1'"
        $html|Should -Match "data-profile='exact, cl100k_base, fixture/1'"
        (QuietCliGain -From ([DateTimeOffset]::UtcNow.AddDays(-30)) -To ([DateTimeOffset]::UtcNow.AddMinutes(1)) -View Object).Totals.SavedTokens|Should -BeNullOrEmpty
    }
    It 'uses measured bytes when token counting is unavailable' {
        Add-GainHtmlSample @{Measured=$false;InputTokens=$null;OutputTokens=$null;SavedTokens=$null;InputBytes=400;OutputBytes=100}
        $null=QuietCliGain -From ([DateTimeOffset]::UtcNow.AddDays(-30)) -To ([DateTimeOffset]::UtcNow.AddMinutes(1)) -View Html -OutputPath $HtmlPath
        $html=[IO.File]::ReadAllText($HtmlPath)
        $html|Should -Match 'Token counts are unavailable'
        $chart=@(Read-GainHtmlSvg $html)[0]
        $chart.SelectSingleNode('//*[local-name()="title"]').InnerText|Should -Be 'Input and delivered bytes by Day'
        $bars=$chart.SelectNodes('//*[local-name()="rect"]')
        $bars[0].GetAttribute('data-value')|Should -Be '400'
        $bars[1].GetAttribute('data-value')|Should -Be '100'
        $html|Should -Match "class='value positive'>300</span>"
    }
    It 'explains empty and unknown measurements without drawing invented zero bars' {
        $null=QuietCliGain -From ([DateTimeOffset]::UtcNow.AddDays(-30)) -To ([DateTimeOffset]::UtcNow.AddMinutes(1)) -View Html -OutputPath $HtmlPath
        $html=[IO.File]::ReadAllText($HtmlPath)
        $html|Should -Match 'No measurements in this period'
        @(Read-GainHtmlSvg $html).Count|Should -Be 0
        Add-GainHtmlSample @{Measured=$false;ByteMeasured=$false;InputTokens=$null;OutputTokens=$null;SavedTokens=$null}
        $null=QuietCliGain -From ([DateTimeOffset]::UtcNow.AddDays(-30)) -To ([DateTimeOffset]::UtcNow.AddMinutes(1)) -View Html -OutputPath $HtmlPath
        $html=[IO.File]::ReadAllText($HtmlPath)
        $html|Should -Match 'No comparable measurements in the selected groups'
        $html|Should -Match '<td>Unavailable</td>'
        @(Read-GainHtmlSvg $html).Count|Should -Be 0
    }
    It 'escapes command labels in the HTML and SVG' {
        Add-GainHtmlSample @{Command='<img src=x onerror="alert(1)"> & quoted'}
        $null=QuietCliGain -From ([DateTimeOffset]::UtcNow.AddDays(-30)) -To ([DateTimeOffset]::UtcNow.AddMinutes(1)) -GroupBy Command -View Html -OutputPath $HtmlPath
        $html=[IO.File]::ReadAllText($HtmlPath)
        $html|Should -Not -Match '<img\b'
        $html|Should -Match '&lt;img'
        $chart=@(Read-GainHtmlSvg $html)[0]
        $chart.SelectSingleNode('//*[local-name()="rect"]/*[local-name()="title"]').InnerText|Should -Match '<img src=x'
    }
    It 'makes omitted chart groups explicit while retaining the full period totals' {
        Add-GainHtmlSample @{Command='a'}
        Add-GainHtmlSample @{Command='b';InputTokens=500;OutputTokens=100;SavedTokens=400}
        $null=QuietCliGain -From ([DateTimeOffset]::UtcNow.AddDays(-30)) -To ([DateTimeOffset]::UtcNow.AddMinutes(1)) -GroupBy Command -MaxItems 1 -View Html -OutputPath $HtmlPath
        $html=[IO.File]::ReadAllText($HtmlPath)
        $html|Should -Match '1 of 2 groups are omitted by MaxItems'
        $chart=@(Read-GainHtmlSvg $html)[0]
        $chart.SelectNodes('//*[local-name()="rect"]').Count|Should -Be 2
        $raw=[Net.WebUtility]::HtmlDecode([regex]::Match($html,'(?s)<pre>(.*?)</pre>').Groups[1].Value)|ConvertFrom-Json
        $raw.Totals.Deliveries|Should -Be 2
        $raw.Totals.InputTokens|Should -Be 1500
        $raw.Groups.Count|Should -Be 1
        $raw.OmittedGroups|Should -Be 1
    }
    It 'draws genuine zero measurements without invalid SVG coordinates' {
        Add-GainHtmlSample @{InputTokens=0;OutputTokens=0;SavedTokens=0}
        $null=QuietCliGain -From ([DateTimeOffset]::UtcNow.AddDays(-30)) -To ([DateTimeOffset]::UtcNow.AddMinutes(1)) -View Html -OutputPath $HtmlPath
        $html=[IO.File]::ReadAllText($HtmlPath)
        $chart=@(Read-GainHtmlSvg $html)[0]
        foreach($bar in $chart.SelectNodes('//*[local-name()="rect"]')){
            $bar.height|Should -Be '0'
            $bar.GetAttribute('data-value')|Should -Be '0'
        }
        $html|Should -Not -Match 'NaN|Infinity'
    }
}

Describe 'Canonical module distribution' {
    It 'keeps consumer sources separate and validates the runtime inventory without making a ZIP' {
        Test-Path (Join-Path $QuietRoot 'modules')|Should -BeFalse
        Test-Path (Join-Path $QuietRoot 'downloads')|Should -BeFalse
        $report=& (Join-Path $QuietRoot 'scripts/New-QuietCliArchive.ps1') -ValidateOnly|ConvertFrom-Json
        $report.Outcome|Should -Be Success
        $report.Data.Mode|Should -Be InventoryOnly
        @($report.Data.Manifest.Files|Where-Object {$_.Path -match '^modules/|^downloads/'}).Count|Should -Be 0
    }
    It 'refuses local release ZIP creation before writing an archive' {
        $old=$env:GITHUB_ACTIONS
        try{
            $env:GITHUB_ACTIONS='false'
            $destination=Join-Path $TestDrive 'blocked-distribution'
            {& (Join-Path $QuietRoot 'scripts/New-QuietCliArchive.ps1') -OutputDirectory $destination}|Should -Throw '*GitHub Actions*'
            Test-Path $destination|Should -BeFalse
        }finally{$env:GITHUB_ACTIONS=$old}
    }
}