Skip to content

Code quality improvements, bug fixes, and monitoring cycle tests#100

Merged
timothyfroehlich merged 3 commits intomainfrom
feature/code-quality-and-testing
Feb 15, 2026
Merged

Code quality improvements, bug fixes, and monitoring cycle tests#100
timothyfroehlich merged 3 commits intomainfrom
feature/code-quality-and-testing

Conversation

@timothyfroehlich
Copy link
Owner

Summary

  • Bug fixes: Rate limiter sleep removal, timezone defensiveness in poll check, simplified seen-submission transaction handling, extracted duplicate helper function, plus previously-fixed .env loading, quote stripping, and race condition prevention
  • Test cleanup: Deleted 5 trivial tests, consolidated repetitive tests into parameterized versions across 5 files
  • New integration tests: Full monitoring cycle end-to-end test, seen-submission deduplication test, race condition regression test, completed runner E2E TODOs
  • Simulation cleanup: Removed redundant simulation tests covered by new integration tests
  • Initial notifications fix: !add now fetches submissions without date filter so users always see the last 5 changes as confirmation
  • CLAUDE.md rewrite: Added architecture diagram, streamlined sections, removed duplication with docs/

Test plan

  • pytest tests/ --ignore=tests/simulation -v — 248 passed
  • pre-commit run --all-files — clean
  • Manual test: !add location shows submissions, !add city shows submissions
  • Manual test: monitor loop runs without duplicate notifications
  • Verified single-instance operation (Cloud Run stopped)

🤖 Generated with Claude Code

Claude Code added 2 commits February 14, 2026 14:42
…gration tests

Phase 1 - Bug fixes:
- Remove dead asyncio.sleep from rate_limited_request (src/api.py)
- Add naive datetime guard in _should_poll_channel (src/cogs/runner.py)
- Replace rollback/retry with query-first in mark_submissions_seen (src/database.py)
- Extract duplicate ensure_timezone_aware to module level (src/database.py)
- Load .env.local before .env (src/main.py)
- Strip quotes from location input (src/cogs/command_handler.py)
- Mark submissions seen before posting to prevent race condition (src/notifier.py)
- Fix token variable name and async entry point (src/local_dev, local_dev.py)

Phase 2 - Test cleanup:
- Delete 5 trivial tests across 3 files
- Consolidate repetitive tests into @pytest.mark.parametrize versions

Phase 3 - New integration tests:
- Add test_monitoring_cycle.py with full cycle, deduplication, and race condition tests
- Complete TODOs in test_runner_e2e.py with real Runner/Database/Notifier execution

Phase 4 - Simulation cleanup:
- Remove redundant simulation tests (covered by new integration tests)

Also rewrites CLAUDE.md with architecture diagram and streamlined sections.

248 tests pass, pre-commit clean.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <claude-code@anthropic.com>
The initial notification on !add was filtering to yesterday's submissions,
which returned empty for cities/coordinates with no recent activity. Now
fetches without date filter so users always see the last 5 changes as
confirmation that monitoring is working.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <claude-code@anthropic.com>
Copilot AI review requested due to automatic review settings February 15, 2026 21:02
Simulation tests were cleared out in favor of integration tests.
The separate CI step failed with exit code 5 (no tests collected).
Merged into a single test run.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <claude-code@anthropic.com>
@codecov
Copy link

codecov bot commented Feb 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@timothyfroehlich timothyfroehlich merged commit 483b692 into main Feb 15, 2026
8 checks passed
@timothyfroehlich timothyfroehlich deleted the feature/code-quality-and-testing branch February 15, 2026 21:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves reliability and code quality for the Discord pinball monitoring bot by refining initial-notification behavior, hardening time handling, simplifying “seen submission” persistence, and consolidating tests into more maintainable parameterized/integration coverage.

