# snouty runs
#
# List all Antithesis runs.
#
# Assertions prefixed with [!staging] are skipped when SNOUTY_STAGING is
# set: they assert against hardcoded mock data (or behaviors that only make
# sense against the in-process mock) that does not exist on the live staging
# backend. Unprefixed assertions are structural and run in both modes.
#
# Run-id scoped read responses may be cached under
# $XDG_RUNTIME_DIR/snouty/api-cache-v1. Caching is internal and must not change
# command output or required arguments.

# The command authenticates with the Antithesis API using
#    ANTITHESIS_API_KEY and ANTITHESIS_TENANT env vars. Every endpoint other
#    than launch requires an API key, so unlike the launch commands,
#    username/password authentication is not supported.
! snouty runs
stderr 'missing environment variable: ANTITHESIS_API_KEY.*'

# With only username/password configured, the command fails fast with a clear
# message instead of sending a request the API would reject with a 403.
env ANTITHESIS_USERNAME=testuser
env ANTITHESIS_PASSWORD=testpass
env ANTITHESIS_TENANT=testtenant
! snouty runs
stderr 'missing environment variable: ANTITHESIS_API_KEY.*'
stderr 'only accepts API key authentication.*'

# On success, the command fetches all pages via next_cursor and prints a table
# with relative timestamps and short status words.
mock-runs-server
snouty runs
stdout 'RUN ID.*STATUS.*CREATED.*TEST NAME'
[!staging] stdout 'run-1.*completed'
[!staging] stdout 'run-2.*in_progress'
# The default table omits the description (it never fit usefully beside the full
# run id); use `runs list --detail` to see it.
! stdout 'DESCRIPTION'
[!staging] ! stdout 'nightly smoke on main'

# `snouty runs list --detail` prints one aligned key-value block per run (no
# table), so full descriptions are visible.
mock-runs-server
snouty runs list --detail
stdout 'Run ID .*run-1'
[!staging] stdout 'Status .*completed'
[!staging] stdout 'Description .*nightly smoke on main'
! stdout 'RUN ID.*STATUS.*CREATED.*TEST NAME'

# `-d` is the short form of `--detail`.
mock-runs-server
snouty runs list -d
stdout 'Run ID .*run-1'
! stdout 'RUN ID.*STATUS.*CREATED.*TEST NAME'

# `snouty runs list` is the same as bare `snouty runs`.
mock-runs-server
snouty runs list
stdout 'RUN ID.*STATUS.*CREATED.*TEST NAME'

# `snouty runs show` requires a run ID.
! snouty runs show
stderr '.*'

# `snouty --json runs` outputs NDJSON (one run per line) so a run id can be
#    captured for use in subsequent commands as $R_run_id. Subsequent read
#    endpoints (properties, build-logs, events) only return data once a run
#    has reached a terminal state, so list with --status completed first.
mock-runs-server
snouty --json runs list --status completed
[!staging] stdout '"run_id":"run-.*'
env_from_json 0 run_id

# `snouty runs show` displays run details in key-value format.
mock-runs-server
snouty runs show $R_run_id
stdout 'Run ID.*$R_run_id'
[!staging] stdout 'Status.*completed'
[!staging] stdout 'Launcher.*nightly'

# `snouty runs show` doesn't print the raw report URL — `snouty runs show --web`
# is the way to follow that link. The hint is shown only when a report link
# exists (mock runs all carry one), since `--web` errors without it.
mock-runs-server
snouty runs show $R_run_id
! stdout 'Report.*https'
[!staging] stdout 'snouty runs show .*--web'

# `snouty --json runs show --web` resolves the report URL without launching a
# browser, so a script can capture it. (Plain `--web` would shell out to a
# browser, so it's not exercised here.)
mock-runs-server
snouty --json runs show $R_run_id --web
[!staging] stdout '"url":.*https.*reports'

# `snouty --json runs show` outputs JSON.
mock-runs-server
snouty --json runs show $R_run_id
stdout '"run_id".*"$R_run_id"'
[!staging] stdout '"status".*"completed"'

