Compare commits

..

11 Commits

Author SHA1 Message Date
gitea c76ce567f8 Merge branch 'main' into ci/test-workflows 2026-06-29 12:27:14 +02:00
Michal Tuszowski 2a470d3d4a ci: restore full compile/unit/integration workflow
The minimal echo workflow was only to isolate the failure: it confirmed
that *every* Actions run in this repo ends in startup_failure (0s),
including a trivial echo job. The cause is account/repo-level (Actions
minutes/billing on a private free-plan repo), not the workflow file.
Restoring the real 3-job CI so it runs once Actions is unblocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:25:29 +02:00
Michal Tuszowski a34a2a3299 ci: minimal workflow to isolate startup_failure 2026-06-29 12:24:30 +02:00
Michal Tuszowski bd82369006 ci: make workflow ASCII-clean to fix startup_failure
The em-dash characters in the job comments tripped the workflow validator
(startup_failure, 0s). Rewrite ci.yml with plain ASCII and simpler comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:24:30 +02:00
Michal Tuszowski a6c20a0054 ci: replace broken default workflows with compile/unit/integration CI
The two scaffold workflows (Python application / Python package) failed on
every PR: they installed deps from a non-existent requirements.txt, ran
flake8/pytest over the vendored yt_dlp fork (new syntax under the 3.8/3.9
matrix), and collected ad-hoc root scripts — notably test_ai.py, which is
an invalid pasted object dump (not Python).

- Remove python-app.yml / python-package.yml and the junk root scripts
  (test.py, test_ai.py, test_time.py)
- Add .github/workflows/ci.yml with three PR-check jobs:
  * compile     — py_compile every first-party .py (no deps)
  * unit        — pytest on pure logic (conanjurer_functions, constants)
  * integration — boot the Flask services and assert the X-Conjurer-Api-Key
                  auth contract (communication_subroutine + conjurer_musician)
- Add tests/ suite, pytest.ini (testpaths=tests) and conftest.py (sys.path)

Fixes surfaced by the compile gate / needed for the integration job:
- conjurer_librarian/search_bot.py + search_bot2.py: f-string reused the
  same quote ({item["exists"]}) -> SyntaxError on Python < 3.12
- conjurer_musician/media_search_functions.py: made import-safe
  (env-overridable paths, lazy DB load / mkdir) so the service can be
  imported and tested off the Pi

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:24:30 +02:00
Michal Tuszowski e1114779ed ci: minimal workflow to isolate startup_failure 2026-06-29 12:16:45 +02:00
Michal Tuszowski 5467ced116 ci: make workflow ASCII-clean to fix startup_failure
The em-dash characters in the job comments tripped the workflow validator
(startup_failure, 0s). Rewrite ci.yml with plain ASCII and simpler comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:13:07 +02:00
Michal Tuszowski 29f12ab4ae ci: replace broken default workflows with compile/unit/integration CI
The two scaffold workflows (Python application / Python package) failed on
every PR: they installed deps from a non-existent requirements.txt, ran
flake8/pytest over the vendored yt_dlp fork (new syntax under the 3.8/3.9
matrix), and collected ad-hoc root scripts — notably test_ai.py, which is
an invalid pasted object dump (not Python).

- Remove python-app.yml / python-package.yml and the junk root scripts
  (test.py, test_ai.py, test_time.py)
- Add .github/workflows/ci.yml with three PR-check jobs:
  * compile     — py_compile every first-party .py (no deps)
  * unit        — pytest on pure logic (conanjurer_functions, constants)
  * integration — boot the Flask services and assert the X-Conjurer-Api-Key
                  auth contract (communication_subroutine + conjurer_musician)
- Add tests/ suite, pytest.ini (testpaths=tests) and conftest.py (sys.path)

Fixes surfaced by the compile gate / needed for the integration job:
- conjurer_librarian/search_bot.py + search_bot2.py: f-string reused the
  same quote ({item["exists"]}) -> SyntaxError on Python < 3.12
