Tests/Pipelines.Tests.ps1

BeforeAll {
    Import-Module (Join-Path $PSScriptRoot '..' 'Jev.psd1') -Force

    $kind = New-JevQuestion -Name kind -Type Choice -Instructions 'What kind of message is this?' -Criteria ([ordered]@{
        billing  = 'Charges and invoices.'
        delivery = 'A missing or late delivery.'
        question = 'A general question.'
    })
    $urgency = New-JevQuestion -Name urgency -Type Score -Instructions 'How urgent is this?' -Criteria @('Routine.', 'Soon.', 'Immediate.')
}

AfterAll {
    Remove-Module Jev -Force -ErrorAction SilentlyContinue
}

Describe 'Select-Jev' {
    It 'keeps the original matching object instances and types in input order' {
        Mock -ModuleName Jev Invoke-JevDecision {
            param($State, $Questions)

            $Questions.answer.instructions | Should -Be 'Keep this record?'
            $probability = if ($State -is [version]) { 0.9 } else { $State.Probability }
            [pscustomobject]@{
                answers = [pscustomobject]@{
                    answer = [pscustomobject]@{ type = 'noul'; noul = $probability }
                }
            }
        }

        $first = [pscustomobject]@{ Id = 1; Probability = 0.8 }
        $dropped = [pscustomobject]@{ Id = 2; Probability = 0.79 }
        $third = [ordered]@{ Id = 3; Probability = 0.95 }
        $fourth = [version] '4.0'
        $results = @(@($first, $dropped, $third, $fourth) | Select-Jev 'Keep this record?' 0.8)

        $results.Count | Should -Be 3
        [object]::ReferenceEquals($results[0], $first) | Should -BeTrue
        [object]::ReferenceEquals($results[1], $third) | Should -BeTrue
        [object]::ReferenceEquals($results[2], $fourth) | Should -BeTrue
        $results[2] | Should -BeOfType [version]
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 4 -Scope It
    }

    It 'accepts a positional question and includes strings at the default 0.5 cutoff' {
        Mock -ModuleName Jev Invoke-JevDecision {
            param($State)

            $probability = if ($State -eq 'Keep me.') { 0.5 } else { 0.49 }
            [pscustomobject]@{
                answers = [ordered]@{
                    answer = [pscustomobject]@{ type = 'noul'; noul = $probability }
                }
            }
        }

        $results = @(@('Drop me.', 'Keep me.') | Select-Jev 'Keep this message?')

        $results.Count | Should -Be 1
        $results[0] | Should -BeExactly 'Keep me.'
        $results[0] | Should -BeOfType [string]
    }

    It 'preserves a named array state as a single matching input' {
        Mock -ModuleName Jev Invoke-JevDecision {
            [pscustomobject]@{
                answers = [pscustomobject]@{
                    answer = [pscustomobject]@{ type = 'noul'; noul = 0.9 }
                }
            }
        }

        $state = @('First part.', 'Second part.')
        $results = @(Select-Jev -State $state -Question 'Keep this whole record?' -Threshold 0.8)

        $results.Count | Should -Be 1
        [object]::ReferenceEquals($results[0], $state) | Should -BeTrue
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 1 -Scope It
    }

    It 'reports backend failures rather than silently discarding the input' {
        Mock -ModuleName Jev Invoke-JevDecision { throw 'Backend unavailable.' }

        { 'A message.' | Select-Jev 'Keep it?' } | Should -Throw '*Backend unavailable*'
    }
}

