Skip to content

[script][status-monitor] Refactor: bug fixes, SOLID decomposition, SQLite storage - #7402

Open
MahtraDR wants to merge 55 commits into
elanthia-online:mainfrom
MahtraDR:refactor/status-monitor-modernize
Open

[script][status-monitor] Refactor: bug fixes, SOLID decomposition, SQLite storage#7402
MahtraDR wants to merge 55 commits into
elanthia-online:mainfrom
MahtraDR:refactor/status-monitor-modernize

Conversation

@MahtraDR

@MahtraDR MahtraDR commented May 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Refactors status-monitor.lic from a monolithic GameFilter class into focused, testable modules and fixes several bugs in the existing implementation.

Bug fixes

  • Gdk::RBGA typo -- crashed health bar rendering when HP dropped below 75%
  • Kernel#open -> File.open -- closed a command injection vector in Marshal file loading
  • File handle leaks -- Marshal.load calls now properly close file handles
  • similarity_scrub mutation -- no longer mutates its argument via gsub!
  • @room_players unbounded growth -- resets on room change instead of accumulating forever
  • @filter_strings busy-wait -- retries with backoff instead of spinning on nil
  • Double Slack notification -- deduplicates alert delivery
  • Duplicate @non_useful_tags -- removed repeated death, percWindow entries

Structural changes

  • Decomposes GameFilter into focused classes: MessageStore, MessageFilter, SpamDetector, AlertHandler, CommandDetector, Monitor
  • Replaces Marshal .dat persistence with SQLite (WAL mode) for the seen-messages corpus
  • Auto-migrates existing .dat files on first run, then renames to .dat.migrated
  • Switches Slack delivery from the shared register_slackbot/send_slackbot_message globals to a dedicated, lazily-constructed Lich::DragonRealms::SlackBot instance (delivery delegates reconnection to direct_message)

New script

  • Adds status-monitor-import.lic for bulk-importing game session logs (.log and .log.gz) into the SQLite corpus. Resumable, tracks processed files.

Review follow-ups

  • Slack alert delivery -- AlertHandler#send_slack no longer gates on initialized?; that gate permanently suppressed delivery whenever the first connection attempt failed (e.g. lnet not up at login), because direct_message's own reconnect path was never reached
  • Lazy Slack init -- the SlackBot is constructed on the first alert instead of at script start, so the status window is no longer blocked behind lnet/users.list startup
  • Multi-instance log import -- find_log_files globs every *-<Character> log directory (DR/DRT/DRX/GSIV) instead of hardcoding the DR- prefix
  • --limit semantics -- the limit now caps not-yet-imported files, so a resumed limited run makes forward progress instead of re-selecting already-imported files
  • MessageFilter#clean -- no longer mutates its argument (tag stripping returns a new String; safe against frozen input)
  • Command auto-execution -- destructive/session-ending verbs (QUIT, SELL, DROP, DUMP, EMPTY, WITHDRAW, RELEASE, GET, PUT, KILL) are denylisted from CommandDetector
  • Docs/rubocop -- documented last_player_line, named the recent-cache TTL constant, and kept the adversarial spec inputs ASCII-only so the repo cops pass
  • Test coverage added for each of the above (169 examples, rubocop clean)

Test plan

  • Run ;status-monitor on a character with an existing .dat file -- verify auto-migration to SQLite and .dat.migrated rename
  • Run ;status-monitor on a fresh character with no prior data -- verify clean SQLite database creation
  • Verify health bar colors update correctly at HP thresholds (especially < 75%)
  • Confirm Slack notifications fire once per alert, not duplicated
  • Confirm a Slack alert is delivered even when the character logs in before lnet is connected
  • Run ;status-monitor-import to populate corpus from logs, then re-run to verify resumability
  • Verify --limit=N imports N not-yet-imported files on a resumed run
  • Verify a character with logs on multiple instances (e.g. DR- and DRT-) imports all of them
  • Verify --reset flag on import clears tracking and re-processes all files

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added per-character log ingestion (plain and gzip) into a SQLite-backed message corpus, including deduping, --reset, and progress/completion reporting.
    • Reworked the status monitoring pipeline to use an SQLite MessageStore, improved message cleaning/similarity normalization, spam alerting, and embedded command detection/execution.
  • Bug Fixes
    • Fixed health bar color parsing.
  • Tests
    • Added extensive RSpec coverage for log import behavior, cleaning/scrubbing rules, database operations, command detection, spam detection, and alert handling.

- Fix Gdk::RBGA typo that crashed health bar when HP < 75%
- Fix Kernel#open -> File.open (command injection risk)
- Fix file handle leaks in Marshal.load calls
- Fix similarity_scrub mutating its argument
- Fix @room_players unbounded growth (reset on room change)
- Fix @filter_strings busy-wait (retry with backoff)
- Fix double Slack notification
- Remove duplicate @non_useful_tags entries
- Decompose monolithic GameFilter into focused classes:
  MessageStore, MessageFilter, SpamDetector, AlertHandler,
  CommandDetector, Monitor
- Replace Marshal .dat persistence with SQLite (WAL mode)
  with auto-migration from legacy .dat files
- Update Slack API from removed register_slackbot to
  Lich::DragonRealms::SlackBot.direct_message
- Add YARD documentation per dr-scripts style guide
- Add status-monitor-import.lic for populating SQLite corpus
  from game session logs (handles .log and .log.gz, resumable)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f1edbca0-beb3-4c4d-b0f6-ce8c24b1aac5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a CLI script to import and normalize character logs into per-character SQLite corpora, and refactors the status monitor into modular SQLite-backed storage, filtering, spam detection, alert handling, command detection, and runtime integration with comprehensive tests.

Changes

Log Import Pipeline

Layer / File(s) Summary
Import script setup and contracts
status-monitor-import.lic
Defines CLI options, runtime dependencies, and normalization constants for log ingestion.
Line normalization and database persistence
status-monitor-import.lic
Creates SQLite tables, tracks imported files, normalizes and scrubs lines, and batch-inserts deduplicated import rows.
File discovery and import orchestration
status-monitor-import.lic
Discovers plain and gzip logs, supports limits and reset, skips completed files, records progress, and handles per-file errors.
Import pipeline validation
spec/status_monitor_import_spec.rb
Tests normalization, database operations, gzip imports and failures, reset behavior, discovery, and resumable processing.

StatusMonitor Refactor

