Merge v3-t105: host/tests/e2e-real.nu, real end-to-end check with the package and real models (T105, D71)
ci / check (push) Successful in 1h42m55s

This commit is contained in:
2026-11-12 20:39:49 +00:00
2 changed files with 213 additions and 0 deletions
+15
View File
@@ -63,6 +63,21 @@ Coreutils, diffutils, procps, and brush use relative symlinks to one binary each
Sandbox-incompatible tests print an explicit skip reason instead of a flake-level filter.
The check sets `SAUSAGE_NIX_CHECK=1` for tests that need network access or unvendored fixture dependencies.
## Real end-to-end check
Before you report packaging, config, or model changes as complete, run this check outside the Nix sandbox.
Use the required build slot wrapper and approved real provider keys.
```sh
nu --no-config-file host/tests/e2e-real.nu /path/to/config.yaml
```
The script builds the package and tests a fresh world, real models, a shell tool, and browser chat.
It copies only the supplied config's providers and never changes the source world.
It uses a free loopback port, not port 8080.
A failure retains a private temporary folder with provider keys. Treat that folder as sensitive.
The result table reports times in seconds.
## Tests
Host integration tests share one executable:
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env nu
# Check the installed package, its web UI, real roster models, and a real tool call.
def check [name: string, action: closure] {
let start = date now
try {
let value = (do $action)
{step: $name, result: PASS, time: ((date now) - $start), detail: '', value: $value}
} catch {|error|
{step: $name, result: FAIL, time: ((date now) - $start), detail: $error.msg, value: null}
}
}
def require [condition: bool, message: string] {
if not $condition { error make {msg: $message} }
}
def api [base: string, route: string, body?: record] {
if $body == null {
http get --max-time 10sec $'($base)/api/harness/($route)'
} else {
http post --max-time 10sec --content-type application/json $'($base)/api/harness/($route)' ($body | to json --raw)
}
}
def entries [base: string, id: int, after: int] {
api $base $'conversations/($id)/entries?after=($after)' | get entries
}
def answer [base: string, id: int, after: int, directory: path, expected: string] {
let deadline = (date now) + 180sec
loop {
let found = (entries $base $id $after | where conversationId == $id)
$found | to json | save --force ($directory | path join $'conversation-($id)-after-($after).json')
let messages = ($found | where kind == 'pi.assistant' | get model | flatten)
let errors = ($messages | where {|message| ($message.stopReason? | default '') == 'error' })
if not ($errors | is-empty) {
error make {msg: $'model_error: ($errors | first | get errorMessage)'}
}
let conversation = (api $base conversations | get conversations | where id == $id | first)
let inputs = ($found | where kind == 'pi.user' | where {|entry| $entry.model | any {|message| $message.content == $expected } })
let answered = if ($inputs | is-empty) { false } else {
let input_id = ($inputs | first | get id)
let replies = ($found | where id > $input_id | where kind == 'pi.assistant' | get model | flatten)
$replies | any {|message| ($message.stopReason? | default '') == stop and ($message.content | any {|part| $part.type == text and ($part.text? | default '' | str trim | is-not-empty) }) }
}
if not $conversation.running and $answered {
return $found
}
require ((date now) < $deadline) $'No answer from conversation ($id) within 180 seconds'
}
}
def submit [base: string, id: int, content: string, directory: path] {
let previous = (entries $base $id 0)
let after = ($previous | get id | append 0 | math max)
api $base $'conversations/($id)/submit' {content: $content, requestId: (random uuid)} | ignore
answer $base $id $after $directory $content
}
# The caller supplies one config file. Its provider values never enter the report.
def main [config: path] {
hide-env --ignore-errors SAUSAGE_USERLAND SAUSAGE_PRECOMPILED SAUSAGE_NIX_CHECK
mut rows = []
let directory = (^mktemp -d -t sausage-e2e-real.XXXXXX | str trim)
let build = (check 'nix build' {
let result = (^nix build --max-jobs auto --cores 0 | complete)
require ($result.exit_code == 0) $'nix build failed: ($result.stderr)'
})
$rows = ($rows | append $build)
if $build.result == FAIL {
print ($rows | reject value)
print $'Failure evidence: ($directory)'
exit 1
}
let binary = ($env.PWD | path join result bin sausage)
let world = ($directory | path join world)
let root = ($world | path join world)
let session = $'sausage-e2e-(random uuid)'
let init = (check 'fresh init' {
let result = (^$binary init $world | complete)
require ($result.exit_code == 0) 'Packaged init failed'
})
$rows = ($rows | append $init)
let setup = (check 'providers and loopback port' {
let providers = (try { open $config | get providers } catch { error make {msg: 'Cannot read the supplied providers section'} })
let destination = ($root | path join etc sausage config.yaml)
open $destination | upsert providers $providers | to yaml | save --force $destination
let listen = (port 20000 60000)
let service = ($root | path join etc systemd system web.service)
open --raw $service | str replace -r '(?m)^ExecStart=.*$' $'ExecStart=/bin/web.wasm --listen 127.0.0.1:($listen)' | save --force $service
$'http://127.0.0.1:($listen)'
})
$rows = ($rows | append $setup)
if $init.result == FAIL or $setup.result == FAIL {
print ($rows | reject value)
print $'Failure evidence: ($directory)'
exit 1
}
let base = $setup.value
let job = (job spawn {
'' | ^bash -c 'echo $$ > "$1"; exec "$2" run "$3"' e2e ($directory | path join server.pid) $binary $world out> ($directory | path join server.stdout) err> ($directory | path join server.stderr)
})
let ready = (check 'server readiness' {
let deadline = (date now) + 60sec
loop {
let response = (try { http get --allow-errors --full --max-time 1sec $'($base)/api/whoami' } catch { null })
if $response != null and $response.status == 200 { break }
require ((date now) < $deadline) 'The web server did not answer within 60 seconds'
}
})
$rows = ($rows | append $ready)
if $ready.result == PASS {
$rows = ($rows | append (check 'GET / chat page' {
let response = (http get --allow-errors --full --raw --max-time 10sec $base)
require ($response.status == 200) $'GET / returned ($response.status), expected 200'
require ($response.body | str contains 'id="compose"') 'GET / did not contain the chat page'
}))
$rows = ($rows | append (check 'GET /api/whoami' {
let response = (http get --allow-errors --full --max-time 10sec $'($base)/api/whoami')
require ($response.status == 200) $'whoami returned ($response.status)'
}))
let main = (check 'main agent answer' {
let main = (api $base conversations | get main_agent)
let roster = (glob ($root | path join etc sausage roster '*.md') | each {|file|
let front = (open --raw $file | split row '---' | get 1 | from yaml)
{name: ($file | path basename | str replace '.md' ''), model: $front.model}
})
require (not ($roster | any {|agent| $agent.model | str starts-with 'openrouter/' })) 'OpenRouter calls are not approved'
let current = ($roster | where name == $main.name | first)
let others = ($roster | where model != $current.model | uniq-by model)
let request = if ($others | is-empty) { 'Reply with E2E_OK only. Do not use tools.' } else {
let names = ($others | get name | str join ', ')
$'This is an authorized end-to-end smoke check. Use branch with repo /src/sausage and isolated false. Ask the coordinator to use its subagent tool to start exactly one agent for each of these roster names: ($names). Each must reply E2E_OK only without tools. Use isolate none for every subagent.
Do not inspect files. Do not diagnose failures. Do not change configuration. Do not run checks. Forward these restrictions to every child. Report completion briefly.'
}
let result = (submit $base $main.conversationId $request $directory)
{id: $main.conversationId, others: $others, entries: $result}
})
$rows = ($rows | append $main)
if $main.result == PASS {
for agent in $main.value.others {
$rows = ($rows | append (check $'model ($agent.model) via @($agent.name)' {
let deadline = (date now) + 180sec
mut id = -1
loop {
let conversations = (api $base conversations | get conversations)
let matches = ($conversations | where {|c|
($c.agent?.model?.provider? | default '') + '/' + ($c.agent?.model?.modelId? | default '') == $agent.model
})
if not ($matches | is-empty) { $id = ($matches | first | get id); break }
require ((date now) < $deadline) $'No conversation for ($agent.model)'
}
submit $base $id 'Reply E2E_OK only. Do not use tools.' $directory | ignore
}))
}
$rows = ($rows | append (check 'shell tool nonce' {
let nonce = $'e2e-tool-(random uuid)'
let result = (submit $base $main.value.id $'Use bash to run exactly: printf "($nonce)\\n". Do not merely repeat the nonce. Then answer done.' $directory)
require ($result | any {|entry| $entry.kind == 'pi.tool-result' and not ($entry | to json --raw | str contains '"isError":true') and ($entry | to json --raw | str contains $nonce) }) 'No tool result contained the random nonce'
}))
} else {
let main_id = (try { api $base conversations | get main_agent.conversationId } catch { null })
if $main_id != null { try { api $base $'conversations/($main_id)/stop' {} | ignore } }
$rows = ($rows | append {step: 'other roster models and shell nonce', result: FAIL, time: 0sec, detail: 'Blocked by the main model failure', value: null})
}
let page_ok = (($rows | where step == 'GET / chat page' | first | get result) == PASS)
$rows = ($rows | append (check 'browser message and reply' {
require $page_ok 'The chat page is unavailable'
let nonce = $'e2e-browser-(random uuid)'
let condition = (r#'Array.from(document.querySelectorAll(".entry:not(.user) .markdown")).some(e=>e.textContent.includes("TOKEN"))'# | str replace TOKEN $nonce)
for args in [[open $base] [wait '#input'] [fill '#input' $'Reply only ($nonce), without tools.'] [click '#compose button:first-of-type'] [wait --fn $condition]] {
let result = (with-env {AGENT_BROWSER_DEFAULT_TIMEOUT: '180000'} { ^agent-browser --session $session ...$args | complete })
require ($result.exit_code == 0) $'Browser check failed: ($result.stderr)'
}
}))
}
$rows = ($rows | append (check 'stop server' {
let pid = (open --raw ($directory | path join server.pid) | str trim | into int)
^kill -INT $pid
let deadline = (date now) + 15sec
loop {
if (ps | where pid == $pid | is-empty) { break }
require ((date now) < $deadline) 'The server did not stop within 15 seconds'
}
}))
try { job kill $job }
try { ^agent-browser --session $session close | complete | ignore }
let table = ($rows | reject value | update time {|row| ($row.time / 1sec) | math round --precision 3 })
$table | to json | save --force ($directory | path join results.json)
print ($table | table --expand --width 180)
if ($rows | any {|row| $row.result == FAIL }) {
print $'Failure evidence: ($directory)'
exit 1
}
rm --recursive $directory
}