Describe 'Add-JevAnnotation' {
    It 'adds multiple named answers and preserves input fields, collisions, and response details' {
        Mock -ModuleName Jev Invoke-JevDecision {
            param($State, $Questions)

            $Questions.Count | Should -Be 2
            $Questions.kind.type | Should -Be 'choice'
            $Questions.kind.criteria.billing | Should -Be 'Charges and invoices.'
            $Questions.urgency.type | Should -Be 'score'
            $Questions.urgency.criteria | Should -Be @('Routine.', 'Soon.', 'Immediate.')
            [pscustomobject]@{
                model = 'jev-test'
                answers = [ordered]@{
                    kind = [pscustomobject]@{ type = 'choice'; choice = 'billing'; probabilities = @{ billing = 0.95 } }
                    urgency = [pscustomobject]@{ type = 'score'; score = 1.4; probabilities = @{ '1' = 0.6; '2' = 0.4 } }
                }
                usage = [pscustomobject]@{ input_tokens = 12; output_tokens = 5 }
            }
        }

        $ticket = [pscustomobject]@{ Id = 'T-1'; Message = 'My invoice is wrong.'; kind = 'original' }
        $results = @(@($ticket, 'Please fix my invoice.') | Add-JevAnnotation -Question $kind, $urgency)

        $results.Count | Should -Be 2
        $results[0].Id | Should -Be 'T-1'
        $results[0].Message | Should -Be $ticket.Message
        $results[0].kind | Should -Be 'original'
        $results[0].Jev_kind | Should -Be 'billing'
        $results[0].urgency | Should -Be 1.4
        $results[0].model | Should -Be 'jev-test'
        $results[0].answers.kind.probabilities.billing | Should -Be 0.95
        $results[0].usage.input_tokens | Should -Be 12
        $results[1].State | Should -Be 'Please fix my invoice.'
        $results[1].kind | Should -Be 'billing'
        $results[1] | Should -BeOfType [pscustomobject]
        $ticket.kind | Should -Be 'original'
        @($ticket.PSObject.Properties.Name) | Should -Not -Contain 'urgency'
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 2 -Scope It
    }

    It 'accepts a named state and propagates request failures' {
        Mock -ModuleName Jev Invoke-JevDecision { throw 'Annotation backend unavailable.' }

        { Add-JevAnnotation -State 'A message.' -Question $kind } | Should -Throw '*Annotation backend unavailable*'
    }
}

Describe 'Selection and annotation pipeline' {
    It 'annotates only the selected records and keeps their identifying properties' {
        Mock -ModuleName Jev Invoke-JevDecision {
            param($State, $Questions)

            if ($Questions.ContainsKey('answer')) {
                [pscustomobject]@{
                    answers = [ordered]@{
                        answer = [pscustomobject]@{ type = 'noul'; noul = $(if ($State.Id -eq 2) { 0.05 } else { 0.95 }) }
                    }
                }
            }
            else {
                $State.Id | Should -Not -Be 2
                $category = switch ($State.Id) {
                    1 { 'billing' }
                    3 { 'delivery' }
                    4 { 'question' }
                }
                [pscustomobject]@{
                    answers = [ordered]@{
                        kind = [pscustomobject]@{ type = 'choice'; choice = $category }
                        urgency = [pscustomobject]@{ type = 'score'; score = $(if ($State.Id -eq 3) { 1.9 } else { 0.2 }) }
                    }
                }
            }
        }

        $messages = @(
            [pscustomobject]@{ Id = 1; Message = 'Please fix the wrong amount on my invoice.' }
            [pscustomobject]@{ Id = 2; Message = 'Just saying thanks, no reply needed.' }
            [pscustomobject]@{ Id = 3; Message = 'Order never arrived and the party is tonight.' }
            [pscustomobject]@{ Id = 4; Message = 'Do you ship to Canada?' }
        )
        $results = @($messages | Select-Jev 'Does this message need a reply?' | Add-JevAnnotation -Question $kind, $urgency)

        $results.Count | Should -Be 3
        $results.Id | Should -Be @(1, 3, 4)
        $results.kind | Should -Be @('billing', 'delivery', 'question')
        $results[1].urgency | Should -Be 1.9
        $results[1].Message | Should -Be $messages[2].Message
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 7 -Scope It
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 3 -Scope It -ParameterFilter {
            $Questions.ContainsKey('kind') -and $Questions.ContainsKey('urgency')
        }
    }
}