Layer / File(s) Summary
Persistence and message filtering
status-monitor.lic
Adds SQLite persistence with Marshal migration and preprocessing for XML, stream state, configured filters, room/player content, and similarity normalization.
Spam detection and alert handling
status-monitor.lic
Adds repeat, similarity, and time-window detection, alert side effects, Slack delivery, responses, and obfuscated command execution.
Monitor orchestration and runtime wiring
status-monitor.lic
Connects filtering, detection, persistence, and alert consumption, and updates headless and GTK processing and shutdown flows.
StatusMonitor test harness and behavior coverage
spec/status_monitor_spec.rb
Tests storage migration, filtering, spam heuristics, command detection, alert effects, and Monitor ordering and lifecycle.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LogFiles
  participant StatusMonitorImport
  participant SQLite
  LogFiles->>StatusMonitorImport: discover pending log files
  StatusMonitorImport->>LogFiles: stream plain or gzip lines
  StatusMonitorImport->>StatusMonitorImport: clean and similarity-scrub lines
  StatusMonitorImport->>SQLite: batch INSERT OR IGNORE and record file
Loading
sequenceDiagram
  participant GameStream
  participant Monitor
  participant MessageFilter
  participant SpamDetector
  participant MessageStore
  GameStream->>Monitor: process raw line
  Monitor->>MessageFilter: clean and filter line
  Monitor->>SpamDetector: check processed line
  Monitor->>MessageStore: check unseen line
  Monitor-->>GameStream: return unseen state or queued alert
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactor, bug fixes, and SQLite storage changes in the status-monitor scripts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
status-monitor.lic (1)

640-641: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the debug copy when the poll returns no line.

script.gets? can return nil in GTK mode; with debug enabled, nil.dup raises and kills the monitor on an idle poll.

Suggested fix
-    back = line.dup if args.debug
+    back = line.dup if args.debug && line
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@status-monitor.lic` around lines 640 - 641, The code calls script.gets? and
then unconditionally dup's it when args.debug is true, causing a crash if
script.gets? returned nil; modify the logic around the call to script.gets? and
the debug copy creation (the variables line, script.gets?, back, and args.debug)
so you only call line.dup when line is non-nil (e.g., check line before
duplicating or combine the conditions), ensuring back is only assigned from
line.dup if both args.debug is true and line is truthy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@status-monitor-import.lic`:
- Line 216: The block argument idx from files.each_with_index is unused and
triggers RuboCop Lint/UnusedBlockArgument; change the iteration to avoid the
unused variable by either using files.each (replace files.each_with_index) or,
if the index is needed elsewhere, rename the unused arg to _idx to mark it
intentionally unused; update the call site referencing files.each_with_index
accordingly so no unused block arguments remain.
- Around line 159-165: flush_batch currently returns lines.size even though
INSERT OR IGNORE may skip duplicates; change flush_batch (the method that runs
db.transaction and executes the INSERT OR IGNORE loop) to compute and return the
actual number of rows inserted by summing the database's change count after each
execute (e.g., after each db.execute call call the DB change counter and add to
an inserted_count) or call an equivalent API (total_changes/delta) supported by
your sqlite adapter and return that inserted_count instead of lines.size.
- Around line 193-195: The --reset branch only deletes import_log
(db.execute('DELETE FROM import_log')) but leaves previously inserted rows (with
source = 'import') in the main tables because flush_batch uses INSERT OR IGNORE;
update the reset handling to fully remove or mark for reprocessing all rows
originating from imports by deleting rows where source = 'import' (or otherwise
truncating the affected tables) and/or change the import logic so reprocessing
can overwrite existing rows (remove INSERT OR IGNORE in flush_batch or replace
with INSERT OR REPLACE/UPSERT) so that rerunning after --reset will truly
rebuild the corpus and reapply scrubbing/cleaning logic.
- Around line 40-44: The timestamp skip regex in SKIP_PATTERNS is too permissive
and matches lines that only start with a timestamp before clean_line strips
prefixes; update the timestamp pattern in SKIP_PATTERNS to only match bare
timestamp-only lines (e.g. add an end anchor so /^\d{4}-\d{2}-\d{2}
\d{2}:\d{2}:\d{2}$/) so normal log lines with a timestamp prefix are not
dropped, or alternatively move the SKIP_PATTERNS check inside the clean_line
function to run after the prefix-stripping logic; reference SKIP_PATTERNS and
clean_line when making the change.

In `@status-monitor.lic`:
- Around line 560-569: The headless branch enters the infinite loop (nowindow)
before the shutdown hook is registered, so monitor.save never runs; move the
before_dying registration to run before entering the nowindow loop (register
before_dying { monitor.save } or equivalent), ensuring the same hook is set in
both the headless path and the normal path; update the code around nowindow and
the infinite loop that calls monitor.process, write_debug_log, and
monitor.consume_spam_line so the before_dying registration occurs prior to
entering the loop (also apply the same change to the analogous block around
lines 590-595).
- Around line 485-488: The code currently checks `@store.unseen`? before calling
`@detector.check`, so exact repeats never reach SpamDetector#check and
counts.values.max stays at 1; move the call to `@detector.check`(scrubbed) so
duplicates are fed into the detector before you short-circuit on
`@store.unseen`?(scrubbed), and then mark the scrubbed line as seen after
detector.check returns (i.e., call `@detector.check`(scrubbed) first, then return
false unless `@store.unseen`?(scrubbed), or alternatively always call
`@detector.check` and only skip further processing when unseen? is false) so the
repeat-counting branch in SpamDetector can increment properly.
- Around line 525-526: The lookup of filter strings must be nil-guarded so a nil
return from get_data('filters') doesn't raise and prevents the retry/backoff;
update the code that computes filters (the assignment that uses
get_data('filters') and the key 'filter_strings') to safely handle a nil data
value (e.g., use safe navigation or a nil-coalescing default) before calling map
so filters becomes nil or an empty list instead of raising, allowing the
surrounding retry loop to continue.
- Around line 417-433: The two scanners can emit the same command twice (e.g.,
"J_U_M_P")—modify the logic to deduplicate per input line: create a local
collection (e.g., sent = Set.new or array) and before calling
fput(normalized_command) check if the normalized command (use a consistent form
like downcase or upcase) is already in sent; if not, add it to sent and call
fput. Apply this check for both the uppercase-strip scanner (the block using
line.gsub(...).scan(/[A-Z\s]{3,}/) and its fput(match.downcase)) and the token
scanner (the block iterating line.split(' ').each and fput(cleaned.downcase)),
ensuring both use the same normalization and that the pause calls around fput in
the first scanner remain unchanged.