# `snouty runs properties` renders one table per group (the group is the section
# heading), each with STATUS, the example count (counterexamples shown inline as
# `examples/counterexamples` when a property has any, SI-shortened and
# right-aligned), and the property NAME. Ungrouped properties go in a trailing
# "(ungrouped)" section.
mock-runs-server
snouty runs properties $R_run_id
[!staging] stdout 'STATUS.*EXAMPLES.*NAME'
# Counter has 12 examples and 3 counterexamples -> `12/3`.
[!staging] stdout 'failing.*12/3.*Counter value stays below limit'
[!staging] stdout 'passing.*Setup completes'

# `snouty runs properties --failing` shows only failing properties.
mock-runs-server
snouty runs properties --failing $R_run_id
[!staging] stdout 'failing.*Counter value stays below limit'
[!staging] stdout -count=0 'Setup completes'

# `snouty runs properties --passing` shows only passing properties.
mock-runs-server
snouty runs properties --passing $R_run_id
[!staging] stdout 'passing.*Setup completes'
[!staging] stdout -count=0 'Counter value stays below limit'

# `snouty runs properties --passing --failing` is rejected.
! snouty runs properties --passing --failing $R_run_id
stderr '.*cannot be used with.*'

# `snouty --json runs properties` outputs JSON.
mock-runs-server
snouty --json runs properties $R_run_id
[!staging] stdout '"name".*"Counter value stays below limit"'
[!staging] stdout '"status".*"Failing"'

# `--name <substr> --detail` expands the matching property into its examples.
# For the Counter event property that's a STATUS/HASH/VTIME table, sorted
# numerically by vtime (5.0 before 10.0), failing before passing.
mock-runs-server
snouty runs properties $R_run_id --name 'Counter value' --detail
[!staging] stdout 'STATUS.*HASH.*VTIME'
[!staging] stdout '(?s)failing.*-100.*5\.0.*failing.*-200.*10\.0.*passing.*-300.*15\.0'

# --name is a case-insensitive substring filter over property names; without
# --detail it just narrows the (grouped) table.
mock-runs-server
snouty runs properties $R_run_id --name counter
[!staging] stdout 'failing.*Counter value stays below limit'
[!staging] stdout -count=0 'Setup completes'

# --group is a case-insensitive substring group filter ('safety' matches 'Safety').
mock-runs-server
snouty runs properties $R_run_id --group safety
[!staging] stdout 'failing.*Counter value stays below limit'
[!staging] stdout -count=0 'Setup completes'

# A non-event ("system") property's --detail shows its value under a `Result`
# label (no `:`, like the other labels). A small object inlines as compact JSON
# against the value column; there's no moment table or Examples.
mock-runs-server
snouty runs properties $R_run_id --name 'Setup completes' --detail
[!staging] stdout 'Result.*final_counter'
[!staging] stdout '"final_counter".*42'

# A *failing* non-event property carries its violating value in counterexamples;
# --detail labels those under `Counter-examples` (shown first), distinct from the
# satisfying `Examples`, so the offending value isn't lost in an unlabelled list.
mock-runs-server
snouty runs properties $R_run_id --name 'Peak memory' --detail
[!staging] stdout '(?s)Counter-examples.*1340.*Examples.*820'

# A filter that matches nothing prints a friendly empty-state, not an error.
mock-runs-server
snouty runs properties $R_run_id --name no-such-property
[!staging] stdout 'No properties match.*no-such-property'

# --detail conflicts with --json (which already emits the full data), for both
# `runs properties` and `runs list`.
! snouty --json runs properties $R_run_id --detail
stderr 'detail.*json'
! snouty --json runs list --detail
stderr 'detail.*json'

# `snouty runs build-logs` streams formatted build log lines. Timestamps are
# reformatted into the local timezone without a tz suffix; TZ is pinned to UTC
# here so the rendered times are deterministic.
mock-runs-server
env TZ=UTC
snouty runs build-logs $R_run_id
[!staging] stdout '2025-03-20 02:01:12 \[stdout\] Building image payments-service\.\.\.'
[!staging] stdout '2025-03-20 02:01:15 \[stderr\] Warning: deprecated feature'
[!staging] stdout '2025-03-20 02:01:20 \[stdout\] Build complete'

