cursor_Setup
This commit is contained in:
parent
c607cd1833
commit
dc87e7f3b8
|
|
@ -27,3 +27,9 @@ ALLOWED_ORIGINS=https://mitai.jinkendo.de
|
||||||
# ── Pfade ───────────────────────────────────────────────────────
|
# ── Pfade ───────────────────────────────────────────────────────
|
||||||
PHOTOS_DIR=/app/photos
|
PHOTOS_DIR=/app/photos
|
||||||
ENVIRONMENT=production
|
ENVIRONMENT=production
|
||||||
|
|
||||||
|
# ── Gitea API (lokal, für scripts/gitea/gitea_api.py – niemals committen) ──
|
||||||
|
GITEA_BASE_URL=http://192.168.2.144:3000
|
||||||
|
GITEA_OWNER=Lars
|
||||||
|
GITEA_REPO=mitai-jinkendo
|
||||||
|
GITEA_TOKEN=
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -61,4 +61,7 @@ tmp/
|
||||||
|
|
||||||
#.claude Konfiguration
|
#.claude Konfiguration
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# Cursor MCP mit Secrets (Example: .cursor/mcp.json.example)
|
||||||
|
.cursor/mcp.json
|
||||||
.claude/settings.local.jsonfrontend/package-lock.json
|
.claude/settings.local.jsonfrontend/package-lock.json
|
||||||
|
|
|
||||||
269
docs/issues/REVIEW_OPEN_ISSUES_2026-04-04.md
Normal file
269
docs/issues/REVIEW_OPEN_ISSUES_2026-04-04.md
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
# Gitea offene Issues – Review-Entwurf (Stand Code 2026-04-04)
|
||||||
|
|
||||||
|
> **Zweck:** Du kannst diese Datei **lesen, anpassen, abstreichen**. Nach Freigabe: Kommentare / Schließungen / Konsolidierungen in Gitea umsetzen (manuell oder per `scripts/gitea/gitea_api.py`).
|
||||||
|
>
|
||||||
|
> **Sortierung:** Nach **`created_at` aufsteigend** (älteste Issues zuerst). Duplikat-Issues mit gleichem Inhalt sind am Ende des Abschnitts „Duplikate“ gebündelt.
|
||||||
|
>
|
||||||
|
> **Legende `Vorschlag`:**
|
||||||
|
> - `OFFEN` – noch sinnvoll, weiterverfolgen
|
||||||
|
> - `TEILWEISE` – Teil umgesetzt, Beschreibung/AC anpassen
|
||||||
|
> - `PRÜFEN` – kurzer manueller Test nötig
|
||||||
|
> - `DUPLIKAT` – mit anderem Issue zusammenführen
|
||||||
|
> - `DONE?` – wirkt im Repo erledigt, ggf. schließen nach deiner Bestätigung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kurz-Übersicht (28 eindeutige Issue-Nummern, zeitlich älteste zuerst)
|
||||||
|
|
||||||
|
| # | Titel (gekürzt) | Vorschlag | Notiz |
|
||||||
|
|---|-----------------|-----------|--------|
|
||||||
|
| 14 | Icon Picker Trainingstypen | OFFEN | Nur Freitext-Feld `icon` |
|
||||||
|
| 15 | Quality-Filter KI & Charts | TEILWEISE | Registry/Charts Confidence, kein durchgängiges Produkt |
|
||||||
|
| 21 | Universeller CSV-Parser + Mapping | OFFEN | Modul-spezifische Parser |
|
||||||
|
| 25 | Ziele-System Goals | DONE? | GoalsPage, Router, Focus Areas |
|
||||||
|
| 26 | Charts erweitern | TEILWEISE | Phase-0c API + History/NutritionCharts |
|
||||||
|
| 27 | Korrelationen & Insights | TEILWEISE | C-Charts + offene Data-Layer-TODOs |
|
||||||
|
| 29 | Abilities-Matrix UI | TEILWEISE | Admin/ProfileBuilder, UX offen |
|
||||||
|
| 30 | Responsive UI Sidebar | OFFEN | Weiterhin Bottom-Nav-fokussiert |
|
||||||
|
| 32 | version.py + `/api/version` | OFFEN | `version.py` ja, dedizierter Endpoint nein |
|
||||||
|
| 33 | main.py Hardcoded Version | OFFEN | FastAPI `3.0.0`, Root `v9c-dev` |
|
||||||
|
| 34 | External Volumes Doku | PRÜFEN | Gegen Compose abgleichen |
|
||||||
|
| 35 | `subscriptions` Tabelle | PRÜFEN | Schema prüfen |
|
||||||
|
| 36 | BUG Trainingstyp ISE | PRÜFEN | Logs nötig |
|
||||||
|
| 37 | Feature Enforcement Activity CSV | OFFEN | Import ohne vorgeschalteten Check |
|
||||||
|
| 38 | Feature Enforcement Nutrition CSV UI | DONE? / TEILWEISE | Backend-Check da |
|
||||||
|
| 39 | Usage-Badges Dashboard/Assistent | TEILWEISE | v. a. Gewicht im Dashboard |
|
||||||
|
| 40 | Logout Header | DONE? | `App.jsx` LogOut-Button |
|
||||||
|
| 42 | Enhanced Debug UI | DUPLIKAT | Mit #43 zusammenführen |
|
||||||
|
| 43 | Enhanced Debug UI | DUPLIKAT | Mit #42 zusammenführen |
|
||||||
|
| 45 | Prompt-Optimierer | OFFEN | Backlog |
|
||||||
|
| 46 | Prompt-Ersteller | OFFEN | Backlog |
|
||||||
|
| 47 | Wertetabelle Optimierung | OFFEN | UX-Feinschliff |
|
||||||
|
| 49 | Prompt-Zuordnung Verlauf | OFFEN | Kein klarer Treffer |
|
||||||
|
| 54 | Placeholder Registry … | DUPLIKAT | Wie #55 |
|
||||||
|
| 55 | Placeholder Registry … | OFFEN / TEILWEISE | `validate_all` existiert |
|
||||||
|
| 56–58 | Body Cluster Restarbeiten | DUPLIKAT | Dreimal gleicher Inhalt → ein Issue |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Details je Issue (älteste zuerst)
|
||||||
|
|
||||||
|
### #14 – [FEAT-001] Icon Picker für Trainingstypen
|
||||||
|
|
||||||
|
**Code-Stand:** `AdminTrainingTypesPage.jsx` nutzt ein **Textfeld** `icon` (frei, z. B. Emoji). Kein dedizierter Icon-Picker (Palette, Vorschau, Kategorien).
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN` – Issue beibehalten; optional präzisieren: „Emoji-Picker oder vordefinierte Icon-Liste statt Freitext“.
|
||||||
|
|
||||||
|
**Kommentar-Entwurf für Gitea:**
|
||||||
|
> Stand Backend/Frontend: `icon` wird als String in `training_types` gespeichert, Eingabe ist Freitext. Icon-Picker-UX steht noch aus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #15 – [FEAT-002] Quality-Filter für KI-Auswertungen & Charts
|
||||||
|
|
||||||
|
**Code-Stand:** Charts/Data-Layer nutzen **Confidence** u. a.; Placeholder-Registry hat viele `quality_filter_policy` / Evidence-Felder (teilweise `UNRESOLVED`/`TO_VERIFY`). Ein **einheitlicher** „Quality-Filter“-Mechanismus über alle KI-Auswertungen ist nicht eindeutig als fertiges Feature erkennbar.
|
||||||
|
|
||||||
|
**Vorschlag:** `TEILWEISE` – Issue-Text auf konkrete Lücken schärfen (welche Prompts/Charts, welche Schwellen, SSoT Registry?).
|
||||||
|
|
||||||
|
**Kommentar-Entwurf:**
|
||||||
|
> Teilaspekte existieren (z. B. Confidence in Chart-Endpoints, Registry-Felder). Offen: durchgängige KI-/Chart-Quality-Pipeline und Abgleich mit Issue-Zielbild.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #21 – [FEATURE] Universeller CSV-Parser mit lernbarem Feldmapping
|
||||||
|
|
||||||
|
**Code-Stand:** Modulspezifische CSV-Imports (Activity, Nutrition, Vitals, Sleep, …) mit jeweils eigenem Parser; **lernbares Mapping** stark bei **Activity** über `activity_type_mappings`. Kein **ein** generischer CSV-Engine wie im Issue beschrieben.
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN` – oder Scope reduzieren („pro Modul konsolidieren“).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #25 – [FEAT] Ziele-System (Goals) v9e Kernfeature
|
||||||
|
|
||||||
|
**Code-Stand:** `goals`-Router, `GoalsPage`, Focus Areas, Migrationen – laut Projektstand **weitgehend implementiert**.
|
||||||
|
|
||||||
|
**Vorschlag:** `DONE?` nach deiner Abnahme – Issue-Body auf verbleibende Teilziele kürzen oder schließen.
|
||||||
|
|
||||||
|
**Kommentar-Entwurf:**
|
||||||
|
> Backend/Frontend Goals + Focus Areas sind im Repo vorhanden. Bitte verbleibende Wünsche als neue Sub-Issues oder AC hier abhaken und schließen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #26 – [FEAT] Charts & Visualisierungen erweitern
|
||||||
|
|
||||||
|
**Code-Stand:** `backend/routers/charts.py` (Phase 0c), viele `api.get…Chart` in `api.js`; `History.jsx` + `NutritionCharts` / `RecoveryCharts` nutzen Chart-Daten.
|
||||||
|
|
||||||
|
**Vorschlag:** `TEILWEISE` – Issue auf konkrete fehlende Chart-Typen/UI-Verdrahtung schärfen (falls noch offen).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #27 – [FEAT] Korrelationen & Insights erweitern
|
||||||
|
|
||||||
|
**Code-Stand:** Chart-Endpunkte C1–C4 u. a.; Data-Layer `correlations.py` mit TODO-Stellen in Teilen.
|
||||||
|
|
||||||
|
**Vorschlag:** `TEILWEISE` – Liste fehlender Korrelationen/Insights vs. Code ergänzen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #29 – [FEAT] Abilities-Matrix UI (v9f)
|
||||||
|
|
||||||
|
**Code-Stand:** Training Types mit `abilities` JSONB, `AdminTrainingProfiles`, `ProfileBuilder` – vollständige „5D Matrix“-UX unklar ohne Produktvorgabe.
|
||||||
|
|
||||||
|
**Vorschlag:** `TEILWEISE` – AC mit aktuellen Screenshots/Flows abgleichen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #30 – [FEAT] Responsive UI – Desktop Sidebar + 2-spaltig
|
||||||
|
|
||||||
|
**Code-Stand:** Weiterhin stark **Mobile-first** (z. B. `bottom-nav` in `App.jsx`); keine ausgebaute Desktop-Sidebar wie im klassischen Admin-Dashboard.
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #32 – Version-System (`version.py` + `/api/version`)
|
||||||
|
|
||||||
|
**Code-Stand:** `backend/version.py` existiert mit `APP_VERSION`. **`GET /api/version`** im Backend **nicht** gefunden (Suche nach Route); Root liefert u. a. `"version": "v9c-dev"`.
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN` für #32 – `/api/version` implementieren oder Issue anpassen („nur version.py ohne Endpoint“).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #33 – main.py hardcoded Version entfernen
|
||||||
|
|
||||||
|
**Code-Stand:** `main.py`: `FastAPI(..., version="3.0.0")`; Root-JSON noch `v9c-dev`.
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN` – auf `version.py` vereinheitlichen (inkl. FastAPI-`version`-Feld und Health-Payload).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #34 – External Volumes dokumentieren (Legacy bodytrack_*)
|
||||||
|
|
||||||
|
**Vorschlag:** `PRÜFEN` – gegen aktuelle `docker-compose`/Deploy-Doku im Repo halten; dann schließen oder Aktualisierung kommentieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #35 – Deprecated Tabelle `subscriptions` entfernen
|
||||||
|
|
||||||
|
**Code-Stand:** Im Migrations-Ordner **kein** aktueller Treffer auf `subscriptions` (Stichprobe); Membership-System nutzt andere Tabellen.
|
||||||
|
|
||||||
|
**Vorschlag:** `PRÜFEN` – einmal `schema.sql` / DB prüfen, ob Tabelle noch existiert. Wenn weg: Issue schließen mit Verweis auf Migration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #36 – BUG-009: Trainingstyp-Erstellung → Internal Server Error
|
||||||
|
|
||||||
|
**Code-Stand:** `TrainingTypeCreate` enthält `abilities` / `profile` (JSONB). ISE oft durch DB-Constraint, NULL/JSON oder fehlende Spalte – **ohne Laufzeit-Log nicht verifiziert**.
|
||||||
|
|
||||||
|
**Vorschlag:** `PRÜFEN` – in Gitea Notiz: aktueller Request-Body + Stacktrace aus `docker logs`; wenn behoben: schließen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #37 – Feature-Enforcement Activity CSV-Import
|
||||||
|
|
||||||
|
**Code-Stand:** `create_activity` nutzt `check_feature_access` für `activity_entries`. **`import_activity_csv`** startet **ohne** vorgeschalteten Limit-Check (im gelesenen Abschnitt nur `get_pid` + Parse) – von Issue #37 noch **nicht** erfüllt.
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN` – hoch priorisieren; analog Nutrition: ein Check vor Bulk-Import + Zählung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #38 – Feature-Enforcement Nutrition CSV-Import UI
|
||||||
|
|
||||||
|
**Code-Stand:** `import_nutrition_csv` ruft **`check_feature_access`** für `nutrition_entries` auf (inkl. Logging).
|
||||||
|
|
||||||
|
**Vorschlag:** `TEILWEISE` / `DONE?` – falls UI-Feedback gewünscht, im Issue auf konkrete UI-Lücken eingehen (Banner, Disable Button).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #39 – Usage-Badges im Dashboard-Assistenten
|
||||||
|
|
||||||
|
**Code-Stand:** `Dashboard.jsx` nutzt `getFeatureUsage()` für **Gewichts-Widget** (Limit/Lock). Unklar ob „Assistent“-Modus = gesamtes Dashboard oder separater Guide.
|
||||||
|
|
||||||
|
**Vorschlag:** `TEILWEISE` – Issue präzisieren, welche Kacheln/Bereiche Badges brauchen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #40 – Logout-Button im App-Header (neben Avatar)
|
||||||
|
|
||||||
|
**Code-Stand:** `App.jsx` – Header mit **`LogOut` neben Avatar** (Umsetzung vorhanden).
|
||||||
|
|
||||||
|
**Vorschlag:** `DONE?` – nach kurzem Klicktest **schließen**.
|
||||||
|
|
||||||
|
**Kommentar-Entwurf:**
|
||||||
|
> In `App.jsx` ist ein Logout-Button im Header umgesetzt. Bitte in target Umgebung verifizieren und schließen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #42 / #43 – Enhanced Debug / Prompt Analysis UI (Issue #28 Phase C)
|
||||||
|
|
||||||
|
**Code-Stand:** `Analysis.jsx` mit Expert-Modus, Platzhalter-Gruppierung – kann Teile von Phase C abdecken.
|
||||||
|
|
||||||
|
**Vorschlag:** `DUPLIKAT` – **ein** Issue behalten; anderes schließen mit Verweis. Inhalte zusammenführen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #45 – KI Prompt-Optimierer
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN` – Backlog, nicht im aktuellen Code sichtbar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #46 – KI Prompt-Ersteller
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN` – wie #45.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #47 – Wertetabelle Optimierung
|
||||||
|
|
||||||
|
**Code-Stand:** Wertetabelle in `Analysis.jsx` / Metadata – viele Punkte eher UX/Performance.
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN` – konkrete UI-Schmerzpunkte in Sub-Tasks splitten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #49 – Prompt-Zuordnung zu Verlaufsseiten
|
||||||
|
|
||||||
|
**Code-Stand:** Kein eindeutiger Treffer zu „History page prompt assignment“ in kurzer Suche.
|
||||||
|
|
||||||
|
**Vorschlag:** `OFFEN` – kurz präzisieren (Welche Seite, welches Datenmodell).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #54 / #55 – Placeholder Registry UNRESOLVED & TO_VERIFY
|
||||||
|
|
||||||
|
**Code-Stand:** `placeholder_registry_export.py` liefert **`validation_report` über `registry.validate_all()`** (nicht mehr leer aus dem frühen Issue-Text für Body-Cluster „{}“-Teil). Evidence `TO_VERIFY`/`UNRESOLVED` existieren weiter in Registrations.
|
||||||
|
|
||||||
|
**Vorschlag:** `#54` und `#55` **zusammenlegen** (gleiches Thema, Titel nur Encoding-Unterschied). Ein Issue offen lassen, Metadaten-Audit fortsetzen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### #56 / #57 / #58 – Body Cluster Restarbeiten & Metadaten-Verifizierung
|
||||||
|
|
||||||
|
**Inhalt:** identische bzw. nahezu identische Langbeschreibung (Metadaten, Layer 2b, Nutrition confidence_logic, Validation Report).
|
||||||
|
|
||||||
|
**Vorschlag:** `DUPLIKAT` – **eines** behalten (z. B. niedrigste Nummer oder neueste #58 mit aktualisiertem Stand), andere **schließen** mit Verweis „Duplicate of #X“. Validation-Teil: Code hat bereits `validate_all` – Issue-Text Abschnitt „leeres validation_report“ **aktualisieren**.
|
||||||
|
|
||||||
|
**Kommentar-Entwurf:**
|
||||||
|
> Dreimal dasselbe Issue. Vorschlag: #56/#57 schließen, Tracking nur in #XX. `validation_report` wird aus `registry.validate_all()` befüllt; verbleibende Arbeit: TO_VERIFY-Felder Layer 2b + Nutrition confidence_logic laut Checkliste.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nachbearbeitung in Gitea (Checkliste für dich)
|
||||||
|
|
||||||
|
- [ ] Duplikate schließen und verlinken (#42/#43, #54/#55, #56–#58).
|
||||||
|
- [ ] „DONE?“-Issues manuell testen (`#25`, `#38`, `#40`).
|
||||||
|
- [ ] `#37` umsetzen oder Kommentar „noch offen“ bestätigen.
|
||||||
|
- [ ] `#32`–`#33` Versionierung planen (ein gemeinsames Mini-Epic).
|
||||||
|
- [ ] Kommentare aus diesem Dokument kopieren/anpassen.
|
||||||
|
- [ ] Optional: Labels in Gitea setzen (`duplicate`, `blocked`, `needs-retest`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technischer Hinweis (Audit / Security)
|
||||||
|
|
||||||
|
Aus dem Code-Audit 2026-04-04: kritische Punkte (`get_pid` / `X-Profile-Id`, `/api/profiles` ohne Admin) sind **nicht** 1:1 als Gitea-Issues in dieser offenen Liste sichtbar – ggf. **separate** Security-Issues aus `.claude/docs/audit/20260404_code_audit/gitea/` anlegen, falls noch nicht vorhanden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Erzeugt aus Gitea API (28 offene Issues, sortiert nach `created_at`) und statischer Code-Analyse im Workspace. Kein Laufzeit-Test auf dem Pi.*
|
||||||
64
scripts/gitea/MCP_SETUP.md
Normal file
64
scripts/gitea/MCP_SETUP.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Gitea MCP-Server für Cursor
|
||||||
|
|
||||||
|
Damit der Agent **strukturierte Tools** nutzen kann (`gitea_list_issues`, `gitea_close_issue`, …), registrierst du diesen MCP-S **lokal** in Cursor.
|
||||||
|
|
||||||
|
## 1. Abhängigkeit
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pip install -r scripts/gitea/requirements-mcp.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder: `pip install "mcp>=1.2.0"`
|
||||||
|
|
||||||
|
## 2. Secrets
|
||||||
|
|
||||||
|
Wie beim CLI: **`GITEA_*` in der Repo-Root `.env`** (wird von `gitea_lib` geladen), **oder** dieselben Variablen in der MCP-`env` (siehe unten).
|
||||||
|
|
||||||
|
**Niemals** Tokens in Git committen.
|
||||||
|
|
||||||
|
## 3. Cursor MCP konfigurieren
|
||||||
|
|
||||||
|
**Variante A – UI:** Einstellungen → **MCP** / Tools → Server hinzufügen → Typ „Command“:
|
||||||
|
|
||||||
|
- **Command:** `python` (oder voller Pfad zu `python.exe`)
|
||||||
|
- **Args:** vollständiger Pfad zu `mcp_server_gitea.py`, z. B.
|
||||||
|
`C:\Dev\mitai-jinkendo\scripts\gitea\mcp_server_gitea.py`
|
||||||
|
- **Working directory (optional):** `C:\Dev\mitai-jinkendo\scripts\gitea`
|
||||||
|
- **Env:** nur nötig, wenn du **keine** `.env` im Repo nutzt:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GITEA_BASE_URL=http://192.168.2.144:3000
|
||||||
|
GITEA_OWNER=Lars
|
||||||
|
GITEA_REPO=mitai-jinkendo
|
||||||
|
GITEA_TOKEN=…
|
||||||
|
```
|
||||||
|
|
||||||
|
**Variante B – JSON:** Datei `~/.cursor/mcp.json` (Benutzer) oder projektbezogen laut Cursor-Doku. Beispielinhalt siehe **`.cursor/mcp.json.example`** im Repo (Platzhalter, ohne echtes Token).
|
||||||
|
|
||||||
|
Cursor nach Änderung **vollständig neu starten**.
|
||||||
|
|
||||||
|
## 4. Netzwerk
|
||||||
|
|
||||||
|
Die Gitea-URL muss von deinem Rechner erreichbar sein (z. B. `192.168.2.144:3000` im LAN).
|
||||||
|
|
||||||
|
## 5. Repo-Zugriff
|
||||||
|
|
||||||
|
- **API:** Tool `gitea_get_repo_file` (Dateiinhalt / Metadaten).
|
||||||
|
- **Git (lokal):** unverändert `git pull` / Agent liest Workspace-Dateien – dafür brauchst du kein MCP.
|
||||||
|
|
||||||
|
## Bereitgestellte Tools (Kurzüberblick)
|
||||||
|
|
||||||
|
| Tool | Zweck |
|
||||||
|
|------|--------|
|
||||||
|
| `gitea_list_issues` | Issues listen, optional alle Seiten |
|
||||||
|
| `gitea_get_issue` | Ein Issue mit Body |
|
||||||
|
| `gitea_comment_issue` | Kommentar |
|
||||||
|
| `gitea_create_issue` | Neu anlegen |
|
||||||
|
| `gitea_close_issue` / `gitea_reopen_issue` | Status |
|
||||||
|
| `gitea_get_repo_file` | Datei remote via API |
|
||||||
|
|
||||||
|
## Issue-Triage durch den Agent
|
||||||
|
|
||||||
|
Sinnvoller Ablauf: Issues listen → je Issue **Code/Commits prüfen** → bei eindeutig erledigt: kurzer Kommentar + **close**; bei teilweise: Kommentar mit Checkboxen; bei unklar: nur Kommentar, **nicht** schließen.
|
||||||
|
|
||||||
|
Autonom alles schließen ist fehleranfällig; klare Regeln oder manuelle Freigabe für `close` empfohlen.
|
||||||
68
scripts/gitea/README.md
Normal file
68
scripts/gitea/README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Gitea API – lokales CLI
|
||||||
|
|
||||||
|
Dient dazu, **Issues** auf deiner Gitea-Instanz zu lesen und anzulegen – mit den in **`.env`** gesetzten Variablen (nicht committen).
|
||||||
|
|
||||||
|
## Umgebungsvariablen (Root `.env`)
|
||||||
|
|
||||||
|
| Variable | Beispiel |
|
||||||
|
|----------|----------|
|
||||||
|
| `GITEA_BASE_URL` | `http://192.168.2.144:3000` |
|
||||||
|
| `GITEA_TOKEN` | Personal Access Token (nur Scope **repo** + **issue** nötig) |
|
||||||
|
| `GITEA_OWNER` | `Lars` |
|
||||||
|
| `GITEA_REPO` | `mitai-jinkendo` |
|
||||||
|
|
||||||
|
## Voraussetzung
|
||||||
|
|
||||||
|
Python 3.10+ (nur Standardbibliothek).
|
||||||
|
|
||||||
|
## Aufruf (im Repo-Root)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Issues auflisten (offen)
|
||||||
|
python scripts/gitea/gitea_api.py issues list
|
||||||
|
|
||||||
|
# Issues mit State
|
||||||
|
python scripts/gitea/gitea_api.py issues list --state all
|
||||||
|
|
||||||
|
# Ein Issue lesen
|
||||||
|
python scripts/gitea/gitea_api.py issues get 42
|
||||||
|
|
||||||
|
# Issue anlegen (Titel + Body aus Datei oder direkt)
|
||||||
|
python scripts/gitea/gitea_api.py issues create --title "Fix: …" --body "…"
|
||||||
|
|
||||||
|
python scripts/gitea/gitea_api.py issues create --title "Fix: …" --body-file path/to/body.md
|
||||||
|
|
||||||
|
# Kommentar
|
||||||
|
python scripts/gitea/gitea_api.py issues comment 42 --body "…"
|
||||||
|
|
||||||
|
# Schließen / wieder öffnen
|
||||||
|
python scripts/gitea/gitea_api.py issues close 42
|
||||||
|
python scripts/gitea/gitea_api.py issues reopen 42
|
||||||
|
|
||||||
|
# Alle Issues (alle Seiten, Vorsicht bei großen Repos)
|
||||||
|
python scripts/gitea/gitea_api.py issues list --all-pages --state open
|
||||||
|
|
||||||
|
# Markdown-Datei (z. B. Audit-Template) als Issue-Body
|
||||||
|
python scripts/gitea/gitea_api.py issues create --title "…" --body-file .claude/docs/audit/.../gitea/TEMPLATE_P0-....md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository-Inhalt (read-only)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Datei über Gitea-API (bei Dateien: Text-Inhalt; bei Verzeichnissen: JSON-Listing)
|
||||||
|
python scripts/gitea/gitea_api.py repo file README.md
|
||||||
|
|
||||||
|
python scripts/gitea/gitea_api.py repo file backend/main.py --ref develop
|
||||||
|
|
||||||
|
# Clone/Push: normales `git remote` – Token nicht dauerhaft in der Remote-URL; SSH oder Credential Helper.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
|
||||||
|
- **Niemals** `GITEA_TOKEN` ins Git oder in Issues/Pastebins.
|
||||||
|
- Token, das in Chat oder Logs gelandet ist, in Gitea **widerrufen** und **neu erzeugen**.
|
||||||
|
- Cursor-Agenten können das CLI über das Terminal nutzen, wenn `.env` gesetzt und Netzwerk zu `GITEA_BASE_URL` erreichbar ist.
|
||||||
|
|
||||||
|
## MCP (Tools direkt im Agent)
|
||||||
|
|
||||||
|
Siehe [`MCP_SETUP.md`](./MCP_SETUP.md) und [`../.cursor/mcp.json.example`](../../.cursor/mcp.json.example).
|
||||||
183
scripts/gitea/gitea_api.py
Normal file
183
scripts/gitea/gitea_api.py
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Minimal Gitea API client. Reads GITEA_* from environment or .env in repo root.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from gitea_lib import (
|
||||||
|
issues_comment,
|
||||||
|
issues_create,
|
||||||
|
issues_get,
|
||||||
|
issues_list_all,
|
||||||
|
issues_list_page,
|
||||||
|
issues_patch,
|
||||||
|
load_dotenv,
|
||||||
|
repo_file_content,
|
||||||
|
repo_root,
|
||||||
|
require_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issues_list(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
|
||||||
|
if args.all_pages:
|
||||||
|
items = issues_list_all(
|
||||||
|
base, token, owner, repo, state=args.state, limit=args.limit
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_, items = issues_list_page(
|
||||||
|
base,
|
||||||
|
token,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
state=args.state,
|
||||||
|
page=args.page,
|
||||||
|
limit=args.limit,
|
||||||
|
)
|
||||||
|
for it in items:
|
||||||
|
num = it.get("number")
|
||||||
|
title = it.get("title")
|
||||||
|
st = it.get("state")
|
||||||
|
print(f"#{num} [{st}] {title}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issues_get(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
|
||||||
|
status, payload = issues_get(base, token, owner, repo, args.number)
|
||||||
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||||
|
if status >= 400:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issues_create(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
|
||||||
|
body = args.body or ""
|
||||||
|
if args.body_file:
|
||||||
|
body = Path(args.body_file).read_text(encoding="utf-8")
|
||||||
|
status, payload = issues_create(
|
||||||
|
base,
|
||||||
|
token,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
title=args.title,
|
||||||
|
body=body,
|
||||||
|
labels=args.labels or [],
|
||||||
|
)
|
||||||
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||||
|
if status >= 400:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issues_comment(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
|
||||||
|
status, payload = issues_comment(
|
||||||
|
base, token, owner, repo, args.number, args.body
|
||||||
|
)
|
||||||
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||||
|
if status >= 400:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issues_close(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
|
||||||
|
status, payload = issues_patch(
|
||||||
|
base, token, owner, repo, args.number, {"state": "closed"}
|
||||||
|
)
|
||||||
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||||
|
if status >= 400:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issues_reopen(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
|
||||||
|
status, payload = issues_patch(
|
||||||
|
base, token, owner, repo, args.number, {"state": "open"}
|
||||||
|
)
|
||||||
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||||
|
if status >= 400:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_repo_contents(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
|
||||||
|
status, payload = repo_file_content(
|
||||||
|
base, token, owner, repo, args.path, ref=args.ref or ""
|
||||||
|
)
|
||||||
|
if status >= 400:
|
||||||
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||||
|
sys.exit(1)
|
||||||
|
if isinstance(payload, dict) and payload.get("encoding") == "text":
|
||||||
|
print(payload.get("content", ""))
|
||||||
|
else:
|
||||||
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
root = repo_root()
|
||||||
|
load_dotenv(root)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Gitea API helper")
|
||||||
|
sub = parser.add_subparsers(dest="domain", required=True)
|
||||||
|
|
||||||
|
p_issues = sub.add_parser("issues", help="Issues")
|
||||||
|
i_sub = p_issues.add_subparsers(dest="issues_cmd", required=True)
|
||||||
|
|
||||||
|
p_il = i_sub.add_parser("list", help="List issues")
|
||||||
|
p_il.add_argument("--state", default="open", choices=["open", "closed", "all"])
|
||||||
|
p_il.add_argument("--limit", type=int, default=50)
|
||||||
|
p_il.add_argument("--page", type=int, default=1)
|
||||||
|
p_il.add_argument(
|
||||||
|
"--all-pages",
|
||||||
|
action="store_true",
|
||||||
|
help="Alle Seiten abfragen (Vorsicht bei sehr vielen Issues)",
|
||||||
|
)
|
||||||
|
p_il.set_defaults(_handler=cmd_issues_list)
|
||||||
|
|
||||||
|
p_ig = i_sub.add_parser("get", help="Get one issue")
|
||||||
|
p_ig.add_argument("number", type=int)
|
||||||
|
p_ig.set_defaults(_handler=cmd_issues_get)
|
||||||
|
|
||||||
|
p_ic = i_sub.add_parser("create", help="Create issue")
|
||||||
|
p_ic.add_argument("--title", required=True)
|
||||||
|
p_ic.add_argument("--body", default="")
|
||||||
|
p_ic.add_argument("--body-file")
|
||||||
|
p_ic.add_argument("--labels", nargs="*", default=[])
|
||||||
|
p_ic.set_defaults(_handler=cmd_issues_create)
|
||||||
|
|
||||||
|
p_co = i_sub.add_parser("comment", help="Add comment")
|
||||||
|
p_co.add_argument("number", type=int)
|
||||||
|
p_co.add_argument("--body", required=True)
|
||||||
|
p_co.set_defaults(_handler=cmd_issues_comment)
|
||||||
|
|
||||||
|
p_cl = i_sub.add_parser("close", help="Close issue")
|
||||||
|
p_cl.add_argument("number", type=int)
|
||||||
|
p_cl.set_defaults(_handler=cmd_issues_close)
|
||||||
|
|
||||||
|
p_ro = i_sub.add_parser("reopen", help="Reopen issue")
|
||||||
|
p_ro.add_argument("number", type=int)
|
||||||
|
p_ro.set_defaults(_handler=cmd_issues_reopen)
|
||||||
|
|
||||||
|
p_repo = sub.add_parser("repo", help="Repository (API)")
|
||||||
|
r_sub = p_repo.add_subparsers(dest="repo_cmd", required=True)
|
||||||
|
|
||||||
|
p_rc = r_sub.add_parser("file", help="Get file or directory metadata/content")
|
||||||
|
p_rc.add_argument("path")
|
||||||
|
p_rc.add_argument("--ref", default="", help="branch/tag/commit")
|
||||||
|
p_rc.set_defaults(_handler=cmd_repo_contents)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
base, token, owner, reponame = require_config()
|
||||||
|
except RuntimeError as e:
|
||||||
|
sys.stderr.write(str(e) + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
handler = args._handler
|
||||||
|
handler(args, base, token, owner, reponame)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
226
scripts/gitea/gitea_lib.py
Normal file
226
scripts/gitea/gitea_lib.py
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
"""
|
||||||
|
Shared Gitea REST helpers (stdlib). Used by gitea_api.py CLI and mcp_server_gitea.py.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def load_dotenv(repo_root: Path) -> None:
|
||||||
|
env_path = repo_root / ".env"
|
||||||
|
if not env_path.is_file():
|
||||||
|
return
|
||||||
|
for line in env_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
k, _, v = line.partition("=")
|
||||||
|
k, v = k.strip(), v.strip().strip('"').strip("'")
|
||||||
|
if k and k not in os.environ:
|
||||||
|
os.environ[k] = v
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def get_config() -> tuple[str, str, str, str]:
|
||||||
|
base = os.getenv("GITEA_BASE_URL", "").rstrip("/")
|
||||||
|
token = os.getenv("GITEA_TOKEN", "")
|
||||||
|
owner = os.getenv("GITEA_OWNER", "")
|
||||||
|
reponame = os.getenv("GITEA_REPO", "")
|
||||||
|
return base, token, owner, reponame
|
||||||
|
|
||||||
|
|
||||||
|
def require_config() -> tuple[str, str, str, str]:
|
||||||
|
base, token, owner, reponame = get_config()
|
||||||
|
missing = [n for n, v in (
|
||||||
|
("GITEA_BASE_URL", base),
|
||||||
|
("GITEA_TOKEN", token),
|
||||||
|
("GITEA_OWNER", owner),
|
||||||
|
("GITEA_REPO", reponame),
|
||||||
|
) if not v]
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Fehlende Umgebungsvariablen: " + ", ".join(missing)
|
||||||
|
+ " — setze sie in .env im Repo-Root oder in der MCP-env."
|
||||||
|
)
|
||||||
|
return base, token, owner, reponame
|
||||||
|
|
||||||
|
|
||||||
|
def request_json(
|
||||||
|
method: str,
|
||||||
|
url: str,
|
||||||
|
token: str,
|
||||||
|
data: dict | None = None,
|
||||||
|
) -> tuple[int, Any]:
|
||||||
|
body = None if data is None else json.dumps(data).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=body, method=method)
|
||||||
|
req.add_header("Authorization", f"token {token}")
|
||||||
|
req.add_header("Accept", "application/json")
|
||||||
|
if body is not None:
|
||||||
|
req.add_header("Content-Type", "application/json")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||||
|
raw = resp.read().decode("utf-8", errors="replace")
|
||||||
|
status = resp.status
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
raw = e.read().decode("utf-8", errors="replace")
|
||||||
|
try:
|
||||||
|
return e.code, json.loads(raw) if raw else {}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return e.code, {"message": raw or str(e)}
|
||||||
|
if not raw:
|
||||||
|
return status, {}
|
||||||
|
try:
|
||||||
|
return status, json.loads(raw)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return status, raw
|
||||||
|
|
||||||
|
|
||||||
|
def issues_list_page(
|
||||||
|
base: str,
|
||||||
|
token: str,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
*,
|
||||||
|
state: str = "open",
|
||||||
|
page: int = 1,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> tuple[int, list]:
|
||||||
|
if state == "all":
|
||||||
|
open_st, open_i = issues_list_page(
|
||||||
|
base, token, owner, repo, state="open", page=page, limit=limit
|
||||||
|
)
|
||||||
|
closed_st, closed_i = issues_list_page(
|
||||||
|
base, token, owner, repo, state="closed", page=page, limit=limit
|
||||||
|
)
|
||||||
|
merged = (open_i or []) + (closed_i or [])
|
||||||
|
st = max(open_st, closed_st) if open_st >= 400 or closed_st >= 400 else 200
|
||||||
|
return st, merged[:limit]
|
||||||
|
q = f"?state={state}&page={page}&limit={limit}"
|
||||||
|
url = f"{base}/api/v1/repos/{owner}/{repo}/issues{q}"
|
||||||
|
status, payload = request_json("GET", url, token)
|
||||||
|
if status >= 400:
|
||||||
|
return status, []
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
return status, []
|
||||||
|
return status, payload
|
||||||
|
|
||||||
|
|
||||||
|
def issues_list_all(
|
||||||
|
base: str,
|
||||||
|
token: str,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
*,
|
||||||
|
state: str = "open",
|
||||||
|
limit: int = 50,
|
||||||
|
) -> list[dict]:
|
||||||
|
if state == "all":
|
||||||
|
o = issues_list_all(
|
||||||
|
base, token, owner, repo, state="open", limit=limit
|
||||||
|
)
|
||||||
|
c = issues_list_all(
|
||||||
|
base, token, owner, repo, state="closed", limit=limit
|
||||||
|
)
|
||||||
|
return o + c
|
||||||
|
out: list[dict] = []
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
_, batch = issues_list_page(
|
||||||
|
base, token, owner, repo, state=state, page=page, limit=limit
|
||||||
|
)
|
||||||
|
if not batch:
|
||||||
|
break
|
||||||
|
out.extend(batch)
|
||||||
|
if len(batch) < limit:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def issues_get(
|
||||||
|
base: str, token: str, owner: str, repo: str, number: int
|
||||||
|
) -> tuple[int, Any]:
|
||||||
|
url = f"{base}/api/v1/repos/{owner}/{repo}/issues/{number}"
|
||||||
|
return request_json("GET", url, token)
|
||||||
|
|
||||||
|
|
||||||
|
def issues_create(
|
||||||
|
base: str,
|
||||||
|
token: str,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
*,
|
||||||
|
title: str,
|
||||||
|
body: str = "",
|
||||||
|
labels: list[str] | None = None,
|
||||||
|
) -> tuple[int, Any]:
|
||||||
|
url = f"{base}/api/v1/repos/{owner}/{repo}/issues"
|
||||||
|
return request_json(
|
||||||
|
"POST",
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
{"title": title, "body": body, "labels": labels or []},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def issues_comment(
|
||||||
|
base: str,
|
||||||
|
token: str,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
number: int,
|
||||||
|
body: str,
|
||||||
|
) -> tuple[int, Any]:
|
||||||
|
url = f"{base}/api/v1/repos/{owner}/{repo}/issues/{number}/comments"
|
||||||
|
return request_json("POST", url, token, {"body": body})
|
||||||
|
|
||||||
|
|
||||||
|
def issues_patch(
|
||||||
|
base: str,
|
||||||
|
token: str,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
number: int,
|
||||||
|
fields: dict,
|
||||||
|
) -> tuple[int, Any]:
|
||||||
|
"""Gitea: PATCH issue (state, title, body, …)."""
|
||||||
|
url = f"{base}/api/v1/repos/{owner}/{repo}/issues/{number}"
|
||||||
|
return request_json("PATCH", url, token, fields)
|
||||||
|
|
||||||
|
|
||||||
|
def repo_file_content(
|
||||||
|
base: str,
|
||||||
|
token: str,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
path: str,
|
||||||
|
ref: str = "",
|
||||||
|
) -> tuple[int, Any]:
|
||||||
|
from urllib.parse import quote
|
||||||
|
from base64 import b64decode
|
||||||
|
|
||||||
|
p = quote(path, safe="/")
|
||||||
|
r = f"?ref={ref}" if ref else ""
|
||||||
|
url = f"{base}/api/v1/repos/{owner}/{repo}/contents/{p}{r}"
|
||||||
|
st, payload = request_json("GET", url, token)
|
||||||
|
if st >= 400:
|
||||||
|
return st, payload
|
||||||
|
if isinstance(payload, dict) and payload.get("type") == "file" and payload.get(
|
||||||
|
"content"
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
text = b64decode(payload["content"]).decode("utf-8", errors="replace")
|
||||||
|
return st, {"path": path, "encoding": "text", "content": text}
|
||||||
|
except Exception:
|
||||||
|
return st, payload
|
||||||
|
return st, payload
|
||||||
127
scripts/gitea/mcp_server_gitea.py
Normal file
127
scripts/gitea/mcp_server_gitea.py
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MCP-Server für Gitea (Issues + Datei-Inhalt via API).
|
||||||
|
|
||||||
|
Cursor: in den MCP-Einstellungen dieses Skript starten (siehe MCP_SETUP.md).
|
||||||
|
Transport: stdio (Standard FastMCP).
|
||||||
|
|
||||||
|
Abhängigkeit: pip install "mcp>=1.2.0" (siehe requirements-mcp.txt)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from gitea_lib import (
|
||||||
|
issues_comment,
|
||||||
|
issues_create,
|
||||||
|
issues_get,
|
||||||
|
issues_list_all,
|
||||||
|
issues_list_page,
|
||||||
|
issues_patch,
|
||||||
|
load_dotenv,
|
||||||
|
repo_file_content,
|
||||||
|
repo_root,
|
||||||
|
require_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
from mcp.server.fastmcp import FastMCP # noqa: E402
|
||||||
|
|
||||||
|
mcp = FastMCP(
|
||||||
|
"mitai-gitea",
|
||||||
|
instructions=(
|
||||||
|
"Gitea-Tools für das Repo aus GITEA_OWNER/GITEA_REPO. "
|
||||||
|
"Schließe Issues nur nach klarer Code-Verifikation; sonst Kommentar mit offenen Punkten."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
load_dotenv(repo_root())
|
||||||
|
return require_config()
|
||||||
|
|
||||||
|
|
||||||
|
def _json(obj) -> str:
|
||||||
|
return json.dumps(obj, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def gitea_list_issues(
|
||||||
|
state: str = "open",
|
||||||
|
limit_per_page: int = 50,
|
||||||
|
fetch_all_pages: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""Listet Issues. state: open | closed | all. fetch_all_pages=true holt alle Seiten (kann langsam sein)."""
|
||||||
|
base, token, owner, repo = _cfg()
|
||||||
|
if fetch_all_pages:
|
||||||
|
items = issues_list_all(
|
||||||
|
base, token, owner, repo, state=state, limit=limit_per_page
|
||||||
|
)
|
||||||
|
return _json(
|
||||||
|
[{"number": i.get("number"), "title": i.get("title"), "state": i.get("state")} for i in items]
|
||||||
|
)
|
||||||
|
_, items = issues_list_page(
|
||||||
|
base, token, owner, repo, state=state, page=1, limit=limit_per_page
|
||||||
|
)
|
||||||
|
return _json(
|
||||||
|
[{"number": i.get("number"), "title": i.get("title"), "state": i.get("state")} for i in items]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def gitea_get_issue(issue_number: int) -> str:
|
||||||
|
"""Holt ein Issue inkl. Body, Labels, State (JSON)."""
|
||||||
|
base, token, owner, repo = _cfg()
|
||||||
|
st, payload = issues_get(base, token, owner, repo, issue_number)
|
||||||
|
return _json({"http_status": st, "issue": payload})
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def gitea_create_issue(title: str, body: str = "", labels: str = "") -> str:
|
||||||
|
"""Legt ein Issue an. labels: kommagetrennte Namen, z.B. \"bug,backend\"."""
|
||||||
|
base, token, owner, repo = _cfg()
|
||||||
|
lab = [x.strip() for x in labels.split(",") if x.strip()]
|
||||||
|
st, payload = issues_create(
|
||||||
|
base, token, owner, repo, title=title, body=body, labels=lab
|
||||||
|
)
|
||||||
|
return _json({"http_status": st, "result": payload})
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def gitea_comment_issue(issue_number: int, body: str) -> str:
|
||||||
|
"""Kommentar an ein Issue anhängen."""
|
||||||
|
base, token, owner, repo = _cfg()
|
||||||
|
st, payload = issues_comment(base, token, owner, repo, issue_number, body)
|
||||||
|
return _json({"http_status": st, "result": payload})
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def gitea_close_issue(issue_number: int) -> str:
|
||||||
|
"""Issue schließen (state=closed)."""
|
||||||
|
base, token, owner, repo = _cfg()
|
||||||
|
st, payload = issues_patch(
|
||||||
|
base, token, owner, repo, issue_number, {"state": "closed"}
|
||||||
|
)
|
||||||
|
return _json({"http_status": st, "result": payload})
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def gitea_reopen_issue(issue_number: int) -> str:
|
||||||
|
"""Geschlossenes Issue wieder öffnen."""
|
||||||
|
base, token, owner, repo = _cfg()
|
||||||
|
st, payload = issues_patch(
|
||||||
|
base, token, owner, repo, issue_number, {"state": "open"}
|
||||||
|
)
|
||||||
|
return _json({"http_status": st, "result": payload})
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def gitea_get_repo_file(path: str, git_ref: str = "") -> str:
|
||||||
|
"""Liest eine Datei aus dem Repo über die Gitea-API (Standard: Default-Branch)."""
|
||||||
|
base, token, owner, repo = _cfg()
|
||||||
|
st, payload = repo_file_content(base, token, owner, repo, path, ref=git_ref)
|
||||||
|
return _json({"http_status": st, "payload": payload})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcp.run()
|
||||||
2
scripts/gitea/requirements-mcp.txt
Normal file
2
scripts/gitea/requirements-mcp.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Nur für MCP-Server (nicht im Backend-Container nötig)
|
||||||
|
mcp>=1.2.0
|
||||||
94
tests/phase3_e2e_test.sql
Normal file
94
tests/phase3_e2e_test.sql
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
-- Phase 3 E2E Test: Branching Workflow
|
||||||
|
-- Test workflow: body analysis → logic (if relevanz = "decrease") → then/else paths
|
||||||
|
|
||||||
|
-- 1. Cleanup (use slug for lookup)
|
||||||
|
DELETE FROM workflow_executions WHERE workflow_id IN (
|
||||||
|
SELECT id FROM workflow_definitions WHERE slug = 'phase3-e2e-branching'
|
||||||
|
);
|
||||||
|
DELETE FROM workflow_definitions WHERE slug = 'phase3-e2e-branching';
|
||||||
|
|
||||||
|
-- 2. Create test workflow
|
||||||
|
INSERT INTO workflow_definitions (name, slug, description, graph, active)
|
||||||
|
VALUES (
|
||||||
|
'Phase 3 E2E Test - Branching',
|
||||||
|
'phase3-e2e-branching',
|
||||||
|
'Test workflow with logic node and conditional branching',
|
||||||
|
'{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "start",
|
||||||
|
"type": "start",
|
||||||
|
"position": {"x": 100, "y": 100}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "body_analysis",
|
||||||
|
"type": "analysis",
|
||||||
|
"prompt_slug": "pipeline_body",
|
||||||
|
"question_augmentations": [
|
||||||
|
{
|
||||||
|
"id": "q1",
|
||||||
|
"type": "relevanz",
|
||||||
|
"question": "Hat sich die Fettmasse relevant verändert?",
|
||||||
|
"answer_spectrum": ["increase", "stable", "decrease"],
|
||||||
|
"reasoning_required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": {"x": 100, "y": 200}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "logic_check",
|
||||||
|
"type": "logic",
|
||||||
|
"condition": {
|
||||||
|
"expression": {
|
||||||
|
"operator": "eq",
|
||||||
|
"ref": "body_analysis.relevanz",
|
||||||
|
"value": "decrease"
|
||||||
|
},
|
||||||
|
"then_path": "e3",
|
||||||
|
"else_path": "e4"
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"strategy": "default_path"
|
||||||
|
},
|
||||||
|
"position": {"x": 100, "y": 300}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "decrease_path",
|
||||||
|
"type": "analysis",
|
||||||
|
"prompt_slug": "pipeline_body",
|
||||||
|
"position": {"x": 50, "y": 400}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "not_decrease_path",
|
||||||
|
"type": "analysis",
|
||||||
|
"prompt_slug": "pipeline_body",
|
||||||
|
"position": {"x": 150, "y": 400}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end",
|
||||||
|
"type": "end",
|
||||||
|
"position": {"x": 100, "y": 500}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{"id": "e1", "from": "start", "to": "body_analysis"},
|
||||||
|
{"id": "e2", "from": "body_analysis", "to": "logic_check"},
|
||||||
|
{"id": "e3", "from": "logic_check", "to": "decrease_path", "label": "then"},
|
||||||
|
{"id": "e4", "from": "logic_check", "to": "not_decrease_path", "label": "else"},
|
||||||
|
{"id": "e5", "from": "decrease_path", "to": "end"},
|
||||||
|
{"id": "e6", "from": "not_decrease_path", "to": "end"}
|
||||||
|
]
|
||||||
|
}',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. Verify workflow was created
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
active,
|
||||||
|
jsonb_array_length(graph->'nodes') as node_count,
|
||||||
|
jsonb_array_length(graph->'edges') as edge_count
|
||||||
|
FROM workflow_definitions
|
||||||
|
WHERE slug = 'phase3-e2e-branching';
|
||||||
123
tests/phase4_e2e_test.sql
Normal file
123
tests/phase4_e2e_test.sql
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
-- Phase 4 E2E Test: Branching + Join Workflow
|
||||||
|
-- Test workflow mit 2 parallelen Pfaden, die wieder zusammengeführt werden
|
||||||
|
|
||||||
|
-- 1. Insert workflow definition
|
||||||
|
INSERT INTO workflow_definitions (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
graph,
|
||||||
|
active,
|
||||||
|
created_by
|
||||||
|
) VALUES (
|
||||||
|
'phase4-join-test',
|
||||||
|
'Phase 4 Join Test Workflow',
|
||||||
|
'Test workflow with branching and join node',
|
||||||
|
'{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "start",
|
||||||
|
"type": "start",
|
||||||
|
"position": {"x": 100, "y": 100}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "body_analysis",
|
||||||
|
"type": "analysis",
|
||||||
|
"prompt_slug": "body",
|
||||||
|
"position": {"x": 100, "y": 200},
|
||||||
|
"question_augmentations": [
|
||||||
|
{
|
||||||
|
"id": "relevanz",
|
||||||
|
"type": "relevanz",
|
||||||
|
"question": "Ist eine Gewichtsveränderung relevant?",
|
||||||
|
"answer_spectrum": ["ja", "nein", "unklar"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "decision_logic",
|
||||||
|
"type": "logic",
|
||||||
|
"position": {"x": 100, "y": 300},
|
||||||
|
"condition": {
|
||||||
|
"type": "if",
|
||||||
|
"expression": {
|
||||||
|
"operator": "eq",
|
||||||
|
"ref": "body_analysis.relevanz",
|
||||||
|
"value": "ja"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"strategy": "default_path"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "path_a_nutrition",
|
||||||
|
"type": "analysis",
|
||||||
|
"prompt_slug": "nutrition",
|
||||||
|
"position": {"x": 50, "y": 400},
|
||||||
|
"question_augmentations": [
|
||||||
|
{
|
||||||
|
"id": "prioritaet",
|
||||||
|
"type": "prioritaet",
|
||||||
|
"question": "Wie wichtig ist Ernährungsoptimierung?",
|
||||||
|
"answer_spectrum": ["hoch", "mittel", "niedrig", "unklar"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "path_b_activity",
|
||||||
|
"type": "analysis",
|
||||||
|
"prompt_slug": "activity",
|
||||||
|
"position": {"x": 150, "y": 400},
|
||||||
|
"question_augmentations": [
|
||||||
|
{
|
||||||
|
"id": "prioritaet",
|
||||||
|
"type": "prioritaet",
|
||||||
|
"question": "Wie wichtig ist Trainingsoptimierung?",
|
||||||
|
"answer_spectrum": ["hoch", "mittel", "niedrig", "unklar"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "join_consolidation",
|
||||||
|
"type": "join",
|
||||||
|
"position": {"x": 100, "y": 500},
|
||||||
|
"join_strategy": "best_effort",
|
||||||
|
"skip_handling": "ignore_skipped"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end",
|
||||||
|
"type": "end",
|
||||||
|
"position": {"x": 100, "y": 600}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{"id": "e1", "from_node": "start", "to_node": "body_analysis"},
|
||||||
|
{"id": "e2", "from_node": "body_analysis", "to_node": "decision_logic"},
|
||||||
|
{"id": "e3", "from_node": "decision_logic", "to_node": "path_a_nutrition", "label": "then"},
|
||||||
|
{"id": "e4", "from_node": "decision_logic", "to_node": "path_b_activity", "label": "else"},
|
||||||
|
{"id": "e5", "from_node": "path_a_nutrition", "to_node": "join_consolidation"},
|
||||||
|
{"id": "e6", "from_node": "path_b_activity", "to_node": "join_consolidation"},
|
||||||
|
{"id": "e7", "from_node": "join_consolidation", "to_node": "end"}
|
||||||
|
]
|
||||||
|
}'::jsonb,
|
||||||
|
true,
|
||||||
|
'test-user'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. Verify workflow was created
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
jsonb_array_length(graph->'nodes') as node_count,
|
||||||
|
jsonb_array_length(graph->'edges') as edge_count
|
||||||
|
FROM workflow_definitions
|
||||||
|
WHERE id = 'phase4-join-test';
|
||||||
|
|
||||||
|
-- Expected result:
|
||||||
|
-- id: phase4-join-test
|
||||||
|
-- name: Phase 4 Join Test Workflow
|
||||||
|
-- active: true
|
||||||
|
-- node_count: 7
|
||||||
|
-- edge_count: 7
|
||||||
58
tests/test_join_deployed.py
Normal file
58
tests/test_join_deployed.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Quick test for deployed join_evaluator.py"""
|
||||||
|
|
||||||
|
from join_evaluator import evaluate_join_node
|
||||||
|
from workflow_models import (
|
||||||
|
WorkflowNode, WorkflowGraph, WorkflowEdge,
|
||||||
|
JoinStrategy, NodeStatus, NodeExecutionState
|
||||||
|
)
|
||||||
|
|
||||||
|
# Minimal test: 2 paths → join
|
||||||
|
join_node = WorkflowNode(
|
||||||
|
id="test_join",
|
||||||
|
type="join",
|
||||||
|
join_strategy=JoinStrategy.BEST_EFFORT
|
||||||
|
)
|
||||||
|
|
||||||
|
graph = WorkflowGraph(
|
||||||
|
nodes=[
|
||||||
|
WorkflowNode(id="path_a", type="analysis"),
|
||||||
|
WorkflowNode(id="path_b", type="analysis"),
|
||||||
|
join_node
|
||||||
|
],
|
||||||
|
edges=[
|
||||||
|
WorkflowEdge(id="e1", from_node="path_a", to_node="test_join"),
|
||||||
|
WorkflowEdge(id="e2", from_node="path_b", to_node="test_join")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"node_results": {
|
||||||
|
"path_a": NodeExecutionState(
|
||||||
|
node_id="path_a",
|
||||||
|
status=NodeStatus.EXECUTED,
|
||||||
|
analysis_core="Analysis from path A"
|
||||||
|
),
|
||||||
|
"path_b": NodeExecutionState(
|
||||||
|
node_id="path_b",
|
||||||
|
status=NodeStatus.EXECUTED,
|
||||||
|
analysis_core="Analysis from path B"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = evaluate_join_node(join_node, graph, context)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
print(f"✅ Ready: {result.ready}")
|
||||||
|
print(f"✅ Consolidated cores: {len(result.consolidated_analysis_core)}")
|
||||||
|
print(f"✅ Executed paths: {result.metadata.get('executed_paths')}")
|
||||||
|
print(f"✅ Strategy: {result.metadata.get('join_strategy')}")
|
||||||
|
|
||||||
|
assert result.ready is True
|
||||||
|
assert len(result.consolidated_analysis_core) == 2
|
||||||
|
assert "path_a" in result.consolidated_analysis_core
|
||||||
|
assert "path_b" in result.consolidated_analysis_core
|
||||||
|
|
||||||
|
print("\n🎉 Phase 4 Join Evaluator: DEPLOYED AND WORKING!")
|
||||||
105
tests/test_join_integration.py
Normal file
105
tests/test_join_integration.py
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Integration test: Full workflow with join node"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from workflow_executor import execute_node
|
||||||
|
from workflow_models import (
|
||||||
|
WorkflowNode, WorkflowGraph, WorkflowEdge,
|
||||||
|
JoinStrategy, NodeStatus, NodeExecutionState
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_join_node_integration():
|
||||||
|
"""Test execute_join_node via execute_node dispatcher"""
|
||||||
|
|
||||||
|
# Setup: 2 executed paths
|
||||||
|
path_a_state = NodeExecutionState(
|
||||||
|
node_id="path_a",
|
||||||
|
status=NodeStatus.EXECUTED,
|
||||||
|
analysis_core="Path A completed successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
path_b_state = NodeExecutionState(
|
||||||
|
node_id="path_b",
|
||||||
|
status=NodeStatus.EXECUTED,
|
||||||
|
analysis_core="Path B completed successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Join node
|
||||||
|
join_node = WorkflowNode(
|
||||||
|
id="join",
|
||||||
|
type="join",
|
||||||
|
join_strategy=JoinStrategy.WAIT_ALL
|
||||||
|
)
|
||||||
|
|
||||||
|
# Graph
|
||||||
|
graph = WorkflowGraph(
|
||||||
|
nodes=[
|
||||||
|
WorkflowNode(id="path_a", type="analysis"),
|
||||||
|
WorkflowNode(id="path_b", type="analysis"),
|
||||||
|
join_node
|
||||||
|
],
|
||||||
|
edges=[
|
||||||
|
WorkflowEdge(id="e1", from_node="path_a", to_node="join"),
|
||||||
|
WorkflowEdge(id="e2", from_node="path_b", to_node="join")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Context with previous node results
|
||||||
|
context = {
|
||||||
|
"variables": {},
|
||||||
|
"profile_id": "test-profile",
|
||||||
|
"node_results": {
|
||||||
|
"path_a": path_a_state,
|
||||||
|
"path_b": path_b_state
|
||||||
|
},
|
||||||
|
"active_edges": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute join node via dispatcher
|
||||||
|
async def mock_llm(prompt, model):
|
||||||
|
return "Mock LLM response"
|
||||||
|
|
||||||
|
result = await execute_node(
|
||||||
|
node=join_node,
|
||||||
|
context=context,
|
||||||
|
catalog={},
|
||||||
|
graph=graph,
|
||||||
|
openrouter_call_func=mock_llm
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
print(f"✅ Node executed: {result.node_id}")
|
||||||
|
print(f"✅ Status: {result.status.value}")
|
||||||
|
print(f"✅ Analysis core exists: {result.analysis_core is not None}")
|
||||||
|
|
||||||
|
assert result.node_id == "join"
|
||||||
|
assert result.status == NodeStatus.EXECUTED
|
||||||
|
assert result.analysis_core is not None
|
||||||
|
|
||||||
|
# Check consolidated data
|
||||||
|
import json
|
||||||
|
consolidated = json.loads(result.analysis_core)
|
||||||
|
print(f"✅ Consolidated paths: {len(consolidated)}")
|
||||||
|
assert len(consolidated) == 2
|
||||||
|
assert "path_a" in consolidated
|
||||||
|
assert "path_b" in consolidated
|
||||||
|
|
||||||
|
print(f"✅ Path A analysis: {consolidated['path_a'][:50]}...")
|
||||||
|
print(f"✅ Path B analysis: {consolidated['path_b'][:50]}...")
|
||||||
|
|
||||||
|
print("\n🎉 Integration Test: JOIN NODE WORKING IN WORKFLOW EXECUTOR!")
|
||||||
|
print(" - Both paths consolidated successfully")
|
||||||
|
print(" - Analysis cores merged correctly")
|
||||||
|
print(" - Join strategy executed properly")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
success = asyncio.run(test_join_node_integration())
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
Loading…
Reference in New Issue
Block a user