---

Outside diff comments:
In `@status-monitor.lic`:
- Around line 640-641: The code calls script.gets? and then unconditionally
dup's it when args.debug is true, causing a crash if script.gets? returned nil;
modify the logic around the call to script.gets? and the debug copy creation
(the variables line, script.gets?, back, and args.debug) so you only call
line.dup when line is non-nil (e.g., check line before duplicating or combine
the conditions), ensuring back is only assigned from line.dup if both args.debug
is true and line is truthy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 39814e55-da3b-4174-af9d-e9c3a6e17729

📥 Commits

Reviewing files that changed from the base of the PR and between 5590b60 and 8ed7213.

📒 Files selected for processing (2)
  • status-monitor-import.lic
  • status-monitor.lic

Comment thread status-monitor-import.lic
Comment thread status-monitor-import.lic
Comment thread status-monitor-import.lic Outdated
Comment thread status-monitor-import.lic Outdated
Comment thread status-monitor.lic Outdated
Comment thread status-monitor.lic Outdated
Comment thread status-monitor.lic
Comment thread status-monitor.lic
MahtraDR and others added 6 commits May 18, 2026 17:14
- Add MessageStore#shutdown and Monitor#shutdown to explicitly close
  the SQLite connection after flushing pending data
- Register before_dying with monitor.shutdown before the nowindow
  branch so both headless and GTK modes flush and close on exit
- Rename line_hash column to line_text (stores full scrubbed text,
  not a hash digest)
- Change import progress labels from "lines" to "lines scanned"
  since INSERT OR IGNORE skips duplicates silently

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove timestamp regex from import SKIP_PATTERNS -- it was matching
  all timestamped log lines, skipping ~85% of content before the
  TIMESTAMP_PATTERN strip could extract it
- Reset now also deletes source='import' rows from seen_messages so
  re-import actually re-derives the corpus
- Deduplicate CommandDetector matches with a Set before executing,
  preventing double-execution of obfuscated tokens like J_U_M_P
  that match both scanners
- Run SpamDetector before the unseen? gate so repeated lines within
  the 10-minute recent window are still counted by the detector,
  restoring the upstream repeat-threshold behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused require 'fileutils' from import script
- Add sync comments on duplicated similarity_scrub and schema
  between status-monitor.lic and status-monitor-import.lic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 115 adversarial specs covering SpamDetector, MessageStore,
  CommandDetector, MessageFilter, Monitor, and StatusMonitorImport
- Fix sqlite3 2.x compatibility: wrap bind params in arrays for
  execute() and get_first_value() calls (splat args removed in 2.x)
- Specs found this real bug: all INSERT/SELECT with bind params
  would crash on sqlite3 >= 2.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@MahtraDR

Copy link
Copy Markdown
Collaborator Author

Re: out-of-diff comment about script.gets? returning nil and line.dup crashing in GTK debug mode (line 666):

No fix needed. nil.dup returns nil without raising in Ruby 2.4+ (lich requires Ruby 3.x). The nil then propagates into monitor.process(nil) where MessageFilter#clean returns nil early on the line.nil? check (line 199). This is the same behavior as upstream (line 380). Verified with ruby -e "puts nil.dup.inspect" -> nil.

@MahtraDR

Copy link
Copy Markdown
Collaborator Author

@coderabbitai - update review on this pr to include all commits.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@MahtraDR Thanks for the clarification on nil.dup — that's a good point. In Ruby 2.4+, nil.dup returns nil without raising, and with the line.nil? guard in MessageFilter#clean the nil is handled gracefully. I'll note that for future reviews.

Triggering a full review now to cover all commits in the PR.


✏️ Learnings added
Learnt from: MahtraDR
URL: https://github.com/elanthia-online/dr-scripts/pull/7402

Timestamp: 2026-05-18T07:03:56.719Z
Learning: In status-monitor.lic (dr-scripts repo), `nil.dup` returning nil in Ruby 2.4+ is intentional. When `script.gets?` returns nil in GTK debug mode (line 666), nil propagates into `monitor.process(nil)` and is safely handled by the `line.nil?` early-return guard in `MessageFilter#clean` (line 199). No fix is needed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
spec/status_monitor_spec.rb (1)

619-623: 💤 Low value

Unused helper method stub_get_data.

This helper is defined but never called—all Monitor tests define get_data inline via Object.send(:define_method, ...) instead. Consider removing it to reduce confusion.

🧹 Proposed removal
-  def stub_get_data
-    define_method(:get_data) do |_type|
-      OpenStruct.new('filter_strings' => ['ignore_this'])
-    end
-  end
-
   # get_data must be defined at top level for Monitor to call it
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/status_monitor_spec.rb` around lines 619 - 623, Remove the unused helper
method stub_get_data (which defines get_data) since none of the Monitor tests
call it and every test defines get_data inline via
Object.send(:define_method,...); delete the stub_get_data method definition to
avoid confusion or, if you prefer to keep a reusable helper, replace inline
Object.send(:define_method, ...) usages with calls to stub_get_data so get_data
is defined consistently across tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@status-monitor-import.lic`:
- Around line 138-140: The similarity scrub currently checks only
scrubbed.empty?, so whitespace-only strings like "   " pass through; update the
conditional around the result of similarity_scrub (the local variable scrubbed
used after calling similarity_scrub(cleaned)) to strip whitespace before
checking emptiness (e.g., use scrubbed.strip.empty? or strip! then empty?) so
whitespace-only rows are skipped and not inserted.

---

Nitpick comments:
In `@spec/status_monitor_spec.rb`:
- Around line 619-623: Remove the unused helper method stub_get_data (which
defines get_data) since none of the Monitor tests call it and every test defines
get_data inline via Object.send(:define_method,...); delete the stub_get_data
method definition to avoid confusion or, if you prefer to keep a reusable
helper, replace inline Object.send(:define_method, ...) usages with calls to
stub_get_data so get_data is defined consistently across tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29e8fac7-474a-4ea7-b363-98c53ec02346

📥 Commits

Reviewing files that changed from the base of the PR and between 5590b60 and d048674.

📒 Files selected for processing (4)
  • spec/status_monitor_import_spec.rb
  • spec/status_monitor_spec.rb
  • status-monitor-import.lic
  • status-monitor.lic