# `snouty --json runs build-logs` outputs raw NDJSON.
mock-runs-server
snouty --json runs build-logs $R_run_id
[!staging] stdout '"timestamp".*"text"'

# `snouty runs events` filters by `--match` needles against the decoded output.
mock-runs-server
snouty runs events $R_run_id --match request
[!staging] stdout '^HASH\s+VTIME\s+SOURCE\s+OUTPUT'
[!staging] stdout '-456.*2\.0.*\[app:error\].*\{"level":"warn","msg":"slow request"\}'

# A second `--match` AND-narrows: every needle must be present in the line.
mock-runs-server
snouty runs events $R_run_id --match request --match slow
[!staging] stdout '-456.*2\.0.*\[app:error\].*\{"level":"warn","msg":"slow request"\}'
# A needle that isn't present excludes the event entirely.
mock-runs-server
snouty runs events $R_run_id --match starting --match nonexistent
stdout 'No events matched "starting nonexistent"\.'

# `snouty --json runs events` outputs raw NDJSON.
mock-runs-server
snouty --json runs events $R_run_id --match counter
[!staging] stdout '"antithesis_assert".*"Counter'\''s value retrieved"'
[!staging] stdout '.*"moment".*"input_hash":"-4735081784258020614".*'
[!staging] stdout '.*"moment".*"vtime":"311\.8487535319291".*'

# An incomplete run carries the failure moment (input_hash + vtime) in its detail
#    response. Filter the list with --status incomplete so we know the run we
#    inspect actually entered the incomplete terminal state, then assert that
#    the human-readable view shows "Failure VTime" / "Failure Hash" and the
#    --json view exposes failure_moment with input_hash and vtime.
mock-runs-server
snouty --json runs list --status incomplete
stdout '"run_id":.*'
stdout '"status":"incomplete".*'
env_from_json 0 run_id

mock-runs-server
snouty runs show $R_run_id
stdout 'Failure VTime.*'
stdout 'Failure Hash.*'

mock-runs-server
snouty --json runs show $R_run_id
stdout '"failure_moment".*'
stdout '"input_hash".*'
stdout '"vtime".*'

[staging] skip

# If no runs are returned, the command prints a friendly empty-state message.
mock-runs-server empty
snouty runs
stdout 'No runs found\.'

# API errors are reported with status code and body. A 4xx is the user's to
# fix, so it prints as a clean message with no backtrace/internal noise. The
# generic mock-server directive only sets username/password, which `snouty
# runs` rejects before making a request, so set an API key explicitly rather
# than relying on one leaking from an earlier mock-runs-server block.
mock-server 400 '{"message":"bad request"}'
env ANTITHESIS_API_KEY=test-api-key
! snouty runs
stderr 'API error: 400.*'
! stderr 'Backtrace omitted'

# `snouty runs properties` prints a friendly empty-state message when no properties exist.
mock-runs-server
snouty runs properties run-empty
stdout 'No properties found\.'

# `snouty runs properties` renders one row per property regardless of whether
# any examples were sampled.
mock-runs-server
snouty runs properties run-no-events
stdout 'STATUS.*EXAMPLES.*NAME'
stdout 'passing.*No events property'

# A bad run id gives the same friendly "run not found" message across every
# run-scoped subcommand, instead of leaking a raw "API error: 404 Not Found".
# The run-scoped endpoints 404 for an unknown run; snouty probes the run and
# reports it as missing rather than passing the bare API error through.
mock-runs-server
! snouty runs show no-such-run
stderr '.*run not found: no-such-run.*'
! stderr '.*API error.*'

mock-runs-server
! snouty runs properties no-such-run
stderr '.*run not found: no-such-run.*'
! stderr '.*API error.*'

mock-runs-server
! snouty runs build-logs no-such-run
stderr '.*run not found: no-such-run.*'
! stderr '.*API error.*'

# A successful but empty build-logs stream prints an explicit note in human
# mode, so the user isn't left wondering whether anything happened.
mock-runs-server
snouty runs build-logs run-empty
stderr 'No build logs for this run\.'

