test: replace mock smoke matrix with a live-only drift check

The 43-case mock smoke duplicated tests/cli.rs, which already runs the
real binary against fixture providers. Replace the flake's smoke-mock
check with a small packaged-cli check (version JSON, bash completions,
one mock quote), and cut scripts/live-smoke.sh from 672 to ~170 lines:
it now only runs shipped commands against live MSN/Yahoo/IDX and
validates output shape. Rewrite docs/SMOKE.md around the Nix gate and
the drift check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hermes 2026-10-14 07:45:51 +00:00
commit 86c108f6eb
7 changed files with 188 additions and 771 deletions

View file

@ -60,13 +60,13 @@ curl -fsSL https://crates.io/api/v1/crates/idx-cli
For a real release or release-hygiene fix, verify the repo state first.
Run the canonical gate (fmt, clippy, tests, release build, `cargo package`
verification, and mock smoke):
verification, and a packaged-binary check):
```bash
nix flake check -L
```
Run targeted smoke when the release changes user-facing CLI behavior. For
Run `scripts/live-smoke.sh` when the release changes provider-facing behavior. For
ownership-sync changes, re-verify a clean-machine flow:
```bash

View file

@ -21,7 +21,7 @@ Docs:
- `docs/ARCHITECTURE.md`: providers, ownership ingest/discovery, error and output contract
- `docs/CONVENTIONS.md`: types, naming, testing, and git rules
- `docs/OWNERSHIP_SYNC.md` / `docs/OWNERSHIP_PUBLISH.md`: snapshot consumer contract / maintainer publishing
- `docs/SMOKE.md`: smoke runner and Nix check coverage
- `docs/SMOKE.md`: Nix checks, test layout, and the live drift check
- `ROADMAP.md`: open work
## Rules
@ -36,7 +36,7 @@ Docs:
## Verify
```bash
nix flake check -L # canonical gate (fmt, clippy, tests, package, smoke)
nix flake check -L # canonical gate (fmt, clippy, tests, package)
nix develop --command cargo test <name> # focused loop
scripts/live-smoke.sh --mode full # opt-in live providers
scripts/live-smoke.sh # opt-in live provider drift check
```

View file

@ -146,7 +146,7 @@ Precedence order:
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): providers, ownership ingest, and discovery
- [docs/OWNERSHIP_SYNC.md](docs/OWNERSHIP_SYNC.md): snapshot manifest and sync rules
- [docs/OWNERSHIP_PUBLISH.md](docs/OWNERSHIP_PUBLISH.md): maintainer snapshot publishing
- [docs/SMOKE.md](docs/SMOKE.md): smoke tests against live providers
- [docs/SMOKE.md](docs/SMOKE.md): test layout and the live provider drift check
- [ROADMAP.md](ROADMAP.md): planned work
## Development
@ -157,10 +157,10 @@ The canonical local and CI verification command is:
nix flake check -L
```
It covers Rust formatting, Clippy, tests, crate package verification, release
builds, offline CLI smoke, publisher regressions, installer tests with `sh` and
`dash`, and ShellCheck for installer/smoke scripts. The checks use pinned tools and cached Nix
outputs; an unchanged successful check is reused rather than rerun.
It covers Rust formatting, Clippy, unit and CLI integration tests, crate
package verification, the packaged binary, publisher regressions, installer
tests with `sh` and `dash`, and ShellCheck. The checks use pinned tools and
cached Nix outputs; an unchanged successful check is reused rather than rerun.
For a focused edit/debug loop, use the same pinned toolchain:
@ -172,10 +172,10 @@ Configured Prek hooks keep whitespace/conflict checks at pre-commit and run
`nix flake check -L` at pre-push. There is no separate full Cargo test/lint gate
to repeat after a successful flake check.
CI additionally checks the installer with native macOS tools for portability.
Live-provider smoke remains opt-in; see [docs/SMOKE.md](docs/SMOKE.md) for check
selection and caching details. The npm distribution harness remains available
separately as `scripts/npm-smoke.sh`.
CI additionally checks the installer with native macOS tools. The live
provider drift check (`scripts/live-smoke.sh`) is opt-in; see
[docs/SMOKE.md](docs/SMOKE.md). The npm wrapper has its own harness,
`scripts/npm-smoke.sh`.
## License

View file

@ -119,7 +119,7 @@ let conn = Connection::open(path)
- **Unit tests:** retain precise parser, normalization, calculation, and boundary regressions; avoid pinning incidental wording, enum counts, or internal wiring.
- **Integration tests:** prefer CLI workflows, real temporary/in-memory SQLite, and deterministic local provider fixtures for user-visible contracts and state transitions.
- **Output contracts:** parse JSON from stdout on success and stderr on failure; errors must leave stdout empty. Assert meaningful values, types, and stable error codes rather than substrings or diagnostic wording.
- **Stateful workflows:** compare cached data across offline/failure transitions, and verify failed snapshot updates preserve both database bytes and query results. Keep packaged-binary smoke representative; put exhaustive boundaries in integration tests.
- **Stateful workflows:** compare cached data across offline/failure transitions, and verify failed snapshot updates preserve both database bytes and query results. Put behavioral coverage in `tests/cli.rs`, not shell smoke.
- **Fixtures:** `tests/fixtures/*.json` — real API responses, sanitized
- **No live API calls in CI** — `IDX_USE_MOCK_PROVIDER=1`
- **Test naming:** `test_<function>_<scenario>` (e.g., `test_parse_id_number_with_dots`)

View file