Comment thread status-monitor-import.lic Outdated
- Add .strip before .empty? check so whitespace-only strings left by
  currency/digit removal don't get inserted as noise rows.
- Remove unused stub_get_data helper from spec (never called, also
  would not work correctly -- uses bare define_method instead of
  Object.send).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@MahtraDR

MahtraDR commented May 18, 2026

Copy link
Copy Markdown
Collaborator Author

Addressing @CodeRabbit review (4307953260):

status-monitor-import.lic:138-140 (whitespace after similarity_scrub) -- Fixed.
Added .strip before .empty? so strings like " " left after currency/digit removal are properly skipped. Replied inline.

spec/status_monitor_spec.rb:619-623 (unused stub_get_data) -- Fixed.
Removed the dead helper. It was never called (all tests use Object.send(:define_method, :get_data) inline), and it was also broken -- bare define_method would define on the example group, not on Object where Monitor needs it.

Both fixes in 00b2f49.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@MahtraDR Thanks for the follow-up — both fixes look correct:

  • .strip before .empty?: The right call. After similarity_scrub strips digits and currency words, strings containing only whitespace like " " would have silently passed through and been stored as noise in the corpus. .strip.empty? closes that gap cleanly.
  • Removing stub_get_data: Good catch on both counts — it was dead code (never invoked) and would have scoped get_data to the RSpec example group rather than Object, meaning it would have silently failed to stub the method where Monitor actually looks for it.

Both addressed in 00b2f498. 🐇✨


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@MahtraDR

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review this pr again taking all commits into account together.

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

@MahtraDR Sure! Kicking off a full review now covering all commits together — including the bug fixes, SOLID decomposition, SQLite storage changes, and the two follow-up fixes from 00b2f498.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
spec/status_monitor_spec.rb (1)

619-670: ⚡ Quick win

Avoid redefining Object#get_data in these examples.

Each example adds and then removes a global Object method. If another spec/support file already defines get_data, this file will delete it in ensure and make the suite order-dependent. Stub StatusMonitor::Monitor#get_data instead.

Proposed refactor
 RSpec.describe StatusMonitor::Monitor do
   let(:tmpdir) { Dir.mktmpdir('status-monitor-test') }
+  let(:filter_data) { OpenStruct.new('filter_strings' => []) }
   let(:settings) do
     OpenStruct.new(
       unique_line_threshold: 3,
@@
   before do
     $echo_messages.clear
     `@original_dir` = Dir.pwd
     Dir.chdir(tmpdir)
+    allow_any_instance_of(described_class)
+      .to receive(:get_data).with('filters').and_return(filter_data)
   end
@@
   it 'detector runs before unseen? gate (spam detection regression test)' do
-    # Define get_data in scope
-    Object.send(:define_method, :get_data) do |_type|
-      OpenStruct.new('filter_strings' => [])
-    end
-
     monitor = described_class.new(settings)
@@
-  ensure
-    Object.send(:remove_method, :get_data) if Object.method_defined?(:get_data)
   end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/status_monitor_spec.rb` around lines 619 - 670, These examples are
defining and removing Object#get_data globally; instead stub the instance method
on the monitor class (StatusMonitor::Monitor / described_class) — replace
Object.send(:define_method, :get_data) { ... } and the ensure
Object.remove_method calls with a local stub such as stubbing described_class
(or monitor) to receive(:get_data).and_return(OpenStruct.new('filter_strings' =>
[])) before creating/using monitor (affecting examples that call monitor.process
and monitor.consume_spam_line), so the tests no longer mutate Object and you can
remove the ensure cleanup blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@status-monitor-import.lic`:
- Around line 33-36: The TIMESTAMP_PATTERN currently only matches prefixes
ending with ": " so bare timestamp-only headers (e.g., "2026-01-04 18:59:20.727
+13:00") survive cleaning and get into seen_messages; update TIMESTAMP_PATTERN
to also match standalone timestamps (no trailing colon/space) and ensure
clean_line uses this updated pattern to strip those headers before
similarity_scrub runs so timestamp-only lines are removed and never added to
seen_messages; reference TIMESTAMP_PATTERN, clean_line, similarity_scrub, and
seen_messages when making the change.
- Around line 217-224: Wrap the call to import_file (inside the
files.each_with_index loop that currently calls already_imported?, import_file,
and record_import) in a begin/rescue that catches Zlib::GzipFile::Error,
EOFError, and SystemCallError, logs the error with the file name, increments the
skipped/failed counter, and then nexts to continue the loop; keep record_import
only on successful imports and ensure any other exceptions still bubble (or are
handled separately) as intended.

In `@status-monitor.lic`:
- Around line 143-144: The backup path currently prepends "seen_messages_" to
File.basename(`@legacy_dat`, '.dat'), causing duplication because `@legacy_dat`
already includes that prefix; change the backup creation to not add the extra
prefix (e.g., build backup using "backup/#{File.basename(`@legacy_dat`,
'.dat')}.bak" or strip a leading "seen_messages_" from
File.basename(`@legacy_dat`, '.dat') before concatenation) and keep the
File.rename(backup, "#{backup}.migrated") if File.exist?(backup) logic unchanged
so the correct backup/seen_messages_<char>.bak file is migrated.
- Around line 510-518: After calling similarity_scrub and storing it in
scrubbed, skip processing when the scrubbed line collapses to only whitespace
(e.g. check scrubbed.strip.empty?), so do not call `@detector.check`,
`@alerter.fire`, or `@store.unseen`? and do not set `@spam_line` for that input;
implement this as a guard immediately after scrubbed =
`@filter.similarity_scrub`(cleaned) to return/next or wrap the downstream logic in
an unless scrubbed.strip.empty? block so whitespace-only tokens are dropped
before passing to SpamDetector and MessageStore.
- Line 20: The code calls Set.new in the CommandDetector.check method but never
requires Ruby's stdlib Set, causing NameError at runtime; add require 'set' near
the other requires (e.g., alongside require 'sqlite3') so Set is loaded before
CommandDetector.check invokes Set.new and any other Set use.

---

Nitpick comments:
In `@spec/status_monitor_spec.rb`:
- Around line 619-670: These examples are defining and removing Object#get_data
globally; instead stub the instance method on the monitor class
(StatusMonitor::Monitor / described_class) — replace Object.send(:define_method,
:get_data) { ... } and the ensure Object.remove_method calls with a local stub
such as stubbing described_class (or monitor) to
receive(:get_data).and_return(OpenStruct.new('filter_strings' => [])) before
creating/using monitor (affecting examples that call monitor.process and
monitor.consume_spam_line), so the tests no longer mutate Object and you can
remove the ensure cleanup blocks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 697b315e-ed73-4dcb-b3e9-cc7be50c3d45

