chore: add CI, lint config, login rate limit and split large modules

- Rate-limit failed logins (8 per 15 min per IP or username, 429 + Retry-After)
- Add ruff/mypy config and GitHub Actions workflow (ruff, pytest, tsc, tests, build)
- Apply ruff autofixes (import order, pyupgrade)
- Extract rpy_helpers.py, engine-util.ts, TranslationParts.tsx and
  translation-types.ts from oversized modules
- Add dependency-free React UI tests
- Refresh README and ARCHITECTURE (Naninovel/Tomie, status and plans)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 11:23:22 +03:00
1 parent 57a4edcbfd
commit bbbcc6a5d8
42 files changed
+678 -391

No files matched your search

+12 -6
View File
@@ -1,13 +1,17 @@
import asyncio
import json
from pathlib import Path
from types import SimpleNamespace
import zipfile
from types import SimpleNamespace
import pytest
from app.config import get_settings
from app.converter.compatibility import CompatibilityError, audit_ir, detect, require_supported_scripts
from app.converter.compatibility import (
CompatibilityError,
audit_ir,
detect,
require_supported_scripts,
)
from app.converter.nani_compiler import compile_package
from app.converter.nani_expr import assignment, parse
from app.converter.nani_package import Package, decode_script, text_records
@@ -92,8 +96,8 @@ def test_renpy_atl_animation_is_rejected_instead_of_becoming_static():
def test_source_encoding_is_preserved_or_explicitly_rejected(tmp_path):
from app.converter.source import read_source
from app.converter.rpy_lexer import ParseError
from app.converter.source import read_source
source = tmp_path / "script.rpy"
source.write_bytes('# coding: cp1251\nlabel start:\n "Привет"'.encode("cp1251"))
assert "Привет" in read_source(source)
@@ -222,8 +226,8 @@ def test_naninovel_resources_keep_keys_delimiters_and_context():
def test_naninovel_import_localization_and_mod_editor_roundtrip(tmp_path, monkeypatch):
from app.blobs import read_bytes
from app.converter.naninovel import convert_naninovel
from app.translation import apply_changes, locale_tables, text_index
from app.mods import document, validate
from app.translation import apply_changes, locale_tables, text_index
monkeypatch.setenv("DATA_DIR", str(tmp_path / "library"))
get_settings.cache_clear()
@@ -250,6 +254,7 @@ def test_naninovel_import_localization_and_mod_editor_roundtrip(tmp_path, monkey
def test_unity_codec_restores_global_hooks_after_error():
from UnityPy.helpers import TypeTreeHelper as helper
from app.converter.unity_scripts import ScriptCodec
codec = ScriptCodec.__new__(ScriptCodec)
before = (helper.read_value, helper.write_value, helper.read_typetree_boost, helper.get_ref_type_node)
@@ -288,9 +293,10 @@ def test_unused_renpy_translation_entries_are_not_script_blockers(tmp_path):
def test_upload_rejects_unsupported_engine_without_queueing(tmp_path, monkeypatch):
from fastapi import HTTPException
from app.routers import versions
from app.uploads import upload_path
from fastapi import HTTPException
source = tmp_path / "unsupported.zip"
with zipfile.ZipFile(source, "w") as archive:
+2 -2
View File
@@ -1,12 +1,11 @@
"""Integration against PostgreSQL in a temporary schema, never touches library rows."""
import asyncio
import copy
import os
from uuid import uuid4
import httpx
import pytest
from sqlalchemy import select, text
from sqlalchemy import text
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app import db as database
@@ -128,6 +127,7 @@ def test_complete_mod_workflow(pg_manifest, monkeypatch):
assert repeated_lines.count("Upstream addition") == 1
# A real replacement is converted, and a stale upload is refused.
import io
from PIL import Image
image = io.BytesIO()
Image.new("RGB", (32, 32), "red").save(image, "PNG")
+25
View File
@@ -0,0 +1,25 @@
from app.ratelimit import FailureLimiter
def test_blocks_after_max_failures_and_resets():
limiter = FailureLimiter(max_failures=3, window_seconds=60)
for _ in range(2):
limiter.fail("ip:1", "user:a")
assert limiter.retry_after("ip:1", "user:a") == 0
limiter.fail("ip:1", "user:a")
assert limiter.retry_after("ip:1", "user:a") > 0
assert limiter.retry_after("ip:2", "user:b") == 0
limiter.reset("ip:1", "user:a")
assert limiter.retry_after("ip:1", "user:a") == 0
def test_window_expires(monkeypatch):
import app.ratelimit as rl
now = [1000.0]
monkeypatch.setattr(rl.time, "monotonic", lambda: now[0])
limiter = FailureLimiter(max_failures=1, window_seconds=10)
limiter.fail("k")
assert limiter.retry_after("k") > 0
now[0] += 11
assert limiter.retry_after("k") == 0
+2 -2
View File
@@ -6,11 +6,11 @@ import pytest
def test_native_translation_and_mod_edits_rebuild_the_same_vm_input(tmp_path, monkeypatch):
from app import mods
from app.config import get_settings
from app.converter.naninovel import _convert_package
from app.converter.nani_package import Package, text_records
from app.converter.naninovel import _convert_package
from app.converter.tomie_format import unpack_modules
from app.converter.tomie_profile import PROFILE
from app.translation import apply_changes, text_index, locale_tables
from app.translation import apply_changes, locale_tables, text_index
monkeypatch.setenv("DATA_DIR", str(tmp_path / "data"))
get_settings.cache_clear()
package = Package(profile=PROFILE, defaults={"points": 3}, function_data={"names": ["Tomie"]})
+9 -3
View File
@@ -1,12 +1,18 @@
import json
import os
from pathlib import Path
from types import SimpleNamespace
import os
import json
import pytest
from app.converter.nani_expr import assignment, parse
from app.converter.tomie_profile import ASSEMBLY_HASH, FUNCTIONS, constant_value, extract_tables, identify
from app.converter.tomie_profile import (
ASSEMBLY_HASH,
FUNCTIONS,
constant_value,
extract_tables,
identify,
)
def instruction(op, operand=None):