@ -1,135 +1,55 @@
# Smoke Checks
# Testing and Smoke Checks
Use `scripts/live-smoke.sh` as the reusable smoke runner for shipped CLI surfaces.
## Automated gate
The script:
- builds `target/debug/idx` once by default
- isolates config, cache, and data under `tmp/live-smoke/<timestamp>/`
- records separate `.stdout` and `.stderr` files per case under `tmp/live-smoke/<timestamp>/logs/`, plus `.validation` diagnostics for JSON checks
- supports fast mock-backed runs and slower live-network passes; `jq` is required and provided by `nix develop`
`nix flake check -L` is the canonical local, pre-push, and CI gate. It runs
offline with pinned tools:
## Common Runs
| Check | Covers |
| --- | --- |
| `fmt`, `clippy` | formatting and lints (`--all-targets`) |
| `test` | unit/module tests and `tests/cli.rs` CLI integration tests |
| `default`, `package` | release build and `cargo package` verification |
| `packaged-cli` | the wrapped Nix binary starts, emits JSON, and generates completions |
| `ownership-publisher`, `publisher-test` | packaged publisher entry point and its regression scenarios |
| `install-script-sh`, `install-script-dash`, `shellcheck` | installer regressions and shell lint |
```bash
scripts/live-smoke.sh
scripts/live-smoke.sh --mode full
scripts/live-smoke.sh --mode mock
scripts/live-smoke.sh --group live-table --group live-json
scripts/live-smoke.sh --group live-nonfinite
scripts/live-smoke.sh --group cache --symbol BBRI
scripts/live-smoke.sh --dry-run --mode full
scripts/live-smoke.sh --bin ./tmp/release-install/bin/idx --no-build --mode mock
scripts/audit-msn-fundamentals.sh --tickers BUMI,ADRO,AIMS
```
`tests/cli.rs` is the behavioral suite: it runs the real `idx` binary against
fixture-backed mock providers (`IDX_USE_MOCK_PROVIDER=1`) and covers output
contracts, cache/offline transitions, provider routing, error codes, ownership
imports and queries, and snapshot sync.
## Nix CI Checks
`nix flake check -L` is the canonical local, pre-push, and Linux CI gate. Its
eleven checks cover:
- Rust formatting, Clippy (`--all-targets`), and all unit/CLI tests
- the optimized application build and verified crate packaging
- the packaged ownership publisher's `--help` entry point
- the offline mock CLI smoke matrix
- publisher regression scenarios, including the SQLite artifact builder
- installer regressions under `sh` and `dash`, plus installer and smoke-runner ShellCheck
All check tools are pinned by `flake.lock`. Installer fixtures use resolved
shell paths, and generated publisher stubs use the running Bash interpreter,
so the regression suites also work inside the Nix sandbox.
Crane shares dev-profile dependency artifacts between tests, Clippy, and
`cargo package` verification; the shipped application uses a separate release
dependency cache. Package verification still builds the packaged crate.
Integration-test edits do not invalidate the application build, and script
checks consume only their relevant source files. README/license edits affect
the packaging check, not the Rust test/application derivations.
CI cache keys include platform, dependency/toolchain inputs, and checked source
contents. Source changes can therefore save new outputs; restore prefixes first
reuse the same dependency/toolchain cache, then another cache for the platform.
The GitHub cache is opportunistic and subject to eviction, not a shared public
binary cache. Obsolete PR runs are cancelled; main-branch runs are not.
Use individual checks when investigating a failure (replace the system as
needed), or targeted Cargo commands inside the pinned development shell:
For a focused loop, run one check or filter Cargo tests:
```bash
nix build .#checks.x86_64-linux.publisher-test -L
nix develop --command cargo test full_screener
```
The native macOS installer job remains separate to exercise BSD/macOS tools.
Live-network smoke is deliberately outside the sandboxed offline gate.
CI cache keys include platform, dependency/toolchain inputs, and checked source
contents. The native macOS installer job runs separately to exercise BSD tools.
For a live full-list screener check, use an isolated config/cache and run:
## Live drift check
Fixtures cannot tell when MSN, Yahoo, or IDX change their responses.
`scripts/live-smoke.sh` runs shipped commands against the real upstreams and
validates each JSON output shape. It is opt-in and never part of the gate.
```bash
idx -o json stocks screen --region us --filter top-performers --limit 600
idx --offline -o json stocks screen --region us --filter top-performers --limit 3
idx --offline -o json stocks screen --region us --filter top-performers --limit 25
nix develop --command scripts/live-smoke.sh # stocks + ownership
nix develop --command scripts/live-smoke.sh --group stocks --symbol BBRI
nix develop --command scripts/live-smoke.sh --bin ./result/bin/idx
scripts/live-smoke.sh --dry-run
```
The first command should not stop at the old 500-row cap when MSN reports more
candidates. The two offline results must match prefixes of the same cached full
list. Provider counts and prices vary; live HTTP failures are not offline-test
failures.
- `stocks`: every `stocks` command for one ticker, plus `compare BUMI,ADRO,AIMS`,
whose MSN key ratios contain non-finite values.
- `ownership`: `discover`, then imports the newest supported report into a temp
DB and lists `releases`. IDX sits behind Cloudflare and returns `403` to many
datacenter IPs, including GitHub-hosted runners; run it from a network IDX accepts.
## Coverage Boundaries
Each run isolates config, cache, and data under `tmp/live-smoke/<timestamp>/`
and keeps per-case `.stdout`, `.stderr`, and `.validation` logs there.
The mock preset has 43 cases: 7 general, 19 stock, 9 cache, 2 routing, 4 error,
and 2 empty/unsupported ownership checks. This is a packaged-binary safety net,
not a claim that every important behavior is covered by shell smoke.
- All fifteen stock commands run in JSON mode with structural and fixture-value
assertions. Four representative table renders check displayed data.
- JSON checks require exactly one document in the correct stream. Failed
commands must leave stdout empty and expose the expected stable error code.
- Cache scenarios use zero TTL, force provider failures, and compare parsed
offline/stale results with the warmed data; diagnostics must stay on stderr.
- Redundant table launches, duplicate routing cases, and repeated setup checks
are omitted rather than counted as extra correctness coverage.
The CLI integration suite carries deeper workflows under the same Nix gate:
cache misses, provider isolation, filtered reports, non-finite values,
XLSX/ZIP import and ownership queries, snapshot install/no-op/force, and rejected
updates preserving database bytes and query results.
Screener regressions generate 503 distinct candidates, with ranking leaders
beyond the first 500. They exercise top/worst performers, volume, and market-cap
ordering, a small first request followed by complete offline reads, and rejected
missing/incomplete totals without cache poisoning. The raw fixture override
`IDX_MOCK_MSN_SCREENER_FIXTURE` is honored only with `IDX_USE_MOCK_PROVIDER`;
responses honor the requested batch size so the complete-fetch path is exercised.
Mocks do not verify current upstream availability, authentication, or response
drift. Live provider checks remain a separate, opt-in operation.
## Modes
- `live` runs the default real-network baseline: `general`, `live-table`, and `ownership`
- `mock` runs the deterministic baseline: `general`, `mock`, `cache`, `routing`, `errors`, and `ownership`
- `full` runs every group, including live JSON output checks and ownership-import verification
## Groups
- `general`: `version`, `completions`, `config`, and `cache` command basics
- `live-table`: all shipped `stocks` commands in live table mode
- `live-json`: all shipped `stocks` commands in live JSON mode
- `mock`: all shipped `stocks` commands against the mock provider in JSON mode, plus representative quote/history/fundamental/financials table renders
- `cache`: exact data equality across warm, `--offline`, and stale-cache fallback reads for quote, technical, and MSN `profile`
- `routing`: Yahoo/MSN provider routing plus explicit MSN history behavior
- `errors`: JSON error contract and invalid flag/input checks
- `live-nonfinite`: opt-in live MSN fundamentals checks for known non-finite ticker payloads (`BUMI`, `ADRO`, `AIMS`)
- `ownership`: safe ownership smoke checks that do not require imported ownership data
- `ownership-import`: live discovery/import hardening checks for supported `above1` import plus expected unsupported legacy-family failures
## Notes
- The runner forces `IDX_OUTPUT=table` so table cases stay stable; JSON checks pass `-o json` explicitly.
- Cache-group warm cases clear the smoke cache first so warm/offline/stale sequences start clean.
- Use `--bin <path> --no-build` to validate an installed binary instead of `target/debug/idx`.
- `ownership-import` and `live-nonfinite` are opt-in (explicit `--group` or `--mode full`). Known non-finite repro tickers as of `2026-04-13`: `BUMI`, `ADRO`, `AIMS`.
- The baseline `ownership` group needs no imported data; `ownership sync` is covered by fixture-backed CLI tests instead.
- `scripts/audit-msn-fundamentals.sh` is a heavier provider-health sweep over the IDX MSN symbol map, not a baseline check.
- When a case fails, inspect its `.stdout`, `.stderr`, and `.validation` files under `tmp/live-smoke/.../logs/`.
`scripts/audit-msn-fundamentals.sh` is a heavier sweep of valuation output
across the whole IDX MSN symbol map, for provider-health audits.