# In --json mode an empty stream is the correct machine answer, so no note.
mock-runs-server
snouty --json runs build-logs run-empty
! stderr 'No build logs for this run\.'

mock-runs-server
! snouty runs events no-such-run --match anything
stderr '.*run not found: no-such-run.*'
! stderr '.*API error.*'

mock-runs-server
! snouty runs logs no-such-run -123 1.0
stderr '.*run not found: no-such-run.*'
! stderr '.*API error.*'

# A real but incomplete run has no properties yet: explain that they're
# generated on completion instead of reporting the run as missing. run-3 is the
# incomplete fixture; its properties endpoint 404s like the real API.
mock-runs-server
! snouty runs properties run-3
stderr '.*no properties for run run-3.*'
stderr '.*this run is incomplete.*'
! stderr '.*run not found.*'

# `snouty runs properties` requires a run ID.
! snouty runs properties
stderr '.*'

# `snouty runs logs` matches the web UI download format: `[vtime] [source]
# [stream] payload`. Text records (those with `output_text`) sit one space
# after the stream bracket; JSON records get a " - " separator and have the
# moment/source/IPT_bytes_out envelope stripped.
mock-runs-server
snouty runs logs run-1 -123 1.0
stdout '\[\s*1\.000\] \[\s*app\] \[out\] \{"level":"info","msg":"starting"\}'
stdout '\[\s*2\.000\] \[\s*app\] \[err\] \{"level":"warn","msg":"slow request"\}'
stdout '\[\s*311\.848\] \[\s*control\] \[\s*\] +- \{"antithesis_assert"'

# `snouty --json runs logs` outputs NDJSON with faults annotated.
mock-runs-server
snouty --json runs logs run-1 -123 1.0
stdout 'output_text.*starting'
stdout 'output_text.*slow request'
stdout '"antithesis_assert".*"display_type":"AlwaysOrUnreachable"'
# moment.vtime is converted in place from the server's seconds string to an
# f64; active_faults is appended by the annotator. The clog window (start 401.5,
# max_duration 0.267) is active at 401.5 and 401.75, then expired by 402.
stdout '"vtime":401.5.*"active_faults":\{"network_clog":\{"vtime":401.5\}\}'
stdout '"vtime":401.75.*"active_faults":\{"network_clog":\{"vtime":401.5\}\}'
stdout '"vtime":402.0.*"active_faults":\{\}'

# `--raw` disables post-processing: NDJSON passes through without fault
# annotation.
mock-runs-server
snouty runs logs --json --raw run-1 -123 1.0
stdout 'output_text.*starting'
! stdout 'active_faults'

# `--raw` in human mode renders the text payload verbatim (no ANSI stripping
# or control-byte escaping); the line prefix is still formatted.
mock-runs-server
snouty runs logs --raw run-1 -123 1.0
stdout '\[\s*1\.000\] \[\s*app\] \[out\] \{"level":"info","msg":"starting"\}'

# `snouty --json runs logs` emits a record whose output_text contains
# a JSON-escaped newline on a single output line.
mock-runs-server
snouty --json runs logs run-1 -123 1.0
stdout -count=1 'line one\\nline two'

# `snouty runs logs` requires hash and vtime positional arguments.
mock-runs-server
! snouty runs logs run-1
stderr '.*INPUT_HASH.*'

# `snouty runs events` prints a friendly message (and no table header) when no
# events match, instead of an empty table.
mock-runs-server
snouty runs events run-1 --match nomatch
stdout 'No events matched "nomatch"\.'
! stdout '^HASH\s+VTIME\s+SOURCE\s+OUTPUT'

# `snouty runs events` requires at least one search needle.
mock-runs-server
! snouty runs events run-1
stderr '.*no search term given.*'

# `snouty runs events` rejects an empty needle rather than matching every line.
mock-runs-server
! snouty runs events run-1 --match ''
stderr '.*empty search term.*'

# `snouty runs events` also accepts a backward-compatible positional query
# whose terms are treated as additional needles.
mock-runs-server
snouty runs events $R_run_id request
[!staging] stdout '-456.*2\.0.*\[app:error\].*\{"level":"warn","msg":"slow request"\}'
