dgtlmoon / dgtlmoon/changedetection.io

Levenshtein condition compares content against a raw history timestamp, not a snapshot, when the current text is empty

Open Beginner friendly
#4,287 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
34.3k
Forks
2.1k
Avg merge
22h 52m
Merged PRs (30d)
70

Description

**Describe the bug**

`levenshtein_ratio_recent_history()` falls back to comparing the current snapshot against a raw history **timestamp string** instead of the previous snapshot's text, whenever the current check's filtered/extracted text is an empty string. The result feeds `res['levenshtein_ratio']` / `levenshtein_similarity`, the exact fields a Levenshtein condition reads to decide whether to fire a notification, so the condition silently makes its decision from a near-meaningless score instead of a real similarity.

```python
# changedetectionio/conditions/plugins/levenshtein_plugin.py:15-29
elif len(k) >= 1:
a = watch.get_history_snapshot(timestamp=k[-1]) # latest saved snapshot, correct
b = incoming_text if incoming_text else k[-2] # <-- k[-2] is a dict KEY (a timestamp), not a snapshot
```

An empty `incoming_text` is a normal, non-error outcome (e.g. a CSS/xpath filter that matches nothing on a given cycle), and Python treats `""` as falsy, so this branch is reachable on any ordinary check, not just an edge case. It was introduced in `5fd8200f` (#3161), which added the `else k[-2]` fallback for the single-snapshot case but forgot to wrap it in `watch.get_history_snapshot()` the way `a` and the `incoming_text is None` branch above it both do.

**Version:** current `master`, commit `aac6fcf`

**How did you install?** Not installed; found by reading the source (the production call path is `processor.py`'s `evaluate_conditions()` -> `execute_ruleset_against_all_plugins()` -> this plugin's `add_data()`) and confirmed with a standalone repro against the extracted function, in a clean `python:3.11-slim` container.

**To Reproduce**

```python
from Levenshtein import ratio, distance

history = {
"1000000000": "The quick brown fox jumps over the lazy dog near the riverbank at dawn.",
"1000000100": "The quick brown fox jumps over the lazy dog near the riverbank at dusk.",
}
k = list(history.keys())
a = history[k[-1]]
b = "" if False else k[-2] # incoming_text="" -> falls to the `else k[-2]` branch
print(round(ratio(a, b) * 100, 2)) # what the condition actually reads
print(round(ratio(a, history[k[-2]]) * 100, 2)) # what it should read (comparing snapshot to snapshot)
```

Output: `0.0` vs the real `95.77`. A condition configured as "block notification if similarity > 90%" would read 0% instead of ~96% whenever this path is hit, so it fails to suppress (or wrongly suppresses, for a low-threshold condition) a notification it was configured to gate on.

**Expected behavior**

When there's no newer text to compare (empty `incoming_text`), the fallback should resolve the previous snapshot the same way the `incoming_text is None` branch already does: `watch.get_history_snapshot(timestamp=k[-2])`.

**Additional context**

- Failure is silent: no exception, no log line distinguishable from the healthy path, so an affected user has no way to notice the condition is misbehaving.
- `git log -G 'levenshtein_ratio_recent_history'` shows only two touches to this function: the original feature (`af568d06`, #3108) and the introducing commit above (`5fd8200f`, #3161); no prior fix attempt.
- Searched open issues/PRs for `levenshtein` and found nothing describing this.

Happy to send a PR (resolve `k[-2]` through `watch.get_history_snapshot()`, with a regression test covering `incoming_text=""` against >=2 existing snapshots) if useful.

Contributor guide

Open the contributing guide

Research direction

Start in changedetectionio/conditions/plugins/levenshtein_plugin.py at levenshtein_ratio_recent_history(), then trace its call path through processor.py's evaluate_conditions() and execute_ruleset_against_all_plugins(). Reproduce the empty incoming_text case with at least two history snapshots and verify that the calculated similarity uses the previous snapshot text; done when a regression test covers this path and the condition reads the expected similarity.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
80/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.