Changes:

  • Adjust initial notifications to fetch submissions without a date filter and add end-to-end monitoring cycle integration tests to prevent duplicate notifications.
  • Refactor and consolidate multiple unit tests into parameterized forms; remove redundant/trivial tests and simulation CRUD-only coverage.
  • Improve robustness in polling time comparisons (timezone defensiveness) and streamline seen-submission handling.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/notifier.py Updates initial submission fetch behavior and marks submissions seen earlier to avoid duplicate notifications.
src/database.py Extracts timezone-awareness helper and changes seen-submission insert strategy.
src/cogs/runner.py Adds defensiveness for naive last_poll_at timestamps.
src/cogs/command_handler.py Strips wrapping quotes from !add location input.
src/api.py Removes fixed inter-request sleep in the request helper.
src/main.py Loads .env.local before .env for local-dev precedence.
src/local_dev/local_dev.py Switches local-dev token env var name to DISCORD_TOKEN.
local_dev.py Runs local dev entrypoint via asyncio.run() with clean KeyboardInterrupt handling.
tests/integration/test_monitoring_cycle.py Adds new full monitoring-cycle integration tests (Runner ↔ DB ↔ Notifier ↔ API).
tests/integration/test_runner_e2e.py Implements runner E2E TODOs by wiring real Runner/Database/Notifier instances.
tests/simulation/test_user_journeys.py Removes CRUD-only “user journey” simulations in favor of real integration tests.
tests/unit/test_notifier.py Parameterizes notifier unit tests and updates expectations for new fetch parameters.
tests/unit/test_main.py Parameterizes HTTP server tests and consolidates cleanup tests.
tests/unit/test_log_config.py Removes a trivial log-level formatting unit test.
tests/unit/test_database_models.py Removes trivial model property/instantiation tests.
tests/unit/test_commands_parsing.py Consolidates parsing tests into parameterized cases.
tests/unit/test_command_handler.py Parameterizes command-handler config tests (poll rate / notifications).
CLAUDE.md Rewrites and streamlines agent/developer guidance and repo architecture notes.

