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:
1 parent
57a4edcbfd
commit
bbbcc6a5d8
42 files changed
+678
-391
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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"]})
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user