Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ src/tls.cert
src/modules/test.ts
dist/
debug/
AGENTS.md
AGENTS.md.venv/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Malformed gitignore entry - likely a merge artifact.

AGENTS.md.venv/ appears to be two entries accidentally merged together. Based on the PR description mentioning .venv should be ignored, this should be split into separate entries.

🔧 Proposed fix
-AGENTS.md.venv/
+AGENTS.md
+.venv/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AGENTS.md.venv/
AGENTS.md
.venv/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 8, The .gitignore currently contains a malformed merged
entry "AGENTS.md.venv/"; update the .gitignore by splitting this into two
separate lines so each pattern is its own entry—e.g., add "AGENTS.md" on one
line and ".venv/" on the next—so the AGENTS.md file and the .venv virtual
environment directory are ignored correctly.

__pycache__/
*.pyc
.pytest_cache/
6 changes: 6 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# tests package
80 changes: 80 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Shared fixtures for Botillo tests."""
import json
import os
import tempfile
import pytest


@pytest.fixture
def tmp_db_dir():
"""Create a temporary directory for test JSON files."""
with tempfile.TemporaryDirectory() as d:
yield d


@pytest.fixture
def bitcoin_file(tmp_db_dir):
"""Path to a test bitcoin.json file."""
return os.path.join(tmp_db_dir, "bitcoin.json")


@pytest.fixture
def prodillos_file(tmp_db_dir):
"""Path to a test prodillos.json file."""
return os.path.join(tmp_db_dir, "prodillos.json")


@pytest.fixture
def trofeillos_file(tmp_db_dir):
"""Path to a test trofeillos.json file."""
return os.path.join(tmp_db_dir, "trofeillos.json")


@pytest.fixture
def sample_bitcoin_data():
"""Sample bitcoin price tracker data."""
return {
"bitcoinATH": 109000,
"dailyMax": 108500,
"dailyMin": 107000,
"bitcoinMax": 108800,
"bitcoinMaxBlock": 890000,
}


@pytest.fixture
def sample_prodillos():
"""Sample prodillos prediction data."""
return {
"users": {
"123": {"user": "Alice", "predict": 110000},
"456": {"user": "Bob", "predict": 105000},
"789": {"user": "Charlie", "predict": 108500},
},
"treasury": 1000,
}


@pytest.fixture
def sample_trofeillos():
"""Sample trofeillos champion data."""
return {
"currentChampion": "Alice",
"currentChampionId": "123",
"123": {
"champion": "Alice",
"trofeillos profesionales": ["🏆 [890000]"],
},
}


def write_json(path, data):
"""Helper to write JSON to a file."""
with open(path, "w") as f:
json.dump(data, f, indent=2)


def read_json(path):
"""Helper to read JSON from a file."""
with open(path) as f:
return json.load(f)
185 changes: 185 additions & 0 deletions tests/test_bitcoin_prices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""Tests for Bitcoin price tracking logic.

Tests the price tracking, ATH detection, and daily max/min logic.
External API calls are mocked.
"""
import json
import pytest
from tests.conftest import write_json, read_json


class TestPriceTracking:
"""Test price comparison and tracking logic."""

def test_new_ath_detected(self, bitcoin_file, sample_bitcoin_data):
"""When price > ATH, update ATH."""
write_json(bitcoin_file, sample_bitcoin_data)
data = read_json(bitcoin_file)

new_price = 110000
assert new_price > data["bitcoinATH"]

data["bitcoinATH"] = new_price
write_json(bitcoin_file, data)

updated = read_json(bitcoin_file)
assert updated["bitcoinATH"] == 110000

def test_new_daily_max(self, bitcoin_file, sample_bitcoin_data):
"""When price > dailyMax but < ATH, update dailyMax."""
write_json(bitcoin_file, sample_bitcoin_data)
data = read_json(bitcoin_file)

new_price = 108700 # > dailyMax (108500), < ATH (109000)
assert new_price > data["dailyMax"]
assert new_price < data["bitcoinATH"]

data["dailyMax"] = new_price
write_json(bitcoin_file, data)

updated = read_json(bitcoin_file)
assert updated["dailyMax"] == 108700

Comment on lines +28 to +42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check field names in TypeScript types and implementation
echo "=== BitcoinPriceTracker interface ==="
rg -n "dailyMax|dailyMin|lastReportedMax|lastReportedMin" --type=ts -C2

echo ""
echo "=== Sample fixture in conftest.py ==="
rg -n "dailyMax|dailyMin|lastReportedMax|lastReportedMin" tests/conftest.py

Repository: Fierillo/botillo

Length of output: 3705


🏁 Script executed:

#!/bin/bash
# Check all occurrences of field names in test files
echo "=== All field usages in test_bitcoin_prices.py ==="
cat -n tests/test_bitcoin_prices.py | grep -E "dailyMax|dailyMin|lastReportedMax|lastReportedMin"

echo ""
echo "=== Check if there are other test files referencing these fields ==="
find tests -name "*.py" -type f | xargs grep -l "dailyMax\|dailyMin\|lastReportedMax\|lastReportedMin"

Repository: Fierillo/botillo

Length of output: 1417


🏁 Script executed:

#!/bin/bash
# Check the imports and test structure in test_bitcoin_prices.py
echo "=== Test file imports and fixtures ==="
head -30 tests/test_bitcoin_prices.py

echo ""
echo "=== Check if tests use actual functions or mocks ==="
rg -n "def test_|@pytest|mock|Mock" tests/test_bitcoin_prices.py | head -20

Repository: Fierillo/botillo

Length of output: 2239


🏁 Script executed:

#!/bin/bash
# Check conftest.py to see the fixture definitions
echo "=== conftest.py content ==="
cat tests/conftest.py

Repository: Fierillo/botillo

Length of output: 1931


Field name mismatch between tests and production code.

Tests use dailyMax/dailyMin in fixtures and assertions, but the TypeScript BitcoinPriceTracker interface (src/modules/types.ts:31-32) and production code (src/modules/bitcoinPrices.ts:17-18) use lastReportedMax/lastReportedMin. Update the fixture in tests/conftest.py and all test assertions to use the correct production field names to ensure tests accurately reflect production behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_bitcoin_prices.py` around lines 28 - 42, The tests reference
fields dailyMax/dailyMin which don't match production; update the test fixture
in tests/conftest.py and all assertions in tests/test_bitcoin_prices.py to use
the production names lastReportedMax and lastReportedMin (matching the
BitcoinPriceTracker interface and src/modules/bitcoinPrices.ts), e.g., replace
any read/write/assert of "dailyMax"/"dailyMin" with
"lastReportedMax"/"lastReportedMin" so tests align with the actual code paths in
BitcoinPriceTracker.

def test_new_daily_min(self, bitcoin_file, sample_bitcoin_data):
"""When price < dailyMin, update dailyMin."""
write_json(bitcoin_file, sample_bitcoin_data)
data = read_json(bitcoin_file)

new_price = 106000 # < dailyMin (107000)
assert new_price < data["dailyMin"]

data["dailyMin"] = new_price
write_json(bitcoin_file, data)

updated = read_json(bitcoin_file)
assert updated["dailyMin"] == 106000

def test_price_between_daily_bounds(self, bitcoin_file, sample_bitcoin_data):
"""Price between daily min/max shouldn't trigger updates."""
write_json(bitcoin_file, sample_bitcoin_data)
data = read_json(bitcoin_file)

new_price = 107500 # between 107000 and 108500
assert new_price > data["dailyMin"]
assert new_price < data["dailyMax"]
assert new_price < data["bitcoinATH"]

# No updates should happen
assert data["dailyMin"] == 107000
assert data["dailyMax"] == 108500

def test_bitcoin_max_for_prodillo(self, bitcoin_file, sample_bitcoin_data):
"""bitcoinMax tracks the all-time high for the current prodillo round."""
write_json(bitcoin_file, sample_bitcoin_data)
data = read_json(bitcoin_file)

new_price = 109500
if new_price > data["bitcoinMax"]:
data["bitcoinMax"] = new_price
data["bitcoinMaxBlock"] = 890100

write_json(bitcoin_file, data)
updated = read_json(bitcoin_file)
assert updated["bitcoinMax"] == 109500
assert updated["bitcoinMaxBlock"] == 890100


class TestBitstampResponse:
"""Test parsing of Bitstamp API responses."""

def test_parse_valid_response(self):
"""Valid Bitstamp response parsed correctly."""
mock_response = {
"last": "108500",
"low": "107000",
"high": "109000",
}
price = int(mock_response["last"])
low = int(mock_response["low"])
high = int(mock_response["high"])

assert price == 108500
assert low == 107000
assert high == 109000

def test_parse_decimal_response(self):
"""Response with decimals handled by parseInt."""
mock_response = {"last": "108500.50"}
price = int(mock_response["last"].split(".")[0])
assert price == 108500


class TestDailyReset:
"""Test daily max/min reset logic."""

def test_daily_reset_preserves_ath(self, bitcoin_file):
"""Daily reset should keep ATH and bitcoinMax."""
data = {
"bitcoinATH": 109000,
"dailyMax": 0,
"dailyMin": float('inf'),
"bitcoinMax": 108800,
"bitcoinMaxBlock": 890000,
}
# Simulate reset
data["dailyMax"] = 0
data["dailyMin"] = float('inf')

# These should be preserved
assert data["bitcoinATH"] == 109000
assert data["bitcoinMax"] == 108800
assert data["bitcoinMaxBlock"] == 890000
assert data["dailyMax"] == 0
assert data["dailyMin"] == float('inf')

def test_round_reset_clears_bitcoin_max(self, bitcoin_file, sample_bitcoin_data):
"""After prodillo round ends, bitcoinMax resets to 0."""
write_json(bitcoin_file, sample_bitcoin_data)
data = read_json(bitcoin_file)

# Round ends
data["bitcoinMax"] = 0
data["bitcoinMaxBlock"] = 0
write_json(bitcoin_file, data)

updated = read_json(bitcoin_file)
assert updated["bitcoinMax"] == 0
assert updated["bitcoinMaxBlock"] == 0
# ATH and daily values preserved
assert updated["bitcoinATH"] == 109000


class TestNotificationLogic:
"""Test when notifications should be sent."""

def test_ath_notification(self):
"""ATH notification when price > all previous."""
ath = 109000
new_price = 110000
should_notify = new_price > ath
assert should_notify is True

def test_daily_max_notification(self):
"""Daily max notification when price > dailyMax but < ATH."""
ath = 109000
daily_max = 108500
new_price = 108700
should_notify = new_price > daily_max and new_price < ath
assert should_notify is True

def test_daily_min_notification(self):
"""Daily min notification when price < dailyMin."""
daily_min = 107000
new_price = 106000
should_notify = new_price < daily_min
assert should_notify is True

def test_no_notification_in_range(self):
"""No notification when price is within daily bounds."""
daily_max = 108500
daily_min = 107000
new_price = 107500
should_notify = (
new_price > daily_max or new_price < daily_min
)
assert should_notify is False
Loading