Comment on lines +545 to +549
session.execute(
select(SeenSubmission.submission_id).where(
SeenSubmission.channel_id == channel_id,
SeenSubmission.submission_id.in_(submission_ids),
)
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

mark_submissions_seen uses SeenSubmission.submission_id.in_(submission_ids) with the full submission_ids list. When the list is large (hundreds/thousands), this can be very slow and can hit SQLite’s parameter limit (OperationalError: too many SQL variables) depending on build settings. This should be batched/chunked (and ideally avoid a giant IN (...) altogether).

Copilot uses AI. Check for mistakes.
Comment on lines +555 to +563
# Only insert truly new ones
new_ids = [sid for sid in submission_ids if sid not in existing]
if new_ids:
session.add_all(
[
SeenSubmission(channel_id=channel_id, submission_id=sid)
for sid in new_ids
]
)
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

This implementation removes the previous duplicate-handling behavior. Because seen_submissions has a unique constraint on (channel_id, submission_id), concurrent calls (e.g., send_initial_notifications racing the monitor loop) can still raise IntegrityError between the SELECT and INSERT. Recommend using an atomic “insert if not exists” strategy (e.g., SQLite INSERT OR IGNORE / SQLAlchemy on_conflict_do_nothing) or reintroducing IntegrityError handling.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +66
# Create mock bot with mock channel
mock_bot = MagicMock()
mock_channel = AsyncMock()
mock_channel.id = CHANNEL_ID
mock_bot.get_channel.return_value = mock_channel
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

This test uses raw MagicMock() / AsyncMock() objects. Repo convention requires spec-based mocks via tests/utils/mock_factories.py (see tests/CLAUDE.md “CRITICAL: Mock Specifications Required”). Please switch to the factory helpers (e.g., create_bot_mock(), create_discord_channel_mock(), create_async_notifier_mock()) to ensure interface compliance.

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +151
mock_channel = AsyncMock()
mock_channel.id = CHANNEL_ID
mock_bot.get_channel.return_value = mock_channel
mock_bot.user = MagicMock()
mock_bot.user.name = "TestBot"
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

Same spec-mock convention issue here: avoid raw MagicMock() / AsyncMock() and use the spec-based factories from tests/utils/mock_factories.py as documented in tests/CLAUDE.md. This prevents async/sync misuse and catches interface drift early.

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +244
mock_ctx = AsyncMock()
mock_ctx.channel = AsyncMock()
mock_ctx.channel.id = CHANNEL_ID
mock_ctx.send = AsyncMock()
mock_ctx.author = MagicMock()
mock_ctx.author.name = "TestUser"
mock_ctx.author.id = 99999
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

Same spec-mock convention issue: this test builds a raw AsyncMock() ctx/channel/author. Please use create_discord_context_mock(...) (and related helpers) from tests/utils/mock_factories.py so the ctx/channel interfaces match production usage.

Copilot uses AI. Check for mistakes.
# Submission 505959 is "Black Knight" removal
# Submission 505958 is "King Kong" addition
# These should not be in the sent messages since they're pre-seen
pass # Messages are formatted, hard to check exact IDs; check count instead
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The for msg in sent_messages: ... pass loop doesn’t assert anything, so the test isn’t actually verifying that pre-seen submissions were excluded from notifications. Replace this with a concrete assertion (e.g., assert pre-seen machine names like “Black Knight” / “King Kong” are not present in any sent message, or assert the expected number of notifications based on fixture contents after excluding pre_seen_ids).

Suggested change
pass # Messages are formatted, hard to check exact IDs; check count instead
assert "Black Knight" not in msg
assert "King Kong" not in msg

Copilot uses AI. Check for mistakes.
Comment on lines 60 to 66
# Mock Discord bot and channel for notifications
from unittest.mock import AsyncMock

mock_bot = AsyncMock()
mock_channel = AsyncMock()
# bot.get_channel() is a sync method, so use MagicMock for the bot
mock_bot = MagicMock()
mock_channel = MagicMock()
mock_channel.id = 12345
mock_channel.send = AsyncMock()
mock_bot.get_channel.return_value = mock_channel
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

This test uses raw MagicMock() / AsyncMock() for the bot/channel and stubs methods ad-hoc, but the repo’s testing convention requires spec-based mock factories (tests/utils/mock_factories.py; see tests/CLAUDE.md). Please switch to the factory helpers here too (this file already uses them later, so keeping it consistent should be straightforward).

Copilot uses AI. Check for mistakes.
Comment on lines +85 to 93
# Mark all fetched submissions as seen BEFORE posting, to prevent the
# monitor loop from re-reporting them during the async post_submissions sleeps
if submissions:
submission_ids = [s["id"] for s in submissions if "id" in s]
if submission_ids:
self.db.mark_submissions_seen(ctx.channel.id, submission_ids)

latest_submissions = filtered_submissions[:5]

Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

send_initial_notifications fetches submissions with use_min_date=False (potentially thousands of rows, as in the location_874_recent.json fixture) but then marks all fetched IDs as seen. This can massively bloat seen_submissions and do unnecessary DB work; it also isn’t needed for the race-condition fix (you only need to mark the submissions you’re about to post). Consider marking only the IDs in latest_submissions (or filtered_submissions[:5]) before posting.

Suggested change
# Mark all fetched submissions as seen BEFORE posting, to prevent the
# monitor loop from re-reporting them during the async post_submissions sleeps
if submissions:
submission_ids = [s["id"] for s in submissions if "id" in s]
if submission_ids:
self.db.mark_submissions_seen(ctx.channel.id, submission_ids)
latest_submissions = filtered_submissions[:5]
latest_submissions = filtered_submissions[:5]
# Mark only the submissions we are about to post as seen BEFORE posting,
# to prevent the monitor loop from re-reporting them during the async
# post_submissions sleeps
if latest_submissions:
submission_ids = [s["id"] for s in latest_submissions if "id" in s]
if submission_ids:
self.db.mark_submissions_seen(ctx.channel.id, submission_ids)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants