test/qrun.tests.ps1

BeforeAll {
    Get-Module ConfigMap -ErrorAction SilentlyContinue | Remove-Module
    Import-Module $PSScriptRoot\..\configmap.psm1 -Force
}

Describe 'qrun process runner' {
    BeforeEach {
        $script:workDir = Join-Path $TestDrive ("qrun-" + [guid]::NewGuid().ToString('n'))
        New-Item -ItemType Directory -Path $script:workDir -Force | Out-Null
        Push-Location $script:workDir

        $script:map = [ordered]@{
            setup = [ordered]@{
                description = 'test process'
                'step-a'    = {
                    New-Item -ItemType Directory -Path 'marker-a' -Force | Out-Null
                    'a' | Set-Content (Join-Path 'marker-a' 'done.txt')
                }
                'step-b'    = {
                    if (!(Test-Path 'marker-a/done.txt')) { throw 'step-a missing' }
                    'b' | Set-Content 'marker-b.txt'
                }
                'step-c'    = {
                    'c' | Set-Content 'marker-c.txt'
                }
            }
        }
    }

    AfterEach {
        Pop-Location
    }

    It 'runs all steps in order and persists success state' {
        qrun -map $script:map setup

        Test-Path 'marker-a/done.txt' | Should -BeTrue
        Test-Path 'marker-b.txt' | Should -BeTrue
        Test-Path 'marker-c.txt' | Should -BeTrue

        $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json')
        $stateFiles.Count | Should -Be 1
        $state = Get-Content $stateFiles[0].FullName -Raw | ConvertFrom-Json
        $state.steps.'step-a'.status | Should -Be 'Succeeded'
        $state.steps.'step-b'.status | Should -Be 'Succeeded'
        $state.steps.'step-c'.status | Should -Be 'Succeeded'
    }

    It 'skips already succeeded steps on re-run' {
        qrun -map $script:map setup
        Remove-Item 'marker-b.txt', 'marker-c.txt' -Force

        qrun -map $script:map setup

        Test-Path 'marker-b.txt' | Should -BeFalse
        Test-Path 'marker-c.txt' | Should -BeFalse
        Test-Path 'marker-a/done.txt' | Should -BeTrue
    }

    It 'reruns succeeded steps when -Force is specified' {
        qrun -map $script:map setup
        Remove-Item 'marker-b.txt' -Force

        qrun -map $script:map setup -Force

        Test-Path 'marker-b.txt' | Should -BeTrue
        Test-Path 'marker-c.txt' | Should -BeTrue
    }

    It 'runs a single step via process.step path and respects skip rules' {
        qrun -map $script:map setup.step-a
        Test-Path 'marker-a/done.txt' | Should -BeTrue
        Test-Path 'marker-b.txt' | Should -BeFalse

        qrun -map $script:map setup.step-a
        Test-Path 'marker-b.txt' | Should -BeFalse

        qrun -map $script:map setup.step-b
        Test-Path 'marker-b.txt' | Should -BeTrue
        Test-Path 'marker-c.txt' | Should -BeFalse
    }

    It 'treats process.all as the full process' {
        qrun -map $script:map setup.all
        Test-Path 'marker-a/done.txt' | Should -BeTrue
        Test-Path 'marker-b.txt' | Should -BeTrue
        Test-Path 'marker-c.txt' | Should -BeTrue
    }

    It 'runs from a step onward with -Follow' {
        qrun -map $script:map setup.step-a

        qrun -map $script:map setup.step-b -Follow

        Test-Path 'marker-b.txt' | Should -BeTrue
        Test-Path 'marker-c.txt' | Should -BeTrue
    }

    It 'rejects -Follow without a step path' {
        { qrun -map $script:map setup -Follow } | Should -Throw '*-Follow requires a step path*'
    }

    It 'blocks starting mid-process when prior steps are incomplete' {
        { qrun -map $script:map setup.step-b } | Should -Throw '*prior steps are incomplete*'
        Test-Path 'marker-b.txt' | Should -BeFalse
    }

    It 'allows skipping incomplete prior steps with -Force' {
        # independent steps (no artifact coupling) so -Force only exercises the runner gate
        $indep = [ordered]@{
            setup = [ordered]@{
                a = { 'a' | Set-Content 'a.txt' }
                b = { 'b' | Set-Content 'b.txt' }
                c = { 'c' | Set-Content 'c.txt' }
            }
        }

        qrun -map $indep setup.b -Force
        Test-Path 'b.txt' | Should -BeTrue
        Test-Path 'a.txt' | Should -BeFalse
    }

    It 'allows -Follow mid-process with -Force when priors are incomplete' {
        $indep = [ordered]@{
            setup = [ordered]@{
                a = { 'a' | Set-Content 'a.txt' }
                b = { 'b' | Set-Content 'b.txt' }
                c = { 'c' | Set-Content 'c.txt' }
            }
        }

        qrun -map $indep setup.b -Follow -Force
        Test-Path 'b.txt' | Should -BeTrue
        Test-Path 'c.txt' | Should -BeTrue
        Test-Path 'a.txt' | Should -BeFalse
    }

    It 'records failure and resumes from the failed step' {
        $failMap = [ordered]@{
            setup = [ordered]@{
                ok   = { 'ok' | Set-Content 'ok.txt' }
                boom = { throw 'boom-error' }
                after = { 'after' | Set-Content 'after.txt' }
            }
        }

        { qrun -map $failMap setup } | Should -Throw '*boom-error*'
        Test-Path 'ok.txt' | Should -BeTrue
        Test-Path 'after.txt' | Should -BeFalse

        $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json')
        $state = Get-Content $stateFiles[0].FullName -Raw | ConvertFrom-Json
        $state.steps.ok.status | Should -Be 'Succeeded'
        $state.steps.boom.status | Should -Be 'Failed'
        $state.steps.boom.error | Should -Match 'boom-error'

        # fix boom and resume
        $failMap.setup.boom = { 'fixed' | Set-Content 'boom.txt' }
        qrun -map $failMap setup

        Test-Path 'boom.txt' | Should -BeTrue
        Test-Path 'after.txt' | Should -BeTrue
    }

    It 'resets state with -Reset' {
        qrun -map $script:map setup
        qrun -map $script:map setup -Reset

        $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json' -ErrorAction SilentlyContinue)
        $stateFiles.Count | Should -Be 0

        Remove-Item 'marker-b.txt', 'marker-c.txt' -Force
        qrun -map $script:map setup
        Test-Path 'marker-b.txt' | Should -BeTrue
    }

    It 'shows status with -Status' {
        qrun -map $script:map setup.step-a
        { qrun -map $script:map setup -Status } | Should -Not -Throw
    }

    It 'shows state with !state and !state show' {
        # list-like: all processes/steps appear even before any run
        { qrun -map $script:map '!state' } | Should -Not -Throw

        qrun -map $script:map setup.step-a
        { qrun -map $script:map '!state' show } | Should -Not -Throw
        { qrun -map $script:map '!state' show setup } | Should -Not -Throw
    }

    It 'treats bare qrun as !state show' {
        InModuleScope ConfigMap {
            param($Map)
            Mock Write-Host
            Invoke-QRun -map $Map
            Should -Invoke Write-Host -ParameterFilter { $Object -eq 'PROCESSES:' -and $ForegroundColor -eq 'Yellow' }
            Should -Invoke Write-Host -ParameterFilter { $Object -eq 'Process state' }
        } -Parameters @{ Map = $script:map }
    }

    It 'cleans state with !state clean' {
        qrun -map $script:map setup
        $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json')
        $stateFiles.Count | Should -Be 1

        qrun -map $script:map '!state' clean setup
        $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json' -ErrorAction SilentlyContinue)
        $stateFiles.Count | Should -Be 0
    }

    It 'cleans all state with !state clean' {
        qrun -map $script:map setup
        qrun -map $script:map '!state' clean
        $stateRoot = Join-Path $script:workDir '.configmap' 'process'
        if (Test-Path $stateRoot) {
            @(Get-ChildItem -Path $stateRoot -Recurse -Filter '*.state.json' -ErrorAction SilentlyContinue).Count | Should -Be 0
        }
    }

    It 'supports exec list ordering' {
        $log = [System.Collections.Generic.List[string]]::new()
        $execMap = [ordered]@{
            setup = [ordered]@{
                exec = @('second', 'first')
                second = { $log.Add('second') }
                first  = { $log.Add('first') }
            }
        }

        qrun -map $execMap setup
        $log | Should -Be @('second', 'first')
    }

    It 'creates a sample process map with !init' {
        $mapFile = Join-Path $script:workDir '.process.map.ps1'
        qrun -map $mapFile '!init'
        Test-Path $mapFile | Should -BeTrue
        (Get-Content $mapFile -Raw) | Should -Match 'setup'
    }
}