ci(nix): unify cached verification and strengthen packaged smoke

Run deterministic installer and publisher checks in Nix, align dev and release dependency artifacts, narrow derivation inputs, and use content-aware CI cache keys. Replace duplicate Cargo gates with a Nix pre-push gate; validate JSON streams and cache data in representative packaged-binary smoke cases.
This commit is contained in:
hermes 2026-10-03 16:22:11 +00:00
commit 26ffd445e8
6 changed files with 252 additions and 162 deletions

View file

@ -57,7 +57,7 @@ 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 commands in table + 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
@ -139,7 +139,7 @@ should_run_group() {
}
add_case() {
CASES+=("$1$SEP$2$SEP$3$SEP$4$SEP$5$SEP$6")
CASES+=("$1$SEP$2$SEP$3$SEP$4$SEP$5$SEP$6$SEP${7:-}")
}
register_cases() {
@ -154,19 +154,13 @@ register_cases() {
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" "" "version" ""
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" "completions-zsh" "0" "" "completions zsh" ""
add_case "general" "completions-fish" "0" "" "completions fish" ""
add_case "general" "config-path" "0" "" "config path" ""
add_case "general" "config-init" "0" "" "config init" ""
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" "config-set-db-path" "0" "" "config set ownership.db_path $OWNERSHIP_DB" ""
add_case "general" "config-get-db-path" "0" "" "config get ownership.db_path" "$OWNERSHIP_DB"
add_case "general" "cache-info" "0" "" "cache info" ""
add_case "general" "cache-clear" "0" "" "cache clear" ""
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" ""
@ -200,64 +194,51 @@ register_cases() {
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" "\"overall_signal\""
add_case "live-nonfinite" "valuation-adro" "0" "$live_env" "-o json stocks valuation ADRO" "\"overall_signal\""
add_case "live-nonfinite" "valuation-aims" "0" "$live_env" "-o json stocks valuation AIMS" "\"overall_signal\""
add_case "live-nonfinite" "compare-bad-tickers" "0" "$live_env" "-o json stocks compare BUMI,ADRO,AIMS" "\"symbol\": \"BUMI.JK\""
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" ""
add_case "mock" "quote-json" "0" "$mock_env" "-o json stocks quote $live_symbol" ""
add_case "mock" "history-table" "0" "$mock_env" "$history_args" ""
add_case "mock" "history-json" "0" "$mock_env" "-o json $history_args" ""
add_case "mock" "technical-table" "0" "$mock_env" "stocks technical $live_symbol" ""
add_case "mock" "technical-json" "0" "$mock_env" "-o json stocks technical $live_symbol" ""
add_case "mock" "growth-table" "0" "$mock_env" "stocks growth $live_symbol" ""
add_case "mock" "growth-json" "0" "$mock_env" "-o json stocks growth $live_symbol" ""
add_case "mock" "valuation-table" "0" "$mock_env" "stocks valuation $live_symbol" ""
add_case "mock" "valuation-json" "0" "$mock_env" "-o json stocks valuation $live_symbol" ""
add_case "mock" "risk-table" "0" "$mock_env" "stocks risk $live_symbol" ""
add_case "mock" "risk-json" "0" "$mock_env" "-o json stocks risk $live_symbol" ""
add_case "mock" "fundamental-table" "0" "$mock_env" "stocks fundamental $live_symbol" ""
add_case "mock" "fundamental-json" "0" "$mock_env" "-o json stocks fundamental $live_symbol" ""
add_case "mock" "compare-table" "0" "$mock_env" "stocks compare $compare_args" ""
add_case "mock" "compare-json" "0" "$mock_env" "-o json stocks compare $compare_args" ""
add_case "mock" "profile-table" "0" "$mock_env" "stocks profile $live_symbol" ""
add_case "mock" "profile-json" "0" "$mock_env" "-o json stocks profile $live_symbol" ""
add_case "mock" "financials-table" "0" "$mock_env" "stocks financials $live_symbol" ""
add_case "mock" "financials-json" "0" "$mock_env" "-o json stocks financials $live_symbol" ""
add_case "mock" "earnings-table" "0" "$mock_env" "stocks earnings $live_symbol" ""
add_case "mock" "earnings-json" "0" "$mock_env" "-o json stocks earnings $live_symbol" ""
add_case "mock" "sentiment-table" "0" "$mock_env" "stocks sentiment $live_symbol" ""
add_case "mock" "sentiment-json" "0" "$mock_env" "-o json stocks sentiment $live_symbol" ""
add_case "mock" "insights-table" "0" "$mock_env" "stocks insights $live_symbol" ""
add_case "mock" "insights-json" "0" "$mock_env" "-o json stocks insights $live_symbol" ""
add_case "mock" "news-table" "0" "$mock_env" "$news_args" ""
add_case "mock" "news-json" "0" "$mock_env" "-o json $news_args" ""
add_case "mock" "screen-table" "0" "$mock_env" "$screen_args" ""
add_case "mock" "screen-json" "0" "$mock_env" "-o json $screen_args" ""
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" "stocks quote $live_symbol" ""
add_case "cache" "quote-offline" "0" "$quote_zero_ttl_env" "--offline stocks quote $live_symbol" ""
add_case "cache" "quote-stale-fallback" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "stocks quote $live_symbol" "serving stale cache"
add_case "cache" "technical-warm" "0" "$quote_zero_ttl_env" "stocks technical $live_symbol" ""
add_case "cache" "technical-offline" "0" "$quote_zero_ttl_env" "--offline stocks technical $live_symbol" ""
add_case "cache" "technical-stale-fallback" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "stocks technical $live_symbol" "serving stale cache"
add_case "cache" "profile-warm" "0" "$profile_zero_ttl_env" "stocks profile $live_symbol" ""
add_case "cache" "profile-offline" "0" "$profile_zero_ttl_env" "--offline stocks profile $live_symbol" ""
add_case "cache" "profile-stale-fallback" "0" "$profile_zero_ttl_env IDX_MOCK_ERROR=1" "stocks profile $live_symbol" "serving stale cache"
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" "stocks quote $live_symbol" ""
add_case "routing" "auto-history-fallback" "0" "$mock_env" "$history_args" ""
add_case "routing" "auto-technical-fallback" "0" "$mock_env" "stocks technical $live_symbol" ""
add_case "routing" "explicit-msn-history" "0" "$mock_env" "stocks history $live_symbol --period 3mo --history-provider msn" "History for"
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" "\"error\": true"
add_case "errors" "profile-provider-gate-json" "1" "IDX_PROVIDER=yahoo" "-o json stocks profile $live_symbol" "requires --provider msn"
add_case "errors" "invalid-screen-region-json" "1" "$mock_env" "-o json stocks screen --region eu" "invalid screener region"
add_case "errors" "offline-no-cache" "1" "IDX_USE_MOCK_PROVIDER=1" "--offline --no-cache stocks quote $live_symbol" "cannot combine --offline with --no-cache"
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" "" "ownership releases" "No ownership releases imported yet."
add_case "ownership" "fetch-bing-unsupported" "1" "" "ownership import --fetch-bing $live_symbol" "--fetch-bing import is not implemented yet"
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 "
@ -402,11 +383,10 @@ build_binary() {
if (( BUILD )); then
build_log="$LOG_DIR/build.log"
(
if ! (
cd "$ROOT_DIR" || exit 1
cargo build --quiet --bin idx
) >"$build_log" 2>&1
if [[ $? -ne 0 ]]; then
) >"$build_log" 2>&1; then
echo "build failed; see $build_log" >&2
exit 1
fi
@ -428,12 +408,10 @@ bootstrap_case() {
return 0
fi
(
if ! (
cd "$ROOT_DIR" || exit 1
env "${BASE_ENV[@]}" "$BIN_PATH" "$@"
) >"$log_file" 2>&1
if [[ $? -ne 0 ]]; then
) >"$log_file" 2>&1; then
echo "bootstrap failed for $label; see $log_file" >&2
exit 1
fi
@ -459,12 +437,10 @@ discover_ownership_import_url() {
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
if [[ $? -ne 0 ]]; then
) >"$log_file" 2>&1; then
echo "bootstrap failed for ownership-discover-$family; see $log_file" >&2
exit 1
fi
@ -518,13 +494,54 @@ command_display() {
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
local safe_name log_file status
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 <<< "$line"
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
@ -532,7 +549,9 @@ run_case_line() {
TOTAL_CASES=$((TOTAL_CASES + 1))
safe_name="$(sanitize_name "${group}-${label}")"
log_file="$(printf '%s/%03d_%s.log' "$LOG_DIR" "$TOTAL_CASES" "$safe_name")"
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"
@ -556,25 +575,50 @@ run_case_line() {
(
cd "$ROOT_DIR" || exit 1
env "${BASE_ENV[@]}" "${env_parts[@]}" "$BIN_PATH" "${cmd_parts[@]}"
) >"$log_file" 2>&1
) >"$stdout_file" 2>"$stderr_file"
status=$?
if [[ "$status" -ne "$expected_exit" ]]; then
FAILED_CASES=$((FAILED_CASES + 1))
FAILURES+=("$group/$label (exit $status, expected $expected_exit) -> $log_file")
printf ' FAIL exit=%s expected=%s log=%s\n' "$status" "$expected_exit" "$log_file"
return 0
output_file="$stdout_file"
if [[ "$status" -ne 0 ]]; then
output_file="$stderr_file"
fi
if [[ -n "$expect_spec" ]] && ! grep -Fq -- "$expect_spec" "$log_file"; then
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 (missing '$expect_spec') -> $log_file")
printf ' FAIL missing=%s log=%s\n' "$expect_spec" "$log_file"
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 log=%s\n' "$log_file"
printf ' PASS logs=%s.{stdout,stderr}\n' "$log_file"
}
main() {
@ -582,6 +626,10 @@ main() {
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"