📥 Commits

Reviewing files that changed from the base of the PR and between 5590b60 and 00b2f49.

📒 Files selected for processing (4)
  • spec/status_monitor_import_spec.rb
  • spec/status_monitor_spec.rb
  • status-monitor-import.lic
  • status-monitor.lic

Comment thread status-monitor-import.lic
Comment thread status-monitor-import.lic Outdated
Comment thread status-monitor.lic
Comment thread status-monitor.lic Outdated
Comment thread status-monitor.lic Outdated
MahtraDR and others added 2 commits May 19, 2026 13:55
CodeRabbit fixes:
- Skip bare timestamp session headers in import clean_line
- Rescue corrupt/truncated .gz files in import loop, continue with remaining
- Fix doubled seen_messages_ prefix in backup migration path
- Guard against whitespace-only lines after similarity_scrub

Hardening:
- CommandDetector: strip non-ASCII before scanning (closes zero-width char evasion)
- SpamDetector: replace 20-entry rolling buffer with time-windowed counts
  (entries expire after 90s, no fixed-size cap to flush)
- MessageFilter: proper stream state tracking (like xmlparser.rb) for all
  filtered streams, not just percWindow. Fixes multi-line stream content
  leaking through and closes percWindow injection gap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Import specs:
- Bare timestamp edge cases (milliseconds-only, trailing content guard)
- Corrupt .gz file: raises directly, skips in loop and continues
- Truncated .gz file (EOFError path)
- Pipeline: lines that clean but scrub to empty/whitespace are excluded

Monitor specs:
- Marshal migration with non-Hash data (Array) survives via rescue
- Backup path correctness assertion (verifies no doubled prefix)
- unseen? rejects tabs, newlines, mixed whitespace
- Double shutdown is safe (before_dying can fire twice)
- SpamDetector: time-windowed retention, buffer flooding resistance, expiry
- SpamDetector: similarity percentage boundaries (0% and 100%)
- CommandDetector: dot/hyphen separators, bare commands, embedded uppercase,
  Cyrillic evasion (known gap), zero-width evasion (now detected),
  mixed-case separators, long lines
- MessageFilter: all filtered streams block content, non-filtered pass,
  percWindow injection no longer triggers from embedded text
- AlertHandler: beeps, command execution, response/no-response, exit/no-exit
- Monitor pipeline: scrub-to-whitespace returns false, filtered line returns false

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@MahtraDR

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review commit 1553e94 and commit b2fa2d6

These two commits address all findings from your previous review:

1553e94 - Source fixes:

  • Bare timestamp headers now filtered via BARE_TIMESTAMP_PATTERN
  • Corrupt .gz files rescued and skipped in import loop
  • Backup migration path fixed (was doubling seen_messages_ prefix)
  • Whitespace-only scrubbed lines now guarded in both Monitor#process and MessageStore#unseen?
  • CommandDetector strips non-ASCII before scanning (closes zero-width char evasion)
  • SpamDetector uses time-windowed counts instead of flushable 20-entry buffer
  • MessageFilter uses proper stream state tracking (like xmlparser.rb) for all filtered streams

b2fa2d6 - Comprehensive edge case, boundary, and adversarial specs (36 new tests, 152 total)

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

@MahtraDR Thanks for the detailed breakdown! Both commits look like substantial improvements — the time-windowed SpamDetector, proper stream-state tracking in MessageFilter, and the non-ASCII strip in CommandDetector in particular address the more complex findings. Triggering a full review now to cover all commits together, including 1553e94b and b2fa2d6a.

✅ Actions performed

Full review triggered.

MahtraDR and others added 2 commits May 19, 2026 13:56
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
spec/status_monitor_spec.rb (1)

249-261: 💤 Low value

Unused let(:original_dir) definition.

Line 250 defines let(:original_dir) but the before/after blocks use @original_dir instance variable instead. Remove the unused let to avoid confusion.

Remove unused let
 RSpec.describe StatusMonitor::MessageStore do
   let(:tmpdir) { Dir.mktmpdir('status-monitor-test') }