- conjurer_musician/media_search_functions.py: made import-safe
  (env-overridable paths, lazy DB load / mkdir) so the service can be
  imported and tested off the Pi

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:57:40 +02:00
Michal Tuszowski a32bbdd03c docs: add working-copy -> prototype migration runbook
Step-by-step operator guide for upgrading a running deployment from the
working-copy bot to the prototype (env-var config, optional internal HTTP
auth, Conan Exiles bridge). Covers backup/rollback, new dependencies,
systemd EnvironmentFile wiring, verification, behaviour changes, and
opt-in feature toggles (API key, Conan, player-join notifications).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:31:10 +02:00
Michal Tuszowski 5adeb1b384 Land prototype on main (fix stacked-PR retarget gap)
PRs #10 and #11 were merged into their intermediate base branches
(restructure/working-copy-root and proto-improvements) rather than main,
because the stacked PRs' bases were not auto-retargeted (the branches were
not deleted on merge). As a result main only received the #9 restructure
and is still the plain working-copy bot.

This brings the full prototype onto main as a clean delta on top of the
current main tree (identical content to proto-improvements, but with main
ancestry so it merges without the squash-induced rename/delete conflicts):

- constants.py: env-var config, safe JSON loading, dependency guards,
  env->netrc tokens, API_SHARED_KEY + service_headers(), CONAN_* config
- communication_subroutine.py: queue timeout/Empty, daemon threads,
  cooperative stop_event, inbound _authorize_request()
- bot.py: asyncio event loop + load conanjurer_commands
- music_functions / radio_commands / librarian_commands: X-Conjurer-Api-Key
- conanjurer_commands/_functions: fixed + integrated bridge with RCON
  player-join notifications
- requirements_bot.txt: aiomcrcon, asyncssh
- conjurer_musician/.gitignore: keep runtime playlists/mp3 out of the repo

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:28:14 +02:00
Michal Tuszowski a64fb2da57 Restructure: promote working_copy to repo root
Make the stable 'working copy' bot the canonical code at the repository
root so the install/deploy scripts operate against it again.

