Files
LangBot/tests/unit_tests/utils/test_safe_regex.py
T
4b537973f9 feat(plugin): upload debug plugin to LangBot Space with optional GitH… (#2612)
* feat(plugin): upload debug plugin to LangBot Space with optional GitHub sync

- SDK: reusable in-memory .lbpkg packaging + git sync helper (local git by default, optional repo/token override)
- routing: space-upload config/upload/git-sync endpoints; debug plugins resolved via OSS bridge without touching get_plugin_info
- frontend: debug-card entry + upload dialog with multi-language metadata, icon upload and changelog
- i18n across 8 locales; operation-trace + action allowlist entries

* fix(plugin-space-upload): stable dialog width, single-line locale tabs, broader error surfacing

- fixed dialog width so toggling the GitHub section no longer resizes it
- language tabs render on one line (horizontal scroll when needed)
- surface the real upload/git error message (msg field) instead of [object Object]
- hint that a missing GitHub repo is auto-created when a token is provided

* fix(plugin-space-upload): narrow dialog and reset sm:max-w clamp

The base DialogContent applies sm:max-w-lg at the sm breakpoint, which overrode the unprefixed max-w-none and clamped the upload card to 32rem on desktop. Set an explicit sm:max-width and size the card to ~36rem. Locale tabs now wrap instead of scrolling horizontally.

* fix

* fix(plugin-space-upload): address CI and UX feedback

- narrow the upload dialog to ~36rem and reset the sm:max-w clamp
- guide the publisher to the Space review page after a successful upload
- drop the redundant null comparison flagged by code quality
- update the operation-action expectation and Space/LangBot Account wording
- ruff format the touched API modules

* feat(plugin-space-upload): public repo, editable author/license, card red dot

* fix(market): replace yellow download count with update available badge

* style(market): unify installed and update indicators into badges

* feat(plugin): redesign debug card banner, style debug icon, and prioritize debug plugins

* feat(plugin-space-upload): simplify upload dialog layout, add creator verification and market redirection

* fix(i18n): use LangBot Account terminology in Space upload dialog

Co-authored-by: multica-agent <[email protected]>

* style(i18n): reflow reworded locale strings for prettier

Co-authored-by: multica-agent <[email protected]>

* test(utils): stop timing out the pattern-count cap test on loaded runners

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Hyu <[email protected]>
Co-authored-by: RockChinQ <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-28 18:23:09 +08:00

135 lines
4.0 KiB
Python

from __future__ import annotations
import json
import threading
from pathlib import Path
import pytest
from langbot.pkg.utils import safe_regex
from langbot.pkg.utils.bounded_executor import blocking_work_scope, current_blocking_work_scope
@pytest.mark.asyncio
async def test_matches_any_runs_off_event_loop_and_preserves_workspace_scope(monkeypatch):
event_loop_thread = threading.get_ident()
observed: dict[str, object] = {}
original = safe_regex._matches_any_sync
def observe(*args, **kwargs):
observed['thread'] = threading.get_ident()
observed['scope'] = current_blocking_work_scope()
return original(*args, **kwargs)
monkeypatch.setattr(safe_regex, '_matches_any_sync', observe)
with blocking_work_scope('workspace-a'):
assert await safe_regex.matches_any(['^hello'], 'hello world') is True
assert observed['scope'] == 'workspace-a'
assert observed['thread'] != event_loop_thread
@pytest.mark.asyncio
async def test_matches_any_interrupts_catastrophic_backtracking():
with pytest.raises(safe_regex.SafeRegexTimeoutError):
await safe_regex.matches_any(
[r'(a+)+$'],
('a' * 100_000) + '!',
timeout_seconds=0.001,
)
@pytest.mark.asyncio
async def test_matches_any_rejects_pattern_and_input_amplification():
with pytest.raises(safe_regex.SafeRegexLimitError):
await safe_regex.matches_any(
['a'] * (safe_regex.MAX_PATTERN_COUNT + 1),
'a',
)
with pytest.raises(safe_regex.SafeRegexLimitError):
await safe_regex.matches_any(
['a'],
'a' * (safe_regex.MAX_INPUT_CHARS + 1),
)
@pytest.mark.asyncio
async def test_bundled_sensitive_words_fit_within_pattern_limit():
config_path = Path(__file__).parents[3] / 'src/langbot/templates/metadata/sensitive-words.json'
config = json.loads(config_path.read_text(encoding='utf-8'))
assert len(config['words']) <= safe_regex.MAX_PATTERN_COUNT
found, masked = await safe_regex.mask_patterns(
config['words'],
'普通消息',
mask=config['mask'],
mask_word=config['mask_word'],
)
assert found is False
assert masked == '普通消息'
@pytest.mark.asyncio
async def test_mask_patterns_honors_explicit_pattern_count_cap():
patterns = ['a'] * (safe_regex.MAX_PATTERN_COUNT + 6)
# The wall-clock deadline is not what this test asserts, and 262 pattern
# compilations do not fit the 50ms default on a loaded CI runner.
found, masked = await safe_regex.mask_patterns(
patterns,
'hello',
mask='*',
mask_word='',
timeout_seconds=30.0,
max_pattern_count=len(patterns),
)
assert found is False
assert masked == 'hello'
with pytest.raises(safe_regex.SafeRegexLimitError):
await safe_regex.mask_patterns(
patterns,
'hello',
mask='*',
mask_word='',
)
@pytest.mark.asyncio
async def test_mask_patterns_rejects_oversized_sequence_before_copying_it():
class OversizedPatterns(list):
def __iter__(self):
raise AssertionError('oversized patterns must not be materialized')
patterns = OversizedPatterns(['a'] * (safe_regex.MAX_PATTERN_COUNT + 1))
with pytest.raises(safe_regex.SafeRegexLimitError):
await safe_regex.mask_patterns(
patterns,
'hello',
mask='*',
mask_word='',
)
@pytest.mark.asyncio
async def test_mask_patterns_bounds_replacement_growth_and_masks_matches():
found, masked = await safe_regex.mask_patterns(
[r'secret-\d+'],
'a secret-42 value',
mask='*',
mask_word='[hidden]',
)
assert found is True
assert masked == 'a [hidden] value'
with pytest.raises(safe_regex.SafeRegexLimitError):
await safe_regex.mask_patterns(
['a'],
'a' * safe_regex.MAX_INPUT_CHARS,
mask='0123456789',
mask_word='',
)