-  let(:original_dir) { Dir.pwd }

   before do
     $echo_messages.clear
     `@original_dir` = Dir.pwd
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/status_monitor_spec.rb` around lines 249 - 261, Remove the unused let
definition let(:original_dir) from the spec; the setup/teardown use the
`@original_dir` instance variable in the before and after blocks, so delete the
redundant let(:original_dir) declaration to avoid confusion and keep only the
`@original_dir` usage in before/after.
spec/status_monitor_import_spec.rb (1)

24-24: ⚡ Quick win

Orphaned temp directory at module load time.

LICH_DIR is created via Dir.mktmpdir when the spec file loads but is never cleaned up. This will accumulate orphaned directories across test runs. Consider using an after(:all) hook or moving this into a let with proper cleanup.

Proposed fix using after(:all) cleanup
-LICH_DIR = Dir.mktmpdir('lich-test-import') unless defined?(LICH_DIR)
+LICH_DIR = Dir.mktmpdir('lich-test-import') unless defined?(LICH_DIR)
+
+RSpec.configure do |config|
+  config.after(:suite) do
+    FileUtils.rm_rf(LICH_DIR) if defined?(LICH_DIR) && File.exist?(LICH_DIR)
+  end
+end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/status_monitor_import_spec.rb` at line 24, LICH_DIR is being created at
module load with Dir.mktmpdir and never removed, leaving orphaned temp dirs;
change this so LICH_DIR is created in a test lifecycle hook (e.g., before(:all))
or as a memoized let and add an after(:all) that calls
FileUtils.remove_entry_secure(LICH_DIR) (ensure FileUtils is required), or if
using let add an around hook to ensure cleanup; update references to use the new
LICH_DIR initialization point and remove the top-level Dir.mktmpdir call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@spec/status_monitor_import_spec.rb`:
- Line 24: LICH_DIR is being created at module load with Dir.mktmpdir and never
removed, leaving orphaned temp dirs; change this so LICH_DIR is created in a
test lifecycle hook (e.g., before(:all)) or as a memoized let and add an
after(:all) that calls FileUtils.remove_entry_secure(LICH_DIR) (ensure FileUtils
is required), or if using let add an around hook to ensure cleanup; update
references to use the new LICH_DIR initialization point and remove the top-level
Dir.mktmpdir call.

In `@spec/status_monitor_spec.rb`:
- Around line 249-261: Remove the unused let definition let(:original_dir) from
the spec; the setup/teardown use the `@original_dir` instance variable in the
before and after blocks, so delete the redundant let(:original_dir) declaration
to avoid confusion and keep only the `@original_dir` usage in before/after.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 090c5e53-cd52-4a86-a40b-9da59c56ca19

📥 Commits

Reviewing files that changed from the base of the PR and between 00b2f49 and 2134fc0.

📒 Files selected for processing (4)
  • spec/status_monitor_import_spec.rb
  • spec/status_monitor_spec.rb
  • status-monitor-import.lic
  • status-monitor.lic
🚧 Files skipped from review as they are similar to previous changes (2)
  • status-monitor-import.lic
  • status-monitor.lic

- Remove unused `let(:original_dir)` in status_monitor_spec.rb
- Add after(:suite) hook to clean up LICH_DIR temp directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@MahtraDR

Copy link
Copy Markdown
Collaborator Author

@coderabbitai Both nitpicks from your review are fixed in 509d174:

  1. Unused let(:original_dir) -- removed from status_monitor_spec.rb
  2. Orphaned LICH_DIR temp directory -- added after(:suite) cleanup hook in status_monitor_import_spec.rb

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@MahtraDR Both fixes look exactly right:

  1. Unused let(:original_dir) — clean removal; dead let bindings in RSpec can silently waste setup work and mislead future readers about test scope.
  2. after(:suite) cleanup hook — the correct hook level for teardown that spans the entire suite run, ensuring the temp LICH_DIR directory is cleaned up regardless of which example fails or passes.

Thanks for the thorough follow-through on all the review findings! 🐇✨

MahtraDR and others added 9 commits July 14, 2026 19:54
AlertHandler#send_slack gated delivery on `@slackbot&.initialized?`. That
gate is wrong: SlackBot#direct_message already self-heals via
`reconnect! unless initialized?` (see lib/dragonrealms/commons/slackbot.rb).
By returning early whenever the bot was not initialized, we guaranteed the
reconnect path could never run.

Concrete failure: status-monitor is commonly auto-started at login. On a
session with no valid cached UserVars.slack_token, SlackBot.new needs lnet,
which is frequently not connected yet, so the constructor returns with
@initialized == false (it does not raise). From that point send_slack
returned early for the entire session and NO alert was ever delivered, even
though direct_message would have reconnected on the first real alert.

The pre-refactor code did not have this bug: send_slackbot_message in
dependency.lic only checked that the instance existed, then let
direct_message handle (re)connection.

Fix: guard on the presence of @slackbot, not on initialized?, so the
self-healing reconnect inside direct_message is reached on the first alert.

Adds a regression test (with a network-free fake SlackBot) proving delivery
happens when the bot reports initialized? == false. Verified the test fails
against the old gated code and passes with the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AlertHandler previously built the SlackBot in its constructor via
init_slackbot, and AlertHandler.new runs inside Monitor.new at script
startup (before the GTK window is created). SlackBot.new can block for
several seconds: it may start lnet, wait up to 30s for the lnet connection,
fetch the Slack users list, and sleep on randomized jitter/backoff. That
delayed the status window appearing on every launch.

Defer construction to the first alert: the window comes up immediately, and
by the time a spam alert actually fires, lnet is far more likely to be
connected (which also improves the odds the very first send succeeds).

- Replaces init_slackbot with a memoized private #slackbot builder.
- Precomputes @slack_enabled from slack_username so send_slack short-circuits
  cheaply when Slack is not configured.
- Construction failures are swallowed (logged) and retried on the next alert,
  matching direct_message's own resilience.

Tests: assert no SlackBot is constructed at initialize time, none is
constructed without a username, and exactly one is constructed and reused
across multiple alerts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
find_log_files hardcoded the "DR-" prefix on the per-character log
directory, so it silently ignored logs from any other instance. Lich names
these directories "<GAMECODE>-<Character>", and real installs contain
DR-, DRT- (test), DRX- (platinum) and GSIV- directories side by side.
A character played on more than one instance had all non-prime logs skipped,
with only a misleading "Log directory not found" message.

The live seen-messages database is scoped by character only, not by
instance, so the corpus already blends instances. Match that by globbing
every "*-<Character>" directory and importing them all.

The glob is anchored on "-<Character>", so it will not match a different
character whose name is a superstring (e.g. importing "Zz" does not pull in
"DR-Zzextra").

Tests: multi-instance discovery, .log + .log.gz inclusion, superstring
non-match, and the empty-result path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CommandDetector evasion tests embedded a literal Cyrillic capital Em and
a literal zero-width space, which trip the repo's Custom/AsciiOnlySource cop
(2 offenses). That cop also rejects \uXXXX escapes that resolve to non-ASCII,
so escaping the literals is not an option either.

Build the offending characters at runtime from their code points via
Integer#chr(Encoding::UTF_8). The source file is now pure ASCII, the runtime
test inputs are byte-for-byte identical to before, and the assertions
(Cyrillic lookalike is missed, zero-width space is tolerated) are unchanged.

rubocop: 4 files inspected, no offenses detected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
clean stripped XML tags with line.gsub!(...), mutating the caller's string.
This was inconsistent with the similarity_scrub mutation fix already made in
this refactor, and it would raise FrozenError if the game stream ever handed
us a frozen line.

The only thing relying on that mutation was the debug logging path: because
clean rewrote `line` in place, the loops could log both the raw line (a
pre-cleaned dup) and the cleaned line from the same variable. Removing the
mutation would have made those two logged values identical.

Changes:
- clean now returns a new String via gsub (no bang), never mutating input.
- Monitor exposes #last_clean_line, set on every process call, so the debug
  path has an explicit handle on the cleaned text.
- Both run loops drop the `back = line.dup` dance and log
  (raw_line, last_clean_line, last_player_line). The GTK idle-pause guard is
  rewritten from `unless back` to the equivalent `unless args.debug && line`,
  preserving the exact prior behavior (pause when not in debug, or on nil
  lines even in debug).

Tests: clean does not mutate its argument, and clean does not raise on a
frozen input line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
run() sliced the file list with `files.first(limit)` BEFORE filtering out
already-imported files. On a resumed run, the first N files are typically the
ones already imported, so `--limit=N` could select an all-imported slice and
import zero new files while reporting "N skipped" -- no forward progress.

Extract selection into pending_files(db, files, limit:), which filters out
already-imported files first and only then applies the limit. run() now
iterates the pending set directly (no in-loop already_imported? check), and
the progress/summary math counts against the pending total. Added an
explicit "all already imported, nothing to do" short-circuit.

reset still works unchanged: it clears import_log first, so every file is
pending again.

Tests: pending_files excludes imported files, applies the limit to the
pending set (regression), returns all when unlimited, and returns [] when
everything is imported.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…TL constant

Two small documentation/clarity gaps from the review:

- MessageFilter#last_player_line had no YARD @return; added one.
- The 10-minute recent-cache window was a bare `600` literal in
  migrate_recent while the class doc described it as "10 minutes". Introduced
  MessageStore::RECENT_TTL_SECONDS = 600 and referenced it from both the code
  and the doc so the number and its description cannot drift apart.

No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cution

On a spam alert, CommandDetector auto-executes any VALID_COMMAND embedded in
the suspicious line. VALID_COMMANDS includes destructive/session-ending verbs
(QUIT, SELL, DROP, DUMP, EMPTY, WITHDRAW, RELEASE, GET, PUT, KILL).

Empirically the scanner only ever fires a BARE verb -- targets never survive
scanning (an all-uppercase phrase concatenates into an invalid token, and
obfuscated tokens reduce to the verb alone). So item verbs are effectively
no-ops today (bare "sell"/"drop" just prompt), and QUIT is the one match with
a real effect: a forced logout in response to a probe, which has no upside.

Add DENYLISTED_COMMANDS and skip them at execution time. This neutralizes the
QUIT logout vector now, and keeps the item verbs safe if the scanner is ever
extended to attach arguments. A legitimate GM sanity-probe never asks you to
run these, so no useful behavior is lost.

Tests: denylisted verbs (plain and obfuscated) never execute, and a safe verb
in the same line as a denylisted one still executes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MahtraDR

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review this PR in full

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@MahtraDR Sure, kicking off a full review of the PR covering all commits.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
spec/status_monitor_spec.rb (1)

929-1006: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the repeated get_data stub/teardown.

The Object.send(:define_method, :get_data) {...} ... ensure Object.send(:remove_method, :get_data) ... pattern is duplicated identically across all 5 examples in this describe block. An around(:each) hook (or a stub_get_data helper) would remove the repetition.

♻️ Suggested refactor
+  around do |example|
+    Object.send(:define_method, :get_data) { |_type| OpenStruct.new('filter_strings' => []) }
+    example.run
+  ensure
+    Object.send(:remove_method, :get_data) if Object.method_defined?(:get_data)
+  end
+
   it 'detector runs before unseen? gate (spam detection regression test)' do
-    Object.send(:define_method, :get_data) do |_type|
-      OpenStruct.new('filter_strings' => [])
-    end
-
     monitor = described_class.new(settings)
     4.times { monitor.process(+'A mysterious voice whispers to you') }
     expect(monitor.spam_line).not_to be_nil, "SpamDetector should have fired after 4 repeats with threshold 3"
-  ensure
-    Object.send(:remove_method, :get_data) if Object.method_defined?(:get_data)
   end

(the 'returns false for lines matching a filter pattern' example would need its own inline override since it uses non-empty filter_strings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/status_monitor_spec.rb` around lines 929 - 1006, Extract the duplicated
top-level get_data definition and cleanup from the five examples into a shared
around(:each) hook or helper within this describe block. Preserve the default
empty filter_strings behavior for the shared setup, while keeping the “returns
false for lines matching a filter pattern” example’s non-empty filter_strings
override local and ensuring get_data is always removed after each example.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@status-monitor-import.lic`:
- Around line 50-70: Update the shared connection setup in open_database to set
db.busy_timeout to 5000 immediately after creating the SQLite3::Database
instance, ensuring concurrent writes wait through transient lock contention
before continuing with PRAGMA and table initialization.

In `@status-monitor.lic`:
- Around line 383-457: Reorder the calls in AlertHandler#fire so fput('exit')
executes before send_slack(counts) when `@quit_on_flag` is enabled. Keep the
existing Slack gating and response behavior unchanged, ensuring the potentially
blocking SlackBot initialization cannot delay the auto-quit path.

---

Nitpick comments:
In `@spec/status_monitor_spec.rb`:
- Around line 929-1006: Extract the duplicated top-level get_data definition and
cleanup from the five examples into a shared around(:each) hook or helper within
this describe block. Preserve the default empty filter_strings behavior for the
shared setup, while keeping the “returns false for lines matching a filter
pattern” example’s non-empty filter_strings override local and ensuring get_data
is always removed after each example.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee621cac-556e-4690-bda6-f3f3d062f7ea

📥 Commits

Reviewing files that changed from the base of the PR and between 5da7733 and f2aa764.

📒 Files selected for processing (4)
  • spec/status_monitor_import_spec.rb
  • spec/status_monitor_spec.rb
  • status-monitor-import.lic
  • status-monitor.lic

Comment thread status-monitor-import.lic
Comment on lines +50 to +70
def self.open_database(character_name)
db_path = "seen_messages_#{character_name}.db"
db = SQLite3::Database.new(db_path)
db.execute('PRAGMA journal_mode=WAL')
db.execute('PRAGMA synchronous=NORMAL')
db.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS seen_messages (
line_text TEXT PRIMARY KEY,
first_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP,
source TEXT DEFAULT 'live'
)
SQL
db.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS import_log (
file_path TEXT PRIMARY KEY,
lines_imported INTEGER,
imported_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
SQL
db
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the importer and live monitor files
fd -a 'status-monitor-import\.lic|status-monitor\.lic' .

# Map the relevant file structure first
for f in $(fd -a 'status-monitor-import\.lic|status-monitor\.lic' .); do
  echo "### OUTLINE: $f"
  ast-grep outline "$f" --view expanded || true
done

Repository: elanthia-online/dr-scripts

Length of output: 384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant regions with line numbers
for f in $(fd -a 'status-monitor-import\.lic|status-monitor\.lic' .); do
  echo "### FILE: $f"
  wc -l "$f"
done

# Try to locate database open/setup code and lock/error handling
rg -n -C 3 "busy_timeout|journal_mode|synchronous|SQLite3::BusyException|open_database|import_log|seen_messages" .

Repository: elanthia-online/dr-scripts

Length of output: 29465


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import sqlite3
print(sqlite3.sqlite_version)
PY

Repository: elanthia-online/dr-scripts

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact open_database implementations and nearby error handling
for f in $(fd -a 'status-monitor-import\.lic|status-monitor\.lic' .); do
  echo "### $f"
  rg -n -C 4 'def self\.open_database|busy_timeout|journal_mode|synchronous|rescue|BusyException|open_database' "$f" || true
  echo
done

Repository: elanthia-online/dr-scripts

Length of output: 3537


Add a busy timeout to the shared SQLite connection.

status-monitor-import.lic and status-monitor.lic both write to seen_messages_<character>.db, but neither sets busy_timeout. Concurrent live writes can raise SQLite3::BusyException, and run doesn’t rescue it, so the import can abort mid-run. db.busy_timeout = 5000 would avoid failing fast on transient lock contention.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@status-monitor-import.lic` around lines 50 - 70, Update the shared connection
setup in open_database to set db.busy_timeout to 5000 immediately after creating
the SQLite3::Database instance, ensuring concurrent writes wait through
transient lock contention before continuing with PRAGMA and table
initialization.