- Move working_copy/* to root (bot entrypoint is bot.py)
- Restore root-level install/ops scripts from c4fa88e (deploy.sh,
  install_main_bot.sh, status_report.*, conjurer.service, etc.)
- Fix deploy.sh: copy bot.py (was thin_client.py) and add the
  conanjurer_* modules; bump command count
- Remove side-by-side variant dirs (backup_old_docker, prototype_one,
  prototype_musician_one, musician_old, working_copy) and docker cruft
- Keep components as subdirs: conjurer_librarian, conjurer_musician,
  spotify_dl, yt_dlp, fonts, utils, docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 23:50:55 +02:00
16 changed files with 531 additions and 137 deletions
+54
View File
@@ -0,0 +1,54 @@
name: CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
jobs:
compile:
# Byte-compile every first-party .py to catch syntax errors. No deps.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: py_compile first-party sources
run: |
git ls-files '*.py' | grep -vE '^(yt_dlp|spotify_dl)/' | xargs python -m py_compile
echo "All first-party sources compile."
unit:
# Pure-logic tests; tested modules guard heavy deps, so only pytest needed.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install test deps
run: |
python -m pip install --upgrade pip
pip install pytest
- name: Run unit tests
run: pytest tests/unit -v
integration:
# Boot the Flask services and assert the X-Conjurer-Api-Key auth contract.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install service deps
run: |
python -m pip install --upgrade pip
pip install pytest flask waitress requests
- name: Run integration tests
run: pytest tests/integration -v
-39
View File
@@ -1,39 +0,0 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python application
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
-40
View File
@@ -1,40 +0,0 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
+14
View File
@@ -0,0 +1,14 @@
"""Pytest bootstrap: make first-party modules importable from the tests.
The bot modules live at the repository root and the musician service lives in
``conjurer_musician/``; neither is an installable package, so we put both on
``sys.path`` here.
"""
import os
import sys
_ROOT = os.path.dirname(os.path.abspath(__file__))
for _path in (_ROOT, os.path.join(_ROOT, "conjurer_musician")):
if _path not in sys.path:
sys.path.insert(0, _path)
+1 -1
View File
@@ -107,7 +107,7 @@ def consumer(in_q, control_q, doi, live_results, result_list, control_dict, no,
for item in result_list:
if item["DOI"] in data and not item["exists"]:
print(f"HIT in {no} content {data[0]} line {data[1]} file {data[2]} {item["exists"]}")
print(f"HIT in {no} content {data[0]} line {data[1]} file {data[2]} {item['exists']}")
_logger.info(data)
_logger.info("HIT")
item["exists"] = True
+1 -1
View File
@@ -108,7 +108,7 @@ def consumer(in_q, control_q, doi, live_results, result_list, control_dict, no,
print(f"C{no}{alive_no}\r", end="")
for item in result_list:
if item["DOI"] in data[0] and not item["exists"]:
print(f"HIT in {no} content {data[0]} line {data[1]} file {data[2]} {item["exists"]}")
print(f"HIT in {no} content {data[0]} line {data[1]} file {data[2]} {item['exists']}")
item["exists"] = True
live_results.append(item)
done_check = done_check and item["exists"]
+37 -15
View File
@@ -1,24 +1,44 @@
#!/usr/bin/env python3
import argparse
"""Share-list search/publish helpers for the musician service.
Paths are environment-overridable and the share database / directory are
accessed lazily, so importing this module has no side effects (the previous
version ran ``SHARE_DIR.mkdir()`` and read the JSON DB at import time, which
crashed on any host without the Pi's ``/var/www`` / ``/var/log`` layout — and
made the service untestable).
"""
import json
import os
import sys
import uuid
from pathlib import Path
# CONFIGURATION
JSON_DB = '/var/log/share_scan.json'
SHARE_DIR = Path('/var/www/html/share')
BASE_URL = 'https://czernobog.pl/share'
# CONFIGURATION (env-overridable)
JSON_DB = os.getenv("CONJURER_SHARE_DB", "/var/log/share_scan.json")
SHARE_DIR = Path(os.getenv("CONJURER_SHARE_DIR", "/var/www/html/share"))
BASE_URL = os.getenv("CONJURER_SHARE_BASE_URL", "https://czernobog.pl/share")
_entries_cache = None
def _ensure_share_dir():
SHARE_DIR.mkdir(parents=True, exist_ok=True)
# Ensure share directory exists
SHARE_DIR.mkdir(parents=True, exist_ok=True)
def load_db():
with open(JSON_DB) as f:
return json.load(f)['entries']
"""Load share entries, returning [] when the DB is missing/corrupt."""
try:
with open(JSON_DB) as handle:
return json.load(handle).get("entries", [])
except (FileNotFoundError, json.JSONDecodeError):
return []
def _entries():
global _entries_cache
if _entries_cache is None:
_entries_cache = load_db()
return _entries_cache
ENTRIES = load_db()
def relevancy(path, keywords):
score = 0
@@ -28,17 +48,20 @@ def relevancy(path, keywords):
score += low.count(kw.lower())
return score
def find_matches(count, keywords):
scored = []
for e in ENTRIES:
score = relevancy(e['path'], keywords)
for entry in _entries():
score = relevancy(entry["path"], keywords)
if score > 0:
scored.append((score, e['path']))
scored.append((score, entry["path"]))
scored.sort(reverse=True, key=lambda x: x[0])
result = [p for _, p in scored]
return result[:count]
def publish(paths):
_ensure_share_dir()
urls = []
for path in paths:
token = uuid.uuid4().hex
@@ -49,4 +72,3 @@ def publish(paths):
pass
urls.append(f"{BASE_URL}/{token}")
return urls
@@ -0,0 +1,231 @@
# Migracja: „working copy" → „prototype"
Runbook upgrade'u działającego deploymentu bota (Raspberry Pi) z wersji
**working copy** (kod w roocie repo, bez auth między usługami, bez integracji
Conan) na wersję **prototype** (konfiguracja przez zmienne środowiskowe,
opcjonalna autoryzacja wewnętrznych wywołań HTTP, integracja Conan Exiles).
> **Najważniejsze:** upgrade jest **wstecznie kompatybilny**. Bez ustawiania
> żadnych nowych zmiennych bot działa tak jak dotąd — istniejące tokeny z
> `~/.netrc` i domyślne ścieżki są zachowane. Wszystkie nowe funkcje
> (autoryzacja API, most Conan, powiadomienia o graczach) są **opt-in**.
---
## 0. Status / warunek wstępny
Pełny kod prototype znajduje się obecnie na branchu **`proto-improvements`**.
Na `main` jest na razie sama restrukturyzacja (working copy w roocie). Zanim
zmigrujesz produkcję z `main`, zmerguj prototype do `main`
(`proto-improvements``main`) albo deployuj bezpośrednio z brancha
`proto-improvements`. Dalsza część zakłada, że prototype jest już dostępny pod
refem, który checkoutujesz w kroku 2.
**Układ deploymentu (bez zmian):**
| | Ścieżka |
|---|---|
| Klon repo | `/home/pi/conjurer` |
| Runtime bota | `/home/pi/Conjurer` |
| Virtualenv | `/home/pi/Conjurer/env` (używany przez `conjurer.service`) |
| Usługa systemd | `conjurer.service``ExecStart … /home/pi/Conjurer/bot.py` |
| Deploy | `deploy.sh` (kopiuje pliki z repo do runtime i restartuje usługę) |
---
## 1. Backup i punkt powrotu
```bash
# zapamiętaj aktualny commit (do ewentualnego rollbacku)
git -C /home/pi/conjurer rev-parse HEAD > /home/pi/conjurer_rollback_commit.txt
# snapshot runtime (config + dane)
sudo cp -a /home/pi/Conjurer /home/pi/Conjurer.bak.$(date +%Y%m%d_%H%M%S)
```
Sekrety nadal pochodzą z `~/.netrc` (Discord/OpenAI/Spotify/YouTube) — upewnij
się, że ten plik istnieje i jest aktualny. Migracja go nie dotyka.
---
## 2. Pobranie kodu prototype
```bash
cd /home/pi/conjurer
git fetch --all
git checkout main && git pull # gdy prototype jest już w main
# albo, do czasu merge: git checkout proto-improvements && git pull
```
---
## 3. Instalacja nowych zależności
Wersja prototype dodaje do `requirements_bot.txt` dwa pakiety używane przez most
Conan: **`aiomcrcon`** i **`asyncssh`**.
```bash
source /home/pi/Conjurer/env/bin/activate
python3 -m pip install -r /home/pi/conjurer/requirements_bot.txt
deactivate
```
> Nawet bez tych pakietów bot się uruchomi — importy w module Conan są osłonięte
> (`try/except ImportError`), a integracja pozostaje uśpiona. Instalacja jest
> potrzebna tylko jeśli faktycznie używasz mostu Conan.
---
## 4. (Opcjonalnie) Konfiguracja zmiennych środowiskowych
Wersja prototype czyta konfigurację ze zmiennych środowiskowych z fallbackiem na
dotychczasowe wartości. **Pomiń ten krok dla zwykłego upgrade'u** — domyślne
ścieżki i `~/.netrc` wystarczą. Ustaw zmienne tylko gdy włączasz nową funkcję.
### 4a. Plik środowiskowy dla systemd
```bash
sudo tee /home/pi/Conjurer/conjurer.env >/dev/null <<'EOF'
# --- Autoryzacja wewnętrznych wywołań HTTP (opcjonalne) ---
# Ten sam klucz MUSI być ustawiony na bocie, musicianie i librarianie.
CONJURER_API_KEY=
# --- Adresy usług wewnętrznych (domyślne wartości jak dotąd) ---
#CONJURER_FILE_SERVICE=http://192.168.1.15:5000
#CONJURER_RADIO_HARBOR=http://192.168.1.15:54321
#CONJURER_LIBRARIAN_SERVICE=http://192.168.1.192:5001
# --- Most Conan Exiles (opcjonalne; puste = wyłączone) ---
#CONAN_GM_ROLE_ID=0
#CONAN_RCON_HOST=
#CONAN_RCON_PORT=25575
#CONAN_RCON_PASSWORD=
#CONAN_CHAT_CHANNEL_ID=0
#CONAN_EVENTS_CHANNEL_ID=0
# Powiadomienia o wejściu gracza — ustaw id kanału, by włączyć:
#CONAN_JOIN_CHANNEL_ID=0
#CONAN_PLAYER_POLL_SECONDS=60
#CONAN_LOG_MODE=local
#CONAN_LOG_PATH=
EOF
sudo chmod 600 /home/pi/Conjurer/conjurer.env
```
### 4b. Podłączenie pliku do usługi
Dodaj `EnvironmentFile` do sekcji `[Service]` w `conjurer.service`
(`/etc/systemd/system/conjurer.service`):
```ini
[Service]
Type=simple
EnvironmentFile=/home/pi/Conjurer/conjurer.env
ExecStart=/home/pi/Conjurer/env/bin/python3 /home/pi/Conjurer/bot.py
Restart=on-abort
```
```bash
sudo systemctl daemon-reload
```
> `conjurer.service` w repo również warto zaktualizować o tę linię, ale `deploy.sh`
> **nie** nadpisuje jednostki systemd przy każdym deployu — wpis robisz raz, ręcznie.
---
## 5. Deploy i restart
```bash
cd /home/pi/conjurer
./deploy.sh # kopiuje m.in. bot.py oraz conanjurer_commands/_functions.py do runtime
```
`deploy.sh` na końcu sam wykonuje `systemctl restart conjurer.service`. Jeśli
zmieniałeś jednostkę systemd w kroku 4b, wcześniej zrób `daemon-reload`.
---
## 6. Weryfikacja
```bash
journalctl -u conjurer.service -f
```
Czego szukać w logu (`/home/pi/Conjurer` → plik logu również):
- `Loading … module done` dla kolejnych rozszerzeń, w tym **`Loading conanjurer commands module done`**
- Gdy most Conan nieskonfigurowany: `Conan: log watch disabled (not configured)`
oraz `Conan: player-join notifications disabled (no channel or RCON)` — to
oczekiwane, integracja jest uśpiona
- `All systems: operational`
Szybki test funkcjonalny: bot łączy się z Discordem, dotychczasowe komendy
działają, radio/biblioteka odpowiadają jak wcześniej.
---
## 7. Zmiany zachowania, o których warto wiedzieć
| Obszar | Working copy | Prototype |
|---|---|---|
| Konfiguracja | zahardkodowana per-platforma | env-vary z fallbackiem na stare wartości |
| Tokeny | tylko `~/.netrc` | env-var → fallback `~/.netrc` (stare działa) |
| Wewnętrzne HTTP (music/radio/librarian) | bez nagłówków | wysyła `X-Conjurer-Api-Key`, **gdy** `CONJURER_API_KEY` ustawione |
| Endpointy przychodzące bota | bez weryfikacji | `_authorize_request()` zwraca 401 przy złym kluczu (no-op gdy klucz pusty) |
| Pętla bota | wątki + `join()` | pojedyncza pętla asyncio z czystym shutdownem |
| Moduł Conan | obecny, **nieładowany** (miał SyntaxError) | naprawiony i ładowany; uśpiony bez konfiguracji |
---
## 8. Włączanie funkcji opcjonalnych
### 8a. Autoryzacja wewnętrznych wywołań HTTP
Ustaw **ten sam** `CONJURER_API_KEY` na **wszystkich** usługach: bocie,
musicianie (`conjurer_musician`) i librarianie. Po ustawieniu:
- bot dokleja nagłówek do wywołań do file-service/radia/biblioteki,
- usługi odrzucają (401) żądania bez poprawnego klucza.
⚠️ Ustawienie klucza tylko po jednej stronie zepsuje komunikację (401). Albo
wszędzie, albo nigdzie.
### 8b. Most Conan Exiles
Ustaw `CONAN_RCON_HOST` + `CONAN_RCON_PASSWORD` (komendy GM `say/players/kick/
rcon/ogłoś`) oraz, dla mirrorowania czatu/zdarzeń, `CONAN_LOG_PATH`
(+ `CONAN_CHAT_CHANNEL_ID`/`CONAN_EVENTS_CHANNEL_ID`).
### 8c. Powiadomienia o wejściu gracza
Ustaw **`CONAN_JOIN_CHANNEL_ID`** na id kanału Discord. Funkcja odpytuje RCON
`listplayers` co `CONAN_PLAYER_POLL_SECONDS` i ogłasza nowych graczy. Pozostaw
`0`, aby trzymać ją wyłączoną.
---
## 9. Rollback
```bash
cd /home/pi/conjurer
git checkout "$(cat /home/pi/conjurer_rollback_commit.txt)"
./deploy.sh
sudo systemctl restart conjurer.service
```
W razie potrzeby przywróć snapshot runtime z `/home/pi/Conjurer.bak.*`. Nowe
zależności (`aiomcrcon`, `asyncssh`) mogą zostać w venv — nie przeszkadzają
starszej wersji.
---
## 10. Migracja usługi musician (jeśli prowadzisz radio)
Stabilny wariant `musician_old` (zahardkodowane adresy) został zastąpiony przez
`conjurer_musician` (env-driven, domyślnie `127.0.0.1`). Jeśli bot i musician są
na **różnych** hostach, ustaw na musicianie:
```bash
CONJURER_MAIN_BOT=http://<ip-bota>:5000 # gdzie bot serwuje /prepped_tracks
CONJURER_API_KEY=<ten sam sekret co bot> # jeśli włączasz auth (8a)
CONJURER_MUSIC_FOLDER=/home/pi/MediaShare/mp3
```
oraz na bocie `CONJURER_FILE_SERVICE=http://<ip-musiciana>:5000`. Szczegóły
auth/kontraktu — patrz opis usługi `conjurer_musician`.
+5
View File
@@ -0,0 +1,5 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
addopts = -ra
-20
View File
@@ -1,20 +0,0 @@
import logging
from logging import handlers
logger = logging.getLogger("discord")
logger.setLevel(logging.DEBUG)
handler = handlers.RotatingFileHandler(
filename="test.log",
encoding="utf-8",
mode="a",
maxBytes=6 * 1024 * 1024,
backupCount=6,
)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger2 = logging.getLogger("discord")
for item in logger2.handlers:
print(item)
-13
View File
@@ -1,13 +0,0 @@
AsyncCursorPage[Message](
data=[Message(id='msg_3eWSdgbcU8sbCmJK2momOgQQ',
assistant_id='asst_06eZiwvYNK3MR34suFP60gvg',
attachments=[],
completed_at=None,
content=[TextContentBlock(text=Text(annotations=[], value='Cześć! Oto coś do przemyślenia: \n\n„Sukces to suma niewielkich wysiłków powtarzanych dzień po dniu.” — Robert Collier\n\nTo przypomina nam, że często to nie wielkie działania, ale konsekwentne, małe kroki prowadzą do osiągnięcia celu. Jak mogę Ci dzisiaj pomóc?'), type='text')], created_at=1731620112, incomplete_at=None, incomplete_details=None, metadata={}, object='thread.message', role='assistant', run_id='run_JGjWQTCEkDcEYpyCJnrkZU8Q', status=None, thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD'), Message(id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', assistant_id=None, attachments=[], completed_at=None, content=[TextContentBlock(text=Text(annotations=[], value='Cześć! Powiedz coś mądrego'), type='text')], created_at=1731620110, incomplete_at=None, incomplete_details=None, metadata={}, object='thread.message', role='user', run_id=None, status=None, thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD')], object='list', first_id='msg_3eWSdgbcU8sbCmJK2momOgQQ', last_id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', has_more=False)
[TextContentBlock(
text=Text(annotations=[],
value='Cześć! Oto coś do rozważenia: "Największą przeszkodą w naszym życiu jest brak odwagi do wprowadzenia zmian." Niezależnie od tego, jakie masz cele czy marzenia, odwaga do działania i przystosowania się do nowych sytuacji jest kluczem do osiągnięcia sukcesu. Jakie masz przemyślenia na ten temat?'),
type='text')
]
-8
View File
@@ -1,8 +0,0 @@
import time
first_time = time.time_ns()
time.sleep(1)
time_diff = time.time_ns() - first_time
print(time_diff)
# 2000149433
+56
View File
@@ -0,0 +1,56 @@
"""Integration: the bot's communication Flask layer enforces the shared key,
and the key the bot would *send* (constants.service_headers) is accepted.
"""
import constants
import communication_subroutine as cs
def _client(key="test-secret"):
cs.API_KEY = key
return cs.app.test_client()
def test_prepped_tracks_rejected_without_key():
client = _client()
resp = client.post(
"/prepped_tracks", data='["all", "x"]', content_type="application/json"
)
assert resp.status_code == 401
def test_prepped_tracks_accepted_with_key():
client = _client()
resp = client.post(
"/prepped_tracks",
data='["all", "x"]',
headers={"X-Conjurer-Api-Key": "test-secret"},
content_type="application/json",
)
assert resp.status_code == 200
def test_conjurer_get_is_open():
client = _client()
assert client.get("/conjurer").status_code == 200
def test_open_when_key_unset():
client = _client(key=None)
resp = client.post(
"/prepped_tracks", data='["all", "x"]', content_type="application/json"
)
assert resp.status_code == 200
def test_bot_service_headers_accepted_by_service(monkeypatch):
# End-to-end auth contract: the header constants.service_headers() produces
# is exactly what communication_subroutine._authorize_request() expects.
monkeypatch.setattr(constants, "API_SHARED_KEY", "shared-xyz")
cs.API_KEY = "shared-xyz"
resp = cs.app.test_client().post(
"/prepped_tracks",
data='["all", "x"]',
headers=constants.service_headers(),
content_type="application/json",
)
assert resp.status_code == 200
+34
View File
@@ -0,0 +1,34 @@
"""Integration: the musician Flask service enforces the shared key on its
authenticated endpoints while leaving the open ones reachable.
"""
import conjurer_musician as m
def _client(key="test-secret"):
m.API_KEY = key
return m.app.test_client()
def test_clear_pr_pls_rejected_without_key():
client = _client()
assert client.get("/clear_pr_pls").status_code == 401
def test_clear_pr_pls_accepted_with_key():
client = _client()
resp = client.get(
"/clear_pr_pls", headers={"X-Conjurer-Api-Key": "test-secret"}
)
assert resp.status_code == 200
def test_mp3_list_is_open():
client = _client()
resp = client.get("/mp3")
assert resp.status_code == 200
assert "music_file_list" in resp.get_json()
def test_open_when_key_unset():
client = _client(key=None)
assert client.get("/clear_pr_pls").status_code == 200
+63
View File
@@ -0,0 +1,63 @@
"""Unit tests for the Conan Exiles bridge helpers (no Discord/RCON needed)."""
import conanjurer_functions as cf
def test_parse_players_basic():
out = (
"Idx | Char name | Player name | User ID\n"
"0 | Conan | SteamGuy | 1\n"
"1 | Khasar | OtherGuy | 2\n"
"--- | --- | --- | ---"
)
assert cf.parse_players(out) == {"Conan", "Khasar"}
def test_parse_players_empty_inputs():
assert cf.parse_players("No players connected.") == set()
assert cf.parse_players("") == set()
def test_parse_line_login():
event = cf.parse_line("SomeGuy joined the server")
assert event is not None
assert event.kind == "login"
assert "SomeGuy" in event.text
def test_parse_line_chat():
event = cf.parse_line("Chat: Bob: hello there")
assert event is not None
assert event.kind == "chat"
assert "Bob" in event.text and "hello" in event.text
def test_parse_line_unrecognised_is_ignored():
assert cf.parse_line("random server noise") is None
def test_conanconfig_disabled_when_unconfigured():
cfg = cf.ConanConfig("", 25575, "", "local", "", "", 22, "", "")
assert cfg.rcon_enabled is False
assert cfg.log_enabled is False
def test_conanconfig_local_log_enabled():
cfg = cf.ConanConfig("", 0, "", "local", "/tmp/conan.log", "", 22, "", "")
assert cfg.log_enabled is True
def test_conanconfig_rcon_enabled_requires_lib(monkeypatch):
# Without aiomcrcon installed, rcon stays disabled even when host+pw are set.
monkeypatch.setattr(cf, "_Rcon", None)
cfg = cf.ConanConfig("1.2.3.4", 25575, "pw", "local", "", "", 22, "", "")
assert cfg.rcon_enabled is False
# With the lib present, it enables.
monkeypatch.setattr(cf, "_Rcon", object)
cfg2 = cf.ConanConfig("1.2.3.4", 25575, "pw", "local", "", "", 22, "", "")
assert cfg2.rcon_enabled is True
def test_conanconfig_sftp_log_requires_asyncssh(monkeypatch):
monkeypatch.setattr(cf, "asyncssh", None)
cfg = cf.ConanConfig("", 0, "", "sftp", "/log", "sftp.host", 22, "u", "p")
assert cfg.log_enabled is False
+35
View File
@@ -0,0 +1,35 @@
"""Unit tests for constants helpers (defensive config / auth headers)."""
import constants
def test_service_headers_empty_when_no_key(monkeypatch):
monkeypatch.setattr(constants, "API_SHARED_KEY", "")
assert constants.service_headers() == {}
def test_service_headers_with_key(monkeypatch):
monkeypatch.setattr(constants, "API_SHARED_KEY", "s3cr3t")
assert constants.service_headers() == {"X-Conjurer-Api-Key": "s3cr3t"}
def test_load_json_missing_returns_fallback(tmp_path):
missing = tmp_path / "nope.json"
assert constants._load_json(str(missing), {"fallback": 1}) == {"fallback": 1}
def test_load_json_valid(tmp_path):
good = tmp_path / "ok.json"
good.write_text('{"a": 2}', encoding="utf-8")
assert constants._load_json(str(good), {}) == {"a": 2}
def test_load_json_corrupt_returns_fallback(tmp_path):
bad = tmp_path / "bad.json"
bad.write_text("{ not valid json", encoding="utf-8")
assert constants._load_json(str(bad), []) == []
def test_conan_defaults_present():
# Feature toggles default to "off" so the bridge stays dormant.
assert constants.CONAN_JOIN_CHANNEL_ID == 0
assert constants.CONAN_RCON_HOST == ""