View file

@ -189,19 +189,19 @@
doInstallCargoArtifacts = false;
installPhaseCommand = "touch $out";
});
smoke-mock = pkgs.runCommand "idx-smoke-mock"
{ nativeBuildInputs = with pkgs; [ bash coreutils diffutils findutils gnugrep gnused gawk jq ]; }
# Sanity check of the packaged (wrapped) binary; CLI behavior is
# covered by tests/cli.rs.
packaged-cli = pkgs.runCommand "idx-packaged-cli"
{ nativeBuildInputs = with pkgs; [ bash jq ]; }
''
mkdir -p scripts
cp ${./scripts/live-smoke.sh} scripts/live-smoke.sh
chmod u+w scripts/live-smoke.sh
patchShebangs scripts/live-smoke.sh
# The mock provider reads fixtures relative to the working dir.
mkdir -p tests
cp -r ${./tests/fixtures} tests/fixtures
export HOME="$TMPDIR"
${idxPackage}/bin/idx version
bash scripts/live-smoke.sh --bin ${idxPackage}/bin/idx --no-build --mode mock
${idxPackage}/bin/idx -o json version | jq -e '.version | type == "string"'
${idxPackage}/bin/idx completions bash | bash -n
IDX_USE_MOCK_PROVIDER=1 ${idxPackage}/bin/idx -o json stocks quote BBCA \
| jq -e '.[0].price | type == "number"'
touch "$out"
'';
install-script-sh = installerCheck "sh";

View file

@ -1,672 +1,169 @@
#!/usr/bin/env bash
# Live provider drift check: runs shipped commands against real MSN, Yahoo,
# and IDX endpoints and validates the JSON output shape. Deterministic CLI
# behavior is covered by tests/cli.rs; this only answers "do the upstreams
# still look the way our parsers expect?"
set -u
set -o pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MODE="live"
BIN_PATH=""
WORKDIR=""
SYMBOL="BBCA"
COMPARE_SYMBOLS="BBCA,BBRI"
NEWS_LIMIT="5"
SCREEN_LIMIT="10"
BIN_PATH=""
DRY_RUN=0
BUILD=1
declare -a GROUP_FILTERS=()
declare -a CASES=()
declare -a GROUPS_SELECTED=()
declare -a FAILURES=()
TOTAL_CASES=0
PASSED_CASES=0
FAILED_CASES=0
SEP=$'\x1f'
CONFIG_HOME=""
CACHE_HOME=""
DATA_HOME=""
OWNERSHIP_DB=""
LOG_DIR=""
OWNERSHIP_ABOVE1_URL=""
OWNERSHIP_ABOVE5_URL=""
OWNERSHIP_INVESTOR_TYPE_URL=""
declare -a BASE_ENV=()
TOTAL=0
PASSED=0
usage() {
cat <<'EOF'
Usage: scripts/live-smoke.sh [options]
Reusable smoke runner for shipped idx-cli command surfaces.
Options:
--mode <live|mock|full> Preset case selection. Default: live
--group <name> Restrict to one or more groups. Repeatable.
--symbol <ticker> Primary ticker for stock cases. Default: BBCA
--compare-symbols <list> Compare symbols. Default: BBCA,BBRI
--news-limit <n> News limit. Default: 5
--screen-limit <n> Screener limit. Default: 10
--workdir <path> Artifact root. Default: tmp/live-smoke/<timestamp>
--bin <path> Use an existing idx binary instead of target/debug/idx
--no-build Skip cargo build
--dry-run Print selected cases without running them
--help Show this help
Groups:
general version/completions/config/cache basics
live-table live network stock commands in table mode
live-json live network stock commands in JSON mode
mock mock-backed stock JSON contracts + representative table checks
cache deterministic cache/offline/stale-cache checks
routing provider routing and explicit unsupported checks
errors JSON error contract and invalid-flag checks
live-nonfinite opt-in live MSN fundamentals checks for known non-finite tickers
ownership ownership commands that are safe without imported data
ownership-import live ownership discovery/import hardening checks
Examples:
scripts/live-smoke.sh
scripts/live-smoke.sh --mode full
scripts/live-smoke.sh --mode mock --group cache
scripts/live-smoke.sh --group live-table --group live-json --symbol BBRI
scripts/live-smoke.sh --group live-nonfinite
--group <stocks|ownership> Restrict to a group. Repeatable. Default: both
--symbol <ticker> Primary ticker. Default: BBCA
--bin <path> Use an existing idx binary (skips cargo build)
--dry-run Print cases without running them
--help Show this help
EOF
}
add_group_filters() {
local raw="$1"
local part
local old_ifs="$IFS"
IFS=','
for part in $raw; do
if [[ -n "$part" ]]; then
GROUP_FILTERS+=("$part")
fi
done
IFS="$old_ifs"
}
mode_allows_group() {
local group="$1"
case "$MODE" in
live)
case "$group" in
general|live-table|ownership) return 0 ;;
*) return 1 ;;
esac
;;
mock)
case "$group" in
general|mock|cache|routing|errors|ownership) return 0 ;;
*) return 1 ;;
esac
;;
full)
return 0
;;
*)
return 1
;;
while [[ $# -gt 0 ]]; do
case "$1" in
--group) GROUPS_SELECTED+=("${2:-}"); shift 2 ;;
--symbol) SYMBOL="${2:-}"; shift 2 ;;
--bin) BIN_PATH="${2:-}"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
--help|-h) usage; exit 0 ;;
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
}
done
[[ ${#GROUPS_SELECTED[@]} -eq 0 ]] && GROUPS_SELECTED=(stocks ownership)
group_requested() {
local group="$1"
local selected
for group in "${GROUPS_SELECTED[@]}"; do
case "$group" in
stocks|ownership) ;;
*) echo "invalid --group '$group' (expected stocks or ownership)" >&2; exit 2 ;;
esac
done
if [[ ${#GROUP_FILTERS[@]} -eq 0 ]]; then
return 0
fi
for selected in "${GROUP_FILTERS[@]}"; do
if [[ "$selected" == "$group" ]]; then
return 0
fi
selected() {
local group
for group in "${GROUPS_SELECTED[@]}"; do
[[ "$group" == "$1" ]] && return 0
done
return 1
}
should_run_group() {
local group="$1"
if [[ ${#GROUP_FILTERS[@]} -gt 0 ]]; then
group_requested "$group"
return
fi
WORKDIR="$ROOT_DIR/tmp/live-smoke/$(date +%Y%m%d-%H%M%S)"
LOG_DIR="$WORKDIR/logs"
BASE_ENV=(
"XDG_CONFIG_HOME=$WORKDIR/config"
"XDG_CACHE_HOME=$WORKDIR/cache"
"XDG_DATA_HOME=$WORKDIR/data"
"IDX_PROVIDER=msn"
"IDX_HISTORY_PROVIDER=auto"
"IDX_NO_COLOR=1"
)
mode_allows_group "$group"
}
add_case() {
CASES+=("$1$SEP$2$SEP$3$SEP$4$SEP$5$SEP$6$SEP${7:-}")
}
register_cases() {
local live_env="IDX_PROVIDER=msn IDX_HISTORY_PROVIDER=auto"
local mock_env="IDX_PROVIDER=msn IDX_HISTORY_PROVIDER=auto IDX_USE_MOCK_PROVIDER=1"
local yahoo_mock_env="IDX_PROVIDER=yahoo IDX_HISTORY_PROVIDER=auto IDX_USE_MOCK_PROVIDER=1"
local quote_zero_ttl_env="IDX_PROVIDER=msn IDX_USE_MOCK_PROVIDER=1 IDX_CACHE_QUOTE_TTL=0"
local profile_zero_ttl_env="IDX_PROVIDER=msn IDX_USE_MOCK_PROVIDER=1 IDX_CACHE_FUNDAMENTAL_TTL=0"
local live_symbol="$SYMBOL"
local compare_args="$COMPARE_SYMBOLS"
local history_args="stocks history $live_symbol --period 3mo"
local screen_args="stocks screen --filter top-performers --limit $SCREEN_LIMIT"
local news_args="stocks news $live_symbol --limit $NEWS_LIMIT"
add_case "general" "version" "0" "" "-o json version" "" '.version | type == "string" and test("^[0-9]+\\.[0-9]+\\.[0-9]+")'
add_case "general" "completions-bash" "0" "" "completions bash" ""
add_case "general" "config-set-output-json" "0" "" "config set general.output json" ""
add_case "general" "config-get-output" "0" "" "config get general.output" "json"
add_case "general" "config-set-output-table" "0" "" "config set general.output table" ""
add_case "general" "cache-info" "0" "" "-o json cache info" "" '.files | type == "number" and . >= 0'
add_case "general" "cache-clear" "0" "" "-o json cache clear" "" '.removed | type == "number" and . >= 0'
add_case "live-table" "quote" "0" "$live_env" "stocks quote $live_symbol" ""
add_case "live-table" "history" "0" "$live_env" "$history_args" ""
add_case "live-table" "technical" "0" "$live_env" "stocks technical $live_symbol" ""
add_case "live-table" "growth" "0" "$live_env" "stocks growth $live_symbol" ""
add_case "live-table" "valuation" "0" "$live_env" "stocks valuation $live_symbol" ""
add_case "live-table" "risk" "0" "$live_env" "stocks risk $live_symbol" ""
add_case "live-table" "fundamental" "0" "$live_env" "stocks fundamental $live_symbol" ""
add_case "live-table" "compare" "0" "$live_env" "stocks compare $compare_args" ""
add_case "live-table" "profile" "0" "$live_env" "stocks profile $live_symbol" ""
add_case "live-table" "financials" "0" "$live_env" "stocks financials $live_symbol" ""
add_case "live-table" "earnings" "0" "$live_env" "stocks earnings $live_symbol" ""
add_case "live-table" "sentiment" "0" "$live_env" "stocks sentiment $live_symbol" ""
add_case "live-table" "insights" "0" "$live_env" "stocks insights $live_symbol" ""
add_case "live-table" "news" "0" "$live_env" "$news_args" ""
add_case "live-table" "screen" "0" "$live_env" "$screen_args" ""
add_case "live-json" "quote" "0" "$live_env" "-o json stocks quote $live_symbol" ""
add_case "live-json" "history" "0" "$live_env" "-o json $history_args" ""
add_case "live-json" "technical" "0" "$live_env" "-o json stocks technical $live_symbol" ""
add_case "live-json" "growth" "0" "$live_env" "-o json stocks growth $live_symbol" ""
add_case "live-json" "valuation" "0" "$live_env" "-o json stocks valuation $live_symbol" ""
add_case "live-json" "risk" "0" "$live_env" "-o json stocks risk $live_symbol" ""
add_case "live-json" "fundamental" "0" "$live_env" "-o json stocks fundamental $live_symbol" ""
add_case "live-json" "compare" "0" "$live_env" "-o json stocks compare $compare_args" ""
add_case "live-json" "profile" "0" "$live_env" "-o json stocks profile $live_symbol" ""
add_case "live-json" "financials" "0" "$live_env" "-o json stocks financials $live_symbol" ""
add_case "live-json" "earnings" "0" "$live_env" "-o json stocks earnings $live_symbol" ""
add_case "live-json" "sentiment" "0" "$live_env" "-o json stocks sentiment $live_symbol" ""
add_case "live-json" "insights" "0" "$live_env" "-o json stocks insights $live_symbol" ""
add_case "live-json" "news" "0" "$live_env" "-o json $news_args" ""
add_case "live-json" "screen" "0" "$live_env" "-o json $screen_args" ""
add_case "live-nonfinite" "valuation-bumi" "0" "$live_env" "-o json stocks valuation BUMI" ""
add_case "live-nonfinite" "valuation-adro" "0" "$live_env" "-o json stocks valuation ADRO" ""
add_case "live-nonfinite" "valuation-aims" "0" "$live_env" "-o json stocks valuation AIMS" ""
add_case "live-nonfinite" "compare-bad-tickers" "0" "$live_env" "-o json stocks compare BUMI,ADRO,AIMS" "" 'any(.[]; .symbol == "BUMI.JK")'
add_case "mock" "quote-table" "0" "$mock_env" "stocks quote $live_symbol" "9,875"
add_case "mock" "quote-json" "0" "$mock_env" "-o json stocks quote $live_symbol" "" '.[0].price == 9875 and .[0].change == 117'
add_case "mock" "history-table" "0" "$mock_env" "$history_args" "2024-03-03"
add_case "mock" "history-json" "0" "$mock_env" "-o json $history_args" "" '.[-1].close == 9925'
add_case "mock" "technical-json" "0" "$mock_env" "-o json stocks technical $live_symbol" "" '.current_price == 9925 and .sma200 == null'
add_case "mock" "growth-json" "0" "$mock_env" "-o json stocks growth $live_symbol" "" '.revenue_growth_pct == 8.1 and .earnings_growth_pct == 12.1'
add_case "mock" "valuation-json" "0" "$mock_env" "-o json stocks valuation $live_symbol" "" '.pe_trailing == 25.4 and .roe_pct == 19.8'
add_case "mock" "risk-json" "0" "$mock_env" "-o json stocks risk $live_symbol" "" '.debt_to_equity == 0.42 and .roa_pct == 3.1'
add_case "mock" "fundamental-table" "0" "$mock_env" "stocks fundamental $live_symbol" "25.40"
add_case "mock" "fundamental-json" "0" "$mock_env" "-o json stocks fundamental $live_symbol" "" '.growth.revenue_growth_pct == 8.1 and .valuation.pe_trailing == 25.4 and .risk.debt_to_equity == 0.42'
add_case "mock" "compare-json" "0" "$mock_env" "-o json stocks compare $compare_args" "" 'length > 0 and all(.[]; .growth.revenue_growth_pct == 8.1 and .valuation.pe_trailing == 25.4)'
add_case "mock" "profile-json" "0" "$mock_env" "-o json stocks profile $live_symbol" "" '.country == "Indonesia" and .employees == 27937'
add_case "mock" "financials-table" "0" "$mock_env" "stocks financials $live_symbol" "400,000,000"
add_case "mock" "financials-json" "0" "$mock_env" "-o json stocks financials $live_symbol" "" '.income_statement.values.netIncome == 400000000 and .cash_flow.values.operatingCashFlow == 600000000'
add_case "mock" "earnings-json" "0" "$mock_env" "-o json stocks earnings $live_symbol" "" '.eps_last_year == 1200 and .forecast[0].eps_forecast == 1300 and .history[0].eps_actual == 1250'
add_case "mock" "sentiment-json" "0" "$mock_env" "-o json stocks sentiment $live_symbol" "" '.statistics[0] == {"time_range":"1D","bullish":10,"bearish":2,"neutral":3}'
add_case "mock" "insights-json" "0" "$mock_env" "-o json stocks insights $live_symbol" "" '.last_updated == "2026-03-26T04:14:57.9197955Z"'
add_case "mock" "news-json" "0" "$mock_env" "-o json $news_args" "" "map(.id) == ([\"news-1\"][:$NEWS_LIMIT])"
add_case "mock" "screen-json" "0" "$mock_env" "-o json $screen_args" "" "map(.symbol) == ([\"BBRI.JK\", \"BBCA.JK\"][:$SCREEN_LIMIT])"
add_case "cache" "quote-warm" "0" "$quote_zero_ttl_env" "-o json stocks quote $live_symbol" ""
add_case "cache" "quote-offline" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "-o json --offline stocks quote $live_symbol" ""
add_case "cache" "quote-stale-fallback" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "-o json stocks quote $live_symbol" ""
add_case "cache" "technical-warm" "0" "$quote_zero_ttl_env" "-o json stocks technical $live_symbol" ""
add_case "cache" "technical-offline" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "-o json --offline stocks technical $live_symbol" ""
add_case "cache" "technical-stale-fallback" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "-o json stocks technical $live_symbol" ""
add_case "cache" "profile-warm" "0" "$profile_zero_ttl_env" "-o json stocks profile $live_symbol" ""
add_case "cache" "profile-offline" "0" "$profile_zero_ttl_env IDX_MOCK_ERROR=1" "-o json --offline stocks profile $live_symbol" ""
add_case "cache" "profile-stale-fallback" "0" "$profile_zero_ttl_env IDX_MOCK_ERROR=1" "-o json stocks profile $live_symbol" ""
add_case "routing" "yahoo-quote" "0" "$yahoo_mock_env" "-o json --no-cache stocks quote $live_symbol" "" '.[0].price == 9875'
add_case "routing" "explicit-msn-history" "0" "$mock_env" "-o json --no-cache stocks history $live_symbol --period 3mo --history-provider msn" "" '.[0].close == 8000 and all(.[]; .volume == 0 and .open == .close and .high == .close and .low == .close)'
add_case "errors" "invalid-provider-json" "1" "IDX_PROVIDER=bogus" "-o json version" "" '.code == "CONFIGERROR"'
add_case "errors" "profile-provider-gate-json" "1" "IDX_PROVIDER=yahoo" "-o json stocks profile $live_symbol" "" '.code == "UNSUPPORTED"'
add_case "errors" "invalid-screen-region-json" "1" "$mock_env" "-o json stocks screen --region eu" "" '.code == "INVALIDINPUT"'
add_case "errors" "offline-no-cache" "1" "IDX_USE_MOCK_PROVIDER=1" "-o json --offline --no-cache stocks quote $live_symbol" "" '.code == "INVALIDINPUT"'
add_case "ownership" "releases-empty" "0" "" "-o json ownership releases" "" '. == []'
add_case "ownership" "fetch-bing-unsupported" "1" "" "-o json ownership import --fetch-bing $live_symbol" "" '.code == "UNSUPPORTED"'
add_case "ownership-import" "discover-default-above1" "0" "" "ownership discover --limit 1" "supported"
add_case "ownership-import" "import-supported-above1" "0" "" "ownership import --url $OWNERSHIP_ABOVE1_URL" "Imported "
add_case "ownership-import" "releases-after-import" "0" "" "ownership releases" "$OWNERSHIP_ABOVE1_URL"
add_case "ownership-import" "import-legacy-above5" "1" "" "ownership import --url $OWNERSHIP_ABOVE5_URL" "legacy IDX \`above5\` ownership PDFs are not supported for import"
add_case "ownership-import" "import-legacy-investor-type" "1" "" "ownership import --url $OWNERSHIP_INVESTOR_TYPE_URL" "legacy IDX \`investor-type\` ownership PDFs are not supported for import"
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
MODE="${2:-}"
shift 2
;;
--group)
add_group_filters "${2:-}"
shift 2
;;
--symbol)
SYMBOL="${2:-}"
shift 2
;;
--compare-symbols)
COMPARE_SYMBOLS="${2:-}"
shift 2
;;
--news-limit)
NEWS_LIMIT="${2:-}"
shift 2
;;
--screen-limit)
SCREEN_LIMIT="${2:-}"
shift 2
;;
--workdir)
WORKDIR="${2:-}"
shift 2
;;
--bin)
BIN_PATH="${2:-}"
BUILD=0
shift 2
;;
--no-build)
BUILD=0
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--help|-h)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
case "$MODE" in
live|mock|full) ;;
# jq filter describing the JSON shape each command must keep.
contract() {
case "$1" in
"stocks quote "*|"stocks screen"*)
echo 'type == "array" and length > 0 and all(.[]; (.symbol | type == "string") and (.price | type == "number"))' ;;
"stocks history "*)
echo 'type == "array" and length > 0 and all(.[]; (.date | type == "string") and (.close | type == "number"))' ;;
"stocks technical "*)
echo '(.current_price | type == "number") and (.signals.overall | type == "string")' ;;
"stocks growth "*|"stocks valuation "*|"stocks risk "*)
echo '.overall_signal | type == "string"' ;;
"stocks fundamental "*)
echo '(.growth.overall_signal | type == "string") and (.valuation.overall_signal | type == "string") and (.risk.overall_signal | type == "string")' ;;
"stocks compare "*)
echo 'type == "array" and length > 0 and all(.[]; (.symbol | type == "string") and (.valuation | type == "object"))' ;;
"stocks profile "*)
echo '(.symbol | type == "string") and has("long_name") and (.officers | type == "array")' ;;
"stocks financials "*)
echo '(.instrument.symbol | type == "string") and has("income_statement") and has("balance_sheet") and has("cash_flow")' ;;
"stocks earnings "*)
echo '(.history | type == "array") and (.forecast | type == "array")' ;;
"stocks sentiment "*)
echo '.statistics | type == "array"' ;;
"stocks insights "*)
echo '(.highlights | type == "array") and (.risks | type == "array")' ;;
"stocks news "*)
echo 'type == "array" and all(.[]; (.title | type == "string") and (.url | type == "string"))' ;;
"ownership discover"*)
echo 'type == "array" and length > 0 and (.[0].pdf_url | type == "string")' ;;
"ownership releases")
echo 'type == "array" and length == 1' ;;
*)
echo "invalid --mode '$MODE' (expected live, mock, or full)" >&2
exit 2
;;
echo 'true' ;;
esac
}
prepare_paths() {
if [[ -z "$WORKDIR" ]]; then
WORKDIR="$ROOT_DIR/tmp/live-smoke/$(date +%Y%m%d-%H%M%S)"
run_case() {
local name="$1" cmd="$2" filter log
local -a args
TOTAL=$((TOTAL + 1))
printf '[%02d] %s\n' "$TOTAL" "$name"
if (( DRY_RUN )); then
printf ' idx -o json %s\n' "$cmd"
return
fi
CONFIG_HOME="$WORKDIR/config"
CACHE_HOME="$WORKDIR/cache"
DATA_HOME="$WORKDIR/data"
OWNERSHIP_DB="$WORKDIR/ownership/ownership.db"
LOG_DIR="$WORKDIR/logs"
BASE_ENV=(
"XDG_CONFIG_HOME=$CONFIG_HOME"
"XDG_CACHE_HOME=$CACHE_HOME"
"XDG_DATA_HOME=$DATA_HOME"
"IDX_OUTPUT=table"
"IDX_NO_COLOR=1"
)
if (( DRY_RUN == 0 )); then
mkdir -p "$CONFIG_HOME" "$CACHE_HOME" "$DATA_HOME" "$(dirname "$OWNERSHIP_DB")" "$LOG_DIR"
log="$LOG_DIR/$(printf '%02d' "$TOTAL")_$name"
read -r -a args <<< "$cmd"
if ! env "${BASE_ENV[@]}" "$BIN_PATH" -o json "${args[@]}" >"$log.stdout" 2>"$log.stderr"; then
FAILURES+=("$name: non-zero exit (see $log.stderr)")
echo " FAIL exit"
return
fi
filter="$(contract "$cmd")"
if ! jq -e -s "length == 1 and (.[0] | $filter)" "$log.stdout" >/dev/null 2>"$log.validation"; then
FAILURES+=("$name: output shape changed (see $log.stdout)")
echo " FAIL shape"
return
fi
PASSED=$((PASSED + 1))
echo " PASS"
}
build_binary() {
local build_log
local stale_input
if (( ! DRY_RUN )); then
command -v jq >/dev/null 2>&1 || { echo "jq is required; run inside nix develop" >&2; exit 1; }
mkdir -p "$LOG_DIR"
if [[ -z "$BIN_PATH" ]]; then
BIN_PATH="$ROOT_DIR/target/debug/idx"
(cd "$ROOT_DIR" && cargo build --quiet --bin idx) || { echo "cargo build failed" >&2; exit 1; }
fi
env "${BASE_ENV[@]}" "$BIN_PATH" config set ownership.db_path "$WORKDIR/ownership.db" >/dev/null
fi
if (( DRY_RUN )); then
return 0
fi
if (( BUILD )); then
if ! command -v cargo >/dev/null 2>&1; then
if [[ -x "$BIN_PATH" ]]; then
printf 'info: cargo not found; reusing existing binary at %s\n' "$BIN_PATH"
BUILD=0
else
echo "cargo not found; run inside nix develop or pass --bin/--no-build with an existing idx binary" >&2
exit 1
fi
fi
fi
if (( BUILD == 0 )) && [[ "$BIN_PATH" == "$ROOT_DIR/target/debug/idx" && -x "$BIN_PATH" ]]; then
stale_input="$(
find \
"$ROOT_DIR/src" \
"$ROOT_DIR/tests" \
"$ROOT_DIR/Cargo.toml" \
"$ROOT_DIR/Cargo.lock" \
"$ROOT_DIR/scripts/live-smoke.sh" \
-type f \
-newer "$BIN_PATH" \
-print \
-quit \
2>/dev/null
)"
if [[ -n "$stale_input" ]]; then
echo "refusing to run smoke checks against stale $BIN_PATH; newer input detected at $stale_input" >&2
echo "rebuild first or omit --no-build so the runner refreshes target/debug/idx" >&2
exit 1
fi
fi
if (( BUILD )); then
build_log="$LOG_DIR/build.log"
if ! (
cd "$ROOT_DIR" || exit 1
cargo build --quiet --bin idx
) >"$build_log" 2>&1; then
echo "build failed; see $build_log" >&2
exit 1
fi
fi
if [[ ! -x "$BIN_PATH" ]]; then
echo "idx binary not found or not executable: $BIN_PATH" >&2
exit 1
fi
}
bootstrap_case() {
local label="$1"
shift
local log_file="$LOG_DIR/bootstrap-${label}.log"
if (( DRY_RUN )); then
printf 'bootstrap %-18s %s\n' "$label" "$*"
return 0
fi
if ! (
cd "$ROOT_DIR" || exit 1
env "${BASE_ENV[@]}" "$BIN_PATH" "$@"
) >"$log_file" 2>&1; then
echo "bootstrap failed for $label; see $log_file" >&2
exit 1
fi
}
prepare_environment() {
bootstrap_case "config-init" config init
bootstrap_case "ownership-db" config set ownership.db_path "$OWNERSHIP_DB"
}
discover_ownership_import_url() {
local family="$1"
local log_file="$LOG_DIR/bootstrap-discover-${family}.log"
local url
if (( DRY_RUN )); then
case "$family" in
above1) printf 'https://example.invalid/above1-lamp1.pdf' ;;
above5) printf 'https://example.invalid/above5-lamp1.pdf' ;;
investor-type) printf 'https://example.invalid/investor-type-lamp1.pdf' ;;
*) return 1 ;;
esac
return 0
fi
if ! (
cd "$ROOT_DIR" || exit 1
env "${BASE_ENV[@]}" "$BIN_PATH" -o json ownership discover --family "$family" --limit 1
) >"$log_file" 2>&1; then
echo "bootstrap failed for ownership-discover-$family; see $log_file" >&2
exit 1
fi
url="$(grep -m1 '"pdf_url"' "$log_file" | sed -E 's/.*"pdf_url": "([^"]+)".*/\1/')"
if [[ -z "$url" ]]; then
echo "bootstrap failed for ownership-discover-$family: could not parse pdf_url from $log_file" >&2
exit 1
fi
printf '%s' "$url"
}
prepare_ownership_import_inputs() {
if ! should_run_group "ownership-import"; then
return 0
fi
OWNERSHIP_ABOVE1_URL="$(discover_ownership_import_url above1)"
OWNERSHIP_ABOVE5_URL="$(discover_ownership_import_url above5)"
OWNERSHIP_INVESTOR_TYPE_URL="$(discover_ownership_import_url investor-type)"
}
cache_case_starts_fresh() {
local group="$1"
local label="$2"
[[ "$group" == "cache" && "$label" == *"-warm" ]]
}
reset_cache_for_case() {
local label="$1"
# Cache checks need a clean starting point so the warm/offline/stale trio
# validates TTL-sensitive fallback behavior instead of reusing prior group state.
bootstrap_case "cache-reset-${label}" cache clear
}
sanitize_name() {
printf '%s' "$1" | tr -cs 'A-Za-z0-9._-' '_'
}
command_display() {
local env_spec="$1"
local cmd_spec="$2"
printf 'env %s' "${BASE_ENV[*]}"
if [[ -n "$env_spec" ]]; then
printf ' %s' "$env_spec"
fi
printf ' %s %s' "$BIN_PATH" "$cmd_spec"
}
json_contract() {
if [[ "$2" -ne 0 ]]; then
printf '%s\n' '.error == true and (.code | type == "string") and (.message | type == "string")'
return
fi
case "$1" in
*"stocks quote "*|*"stocks screen"*)
printf '%s\n' 'type == "array" and all(.[]; (.symbol | type == "string") and (.price | type == "number") and has("change_pct"))' ;;
*"stocks history "*)
printf '%s\n' 'type == "array" and all(.[]; (.date | type == "string") and (.close | type == "number") and (.volume | type == "number"))' ;;
*"stocks technical "*)
printf '%s\n' '(.symbol | type == "string") and (.current_price | type == "number") and has("sma200") and (.signals.overall | type == "string")' ;;
*"stocks growth "*)
printf '%s\n' 'has("revenue_growth_pct") and has("earnings_growth_pct") and (.overall_signal | type == "string")' ;;
*"stocks valuation "*)
printf '%s\n' 'has("pe_trailing") and has("roe_pct") and (.overall_signal | type == "string")' ;;
*"stocks risk "*)
printf '%s\n' 'has("debt_to_equity") and has("roa_pct") and (.overall_signal | type == "string")' ;;
*"stocks fundamental "*)
printf '%s\n' '(.symbol | type == "string") and (.growth.overall_signal | type == "string") and (.valuation.overall_signal | type == "string") and (.risk.overall_signal | type == "string")' ;;
*"stocks compare "*)
printf '%s\n' 'type == "array" and all(.[]; (.symbol | type == "string") and (.growth | type == "object") and (.valuation | type == "object") and (.risk | type == "object"))' ;;
*"stocks profile "*)
printf '%s\n' '(.symbol | type == "string") and has("long_name") and has("country") and (.officers | type == "array")' ;;
*"stocks financials "*)
printf '%s\n' '(.instrument.symbol | type == "string") and has("income_statement") and has("balance_sheet") and has("cash_flow")' ;;
*"stocks earnings "*)
printf '%s\n' '(.symbol | type == "string") and (.history | type == "array") and (.forecast | type == "array")' ;;
*"stocks sentiment "*)
printf '%s\n' '(.symbol | type == "string") and (.statistics | type == "array") and all(.statistics[]; (.bullish | type == "number") and (.bearish | type == "number") and (.neutral | type == "number"))' ;;
*"stocks insights "*)
printf '%s\n' '(.symbol | type == "string") and (.highlights | type == "array") and (.risks | type == "array")' ;;
*"stocks news "*)
printf '%s\n' 'type == "array" and all(.[]; (.symbol | type == "string") and (.title | type == "string") and (.url | type == "string"))' ;;
*)
printf '%s\n' 'type == "object" or type == "array"' ;;
esac
}
run_case_line() {
local line="$1"
local group label expected_exit env_spec cmd_spec expect_spec json_expect
local safe_name log_file stdout_file stderr_file output_file status contract baseline_file
local failure=""
local -a env_parts cmd_parts
IFS="$SEP" read -r group label expected_exit env_spec cmd_spec expect_spec json_expect <<< "$line"
if ! should_run_group "$group"; then
return 0
fi
TOTAL_CASES=$((TOTAL_CASES + 1))
safe_name="$(sanitize_name "${group}-${label}")"
log_file="$(printf '%s/%03d_%s' "$LOG_DIR" "$TOTAL_CASES" "$safe_name")"
stdout_file="$log_file.stdout"
stderr_file="$log_file.stderr"
printf '[%02d] %-10s %s\n' "$TOTAL_CASES" "$group" "$label"
if cache_case_starts_fresh "$group" "$label"; then
reset_cache_for_case "$label"
fi
if (( DRY_RUN )); then
printf ' %s\n' "$(command_display "$env_spec" "$cmd_spec")"
return 0
fi
env_parts=()
cmd_parts=()
if [[ -n "$env_spec" ]]; then
read -r -a env_parts <<< "$env_spec"
fi
read -r -a cmd_parts <<< "$cmd_spec"
(
cd "$ROOT_DIR" || exit 1
env "${BASE_ENV[@]}" "${env_parts[@]}" "$BIN_PATH" "${cmd_parts[@]}"
) >"$stdout_file" 2>"$stderr_file"
status=$?
output_file="$stdout_file"
if [[ "$status" -ne 0 ]]; then
output_file="$stderr_file"
fi
if [[ "$status" -ne "$expected_exit" ]]; then
failure="exit $status, expected $expected_exit"
elif [[ "$status" -ne 0 && -s "$stdout_file" ]]; then
failure="error command wrote to stdout"
elif [[ -n "$expect_spec" ]] && ! grep -Fq -- "$expect_spec" "$output_file"; then
failure="missing '$expect_spec' in expected output stream"
elif [[ " $cmd_spec " == *" -o json "* ]]; then
contract="$(json_contract "$cmd_spec" "$status")"
if ! jq -e -s "length == 1 and (.[0] | ($contract) and (${json_expect:-true}))" \
"$output_file" > /dev/null 2>"$log_file.validation"; then
failure="invalid JSON document or output contract"
fi
elif [[ "$label" == "completions-bash" ]] && ! bash -n "$stdout_file"; then
failure="invalid Bash completion script"
fi
if [[ -z "$failure" && "$group" == "cache" ]]; then
baseline_file="$LOG_DIR/cache-${label%%-*}-baseline.json"
if [[ "$label" == *"-warm" ]]; then
jq -S . "$stdout_file" >"$baseline_file"
elif ! jq -S . "$stdout_file" | cmp -s "$baseline_file" -; then
failure="offline/stale data differs from the warmed cache"
elif [[ ! -s "$stderr_file" ]]; then
failure="expired-cache diagnostic missing from stderr"
fi
fi
if [[ -n "$failure" ]]; then
FAILED_CASES=$((FAILED_CASES + 1))
FAILURES+=("$group/$label ($failure) -> $log_file.{stdout,stderr,validation}")
printf ' FAIL %s logs=%s.{stdout,stderr,validation}\n' "$failure" "$log_file"
return 0
fi
PASSED_CASES=$((PASSED_CASES + 1))
printf ' PASS logs=%s.{stdout,stderr}\n' "$log_file"
}
main() {
local case_line
parse_args "$@"
prepare_paths
if (( DRY_RUN == 0 )) && ! command -v jq >/dev/null 2>&1; then
echo "jq is required for smoke output validation; run inside nix develop" >&2
exit 1
fi
printf 'mode: %s\n' "$MODE"
printf 'workdir: %s\n' "$WORKDIR"
printf 'symbol: %s\n' "$SYMBOL"
printf 'compare symbols: %s\n' "$COMPARE_SYMBOLS"
if [[ ${#GROUP_FILTERS[@]} -gt 0 ]]; then
printf 'group filter: %s\n' "${GROUP_FILTERS[*]}"
fi
build_binary
prepare_environment
prepare_ownership_import_inputs
register_cases
if should_run_group "ownership-import"; then
printf 'ownership above1 url: %s\n' "$OWNERSHIP_ABOVE1_URL"
printf 'ownership above5 url: %s\n' "$OWNERSHIP_ABOVE5_URL"
printf 'ownership investor-type url: %s\n' "$OWNERSHIP_INVESTOR_TYPE_URL"
fi
for case_line in "${CASES[@]}"; do
run_case_line "$case_line"
if selected stocks; then
for cmd in quote technical growth valuation risk fundamental profile financials earnings sentiment insights; do
run_case "$cmd" "stocks $cmd $SYMBOL"
done
run_case history "stocks history $SYMBOL --period 3mo"
run_case news "stocks news $SYMBOL --limit 5"
run_case screen "stocks screen --filter top-performers --limit 10"
run_case compare "stocks compare $SYMBOL,BBRI"
# BUMI/ADRO/AIMS return non-finite MSN key ratios (Infinity/NaN).
run_case compare-nonfinite "stocks compare BUMI,ADRO,AIMS"
fi
if (( DRY_RUN )); then
printf '\ndry-run complete: %d case(s) selected\n' "$TOTAL_CASES"
exit 0
if selected ownership; then
run_case discover "ownership discover --limit 1"
if (( ! DRY_RUN )) && [[ -s "$LOG_DIR/$(printf '%02d' "$TOTAL")_discover.stdout" ]]; then
url="$(jq -r '.[0].pdf_url' "$LOG_DIR/$(printf '%02d' "$TOTAL")_discover.stdout")"
run_case import "ownership import --url $url"
run_case releases "ownership releases"
fi
fi
printf '\nsummary: %d passed, %d failed, %d total\n' "$PASSED_CASES" "$FAILED_CASES" "$TOTAL_CASES"
printf 'logs: %s\n' "$LOG_DIR"
if (( FAILED_CASES > 0 )); then
printf 'failures:\n'
printf ' - %s\n' "${FAILURES[@]}"
exit 1
fi
}
main "$@"
(( DRY_RUN )) && { echo "dry-run: $TOTAL case(s)"; exit 0; }
printf '\nsummary: %d passed, %d failed\nlogs: %s\n' "$PASSED" "$((TOTAL - PASSED))" "$LOG_DIR"
if (( ${#FAILURES[@]} > 0 )); then
printf ' - %s\n' "${FAILURES[@]}"
exit 1
fi