Comment thread status-monitor.lic
Comment on lines +383 to 457
# Handles alert responses when spam is detected: audio beeps,
# optional auto-response, Slack notification, and optional auto-quit.
#
# @note Configurable via YAML settings:
# - `status_monitor_respond` [Boolean] whether to auto-respond
# - `quit_on_status_warning` [Boolean] whether to auto-quit
# - `slack_username` [String] Slack user to notify
class AlertHandler
# @param settings [OpenStruct] user settings from get_settings
def initialize(settings)
@settings = settings
@responses = ["'Hmmm?", "'Yes", "'Ok?"].shuffle
@quit_on_flag = settings.quit_on_status_warning
@slack_enabled = !@settings.slack_username.nil? && !@settings.slack_username.to_s.strip.empty?
@slackbot = nil
end

# Fires an alert: beeps, auto-responds, notifies Slack, optionally quits.
#
# @param line [String] the suspicious line that triggered the alert
# @param counts [String] debug info about detection counts
# @return [void]
def fire(line, counts)
3.times do
echo("\a")
pause 0.25
end
CommandDetector.check(line)
fput @responses.first if @settings.status_monitor_respond
echo(line)
send_slack(counts)
fput('exit') if @quit_on_flag
@responses.rotate!
pause 2
end

private

# Lazily constructs the SlackBot on first use.
#
# SlackBot.new can block for several seconds (lnet startup, users.list
# fetch, jitter/backoff). Building it here rather than in the constructor
# keeps script startup and the status window responsive, and defers the
# connection until an alert actually needs to be sent (by which point lnet
# is far more likely to be up).
#
# @return [Lich::DragonRealms::SlackBot, nil] the bot, or nil if construction failed
def slackbot
return @slackbot if @slackbot

@slackbot = Lich::DragonRealms::SlackBot.new
rescue => e
echo "SlackBot init failed: #{e.message}"
nil
end

# @param message [String] message body to send
# @return [void]
#
# @note Delegates the connection check to SlackBot#direct_message, which
# reconnects itself when not yet initialized. Gating on #initialized?
# here would permanently suppress delivery whenever the first connection
# attempt failed (e.g. lnet not up yet at login), because the self-healing
# reconnect path inside direct_message would never be reached.
def send_slack(message)
return unless @slack_enabled

bot = slackbot
return unless bot

bot.direct_message(@settings.slack_username, message.to_s)
rescue => e
echo "SlackBot error: #{e.message}"
end
lev_array[source_length][compare_length]
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant area with line numbers.
sed -n '383,457p' status-monitor.lic | cat -n

printf '\n---\n'

# Find the SlackBot definition and any direct_message docs/usages.
rg -n "class SlackBot|def direct_message|SlackBot.new|direct_message\(" status-monitor.lic

Repository: elanthia-online/dr-scripts

Length of output: 3609


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate quit handling and any cleanup that depends on the alert path.
rg -n "quit_on_status_warning|fput\\('exit'\\)|fput\\(\"exit\"\\)|def fput\\b|\\bexit\\b" status-monitor.lic

printf '\n---\n'

# Show nearby code around any additional quit-related logic.
sed -n '300,380p' status-monitor.lic | cat -n

Repository: elanthia-online/dr-scripts

Length of output: 3672


Move send_slack after fput('exit').
SlackBot.new is documented as potentially blocking for several seconds, so sending Slack before the auto-quit path delays the safety-critical exit on the first alert.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@status-monitor.lic` around lines 383 - 457, Reorder the calls in
AlertHandler#fire so fput('exit') executes before send_slack(counts) when
`@quit_on_flag` is enabled. Keep the existing Slack gating and response behavior
unchanged, ensuring the potentially blocking SlackBot initialization cannot
delay the auto-quit path.

MahtraDR and others added 16 commits July 14, 2026 22:38
… alert

Two robustness fixes from PR review:

- open_database (both StatusMonitorImport and MessageStore, kept in sync) now
  sets db.busy_timeout = 5000 immediately after opening the connection.
  Default busy_timeout is 0, so a concurrent writer -- e.g. a running
  status-monitor and a status-monitor-import both touching the same
  per-character db -- would raise SQLITE_BUSY on transient lock contention.
  Waiting up to 5s lets the transient lock clear.

- AlertHandler#fire now queues fput('exit') before send_slack(counts) when
  quit_on_status_warning is set. Because the SlackBot is constructed lazily on
  the first alert and that can block for seconds (lnet/users.list), sending
  Slack first could delay the auto-quit. The exit command is queued to the
  game, so the Slack notification still goes out immediately afterward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…itor specs

- Assert PRAGMA busy_timeout == 5000 on both open_database connections.
- Assert the auto-quit is queued before the Slack send in AlertHandler#fire,
  via a shared $event_log recording both fput and Slack sends in order.
  Verified this fails against the old (slack-first) ordering.
- Extract the duplicated top-level get_data define/remove from the Monitor
  examples into a single around(:each) hook (default empty filter_strings,
  always removed in ensure). The one example needing a non-empty filter
  redefines get_data locally before building the Monitor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MahtraDR

MahtraDR commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

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.

1 participant