fix(sentiment): treat an empty MSN response as no votes, not a parse error

MSN SentimentBrowser returns `[]` for every IDX ticker checked live
(BBCA, BBRI, TLKM), so `stocks sentiment` always failed with
PARSEERROR. Return empty statistics instead; the table prints a
one-line notice and JSON gets `"statistics": []` with exit 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld3fDSDiBzAaQvgZJX5NKM
This commit is contained in:
Claude 2026-10-02 14:28:52 +00:00
commit f77f81c97b
No known key found for this signature in database
2 changed files with 20 additions and 3 deletions

View file

@ -460,9 +460,15 @@ pub(super) fn parse_sentiment(
symbol: &str,
raw: &[RawSentiment],
) -> Result<SentimentData, IdxError> {
let item = raw
.first()
.ok_or_else(|| IdxError::ParseError("no sentiment data".into()))?;
// MSN answers `[]` for tickers nobody has voted on (all IDX names as of
// 2026-10). That is "no votes", not a malformed response.
let Some(item) = raw.first() else {
let ticker = ticker_from_symbol(symbol).unwrap_or_default();
return Ok(SentimentData {
symbol: normalized_symbol(symbol, &ticker),
statistics: Vec::new(),
});
};
let stats = item
.sentiment_statistics
.as_ref()
@ -929,6 +935,13 @@ mod tests {
);
}
#[test]
fn empty_sentiment_response_is_empty_data_not_an_error() {
let sentiment = parse_sentiment("BBCA", &[]).expect("empty sentiment is valid");
assert_eq!(sentiment.symbol, "BBCA.JK");
assert!(sentiment.statistics.is_empty());
}
#[test]
fn parses_msn_chart_price_only_fixture_as_synthetic_ohlc() {
let raw: Vec<RawChartResponse> = serde_json::from_str(include_str!(

View file

@ -768,6 +768,10 @@ pub fn print_earnings(report: &EarningsReport) -> Result<(), IdxError> {
}
pub fn print_sentiment(data: &SentimentData) -> Result<(), IdxError> {
if data.statistics.is_empty() {
println!("No sentiment votes for {}.", data.symbol);
return Ok(());
}
let mut table = Table::new();
table
.load_preset(UTF8_FULL)