2025-12-12 09:11:01 +01:00
# tests/test_smart_chunking_integration.py (Final für Stabilität)
2025-12-12 08:47:12 +01:00
import asyncio
import unittest
import os
import sys
from pathlib import Path
from typing import List , Dict
2025-12-12 09:11:01 +01:00
import time # Nur für debug/sleep
2025-12-12 08:47:12 +01:00
2025-12-12 08:59:08 +01:00
# --- PFAD-KORREKTUR ---
ROOT_DIR = Path ( __file__ ) . resolve ( ) . parent . parent
sys . path . insert ( 0 , str ( ROOT_DIR ) )
# ----------------------
2025-12-12 08:47:12 +01:00
# Import der Kernkomponenten
from app . core import chunker
from app . core import derive_edges
2025-12-12 09:11:01 +01:00
from app . services . semantic_analyzer import SemanticAnalyzer
from app . core . chunker import extract_frontmatter_from_text # Wichtig für Status Check
2025-12-12 08:59:08 +01:00
2025-12-12 09:07:33 +01:00
# 1. Definieren der Test-Note (Simuliert eine journal.md Datei)
2025-12-12 08:47:12 +01:00
TEST_NOTE_ID = " 20251211-journal-sem-test "
TEST_NOTE_TYPE = " journal "
TEST_MARKDOWN = """
- - -
id : 20251211 - journal - sem - test
title : Tägliches Log - Semantischer Test
type : journal
status : active
created : 2025 - 12 - 11
tags : [ test , daily - log ]
- - -
# Log-Eintrag 2025-12-11
Heute war ein guter Tag . Zuerst habe ich mit der R1 Meditation begonnen , um meinen Nordstern Fokus zu klären . Das Ritual [ [ leitbild - rituale - system ] ] hat mir geholfen , ruhig in den Tag zu starten . Ich habe gespürt , wie wichtig meine [ [ leitbild - werte #Integrität]] für meine Entscheidungen ist. Das ist das Fundament.
Am Nachmittag gab es einen Konflikt bei der Karate - Trainer - Ausbildung . Ein Schüler war uneinsichtig . Ich habe die Situation nach [ [ leitbild - prinzipien #P4 Gerechtigkeit & Fairness]] behandelt und beide Seiten gehört (Steelman). Das war anstrengend, aber ich habe meine [[leitbild-rollen#Karate-Trainer]] Mission erfüllt. Die Konsequenz war klar und ruhig.
Abends habe ich den wöchentlichen Load - Check mit meinem Partner gemacht . Das Paar - Ritual [ [ leitbild - rituale - system #R5]] hilft, das Ziel [[leitbild-ziele-portfolio#Nordstern Partner]] aktiv zu verfolgen. Es ist der operative Rhythmus für uns beide.
"""
2025-12-12 09:11:01 +01:00
# Helper, um den Client im vorhandenen Loop zu schließen (Muss in der Klasse sein)
async def _async_close_analyzer ( analyzer ) :
await analyzer . close ( )
2025-12-12 08:47:12 +01:00
class TestSemanticChunking ( unittest . TestCase ) :
2025-12-12 09:11:01 +01:00
# 2. Ressourcen-Management
2025-12-12 09:07:33 +01:00
_analyzer_instance = None
@classmethod
def setUpClass ( cls ) :
""" Initialisiert den SemanticAnalyzer einmalig und asynchron. """
# Da LLMService async ist, nutzen wir die Singleton-Instanz der Klasse
cls . _analyzer_instance = SemanticAnalyzer ( )
2025-12-12 09:11:01 +01:00
# Stellt sicher, dass der Chunker diese Instanz verwenden kann.
2025-12-12 09:07:33 +01:00
chunker . _semantic_analyzer_instance = cls . _analyzer_instance
@classmethod
def tearDownClass ( cls ) :
2025-12-12 09:11:01 +01:00
""" Schließt den httpx.AsyncClient nach allen Tests (Korrektur des Event Loop Fehlers). """
2025-12-12 09:07:33 +01:00
if cls . _analyzer_instance :
2025-12-12 09:11:01 +01:00
# Wir nutzen run_coroutine_threadsafe, da der Loop geschlossen ist
loop = asyncio . get_event_loop ( )
if loop . is_running ( ) :
# Wenn Loop noch läuft, planen wir den Aufruf
asyncio . run_coroutine_threadsafe ( cls . _analyzer_instance . close ( ) , loop )
else :
# Andernfalls starten wir einen neuen temporären Loop nur zum Schließen
asyncio . run ( cls . _analyzer_instance . close ( ) )
2025-12-12 09:07:33 +01:00
2025-12-12 08:47:12 +01:00
def setUp ( self ) :
self . config = chunker . get_chunk_config ( TEST_NOTE_TYPE )
def test_a_strategy_selection ( self ) :
""" Prüft, ob die Strategie ' semantic_llm ' für den Typ ' journal ' gewählt wird. """
self . assertEqual ( self . config [ ' strategy ' ] , ' semantic_llm ' ,
" Fehler: ' journal ' sollte die Strategie ' semantic_llm ' nutzen. " )
def test_b_llm_chunking_and_injection ( self ) :
"""
2025-12-12 09:11:01 +01:00
Prüft den gesamten End - to - End - Flow : 1. LLM - Chunking , 2. Kanten - Injektion , 3. Kanten - Erkennung .
( Diese Tests setzen voraus , dass das LLM JSON liefert )
2025-12-12 08:47:12 +01:00
"""
2025-12-12 09:07:33 +01:00
2025-12-12 08:47:12 +01:00
# --- 1. Chunking (Asynchron) ---
chunks = asyncio . run ( chunker . assemble_chunks (
note_id = TEST_NOTE_ID ,
md_text = TEST_MARKDOWN ,
note_type = TEST_NOTE_TYPE
) )
print ( f " \n --- LLM Chunker Output: { len ( chunks ) } Chunks --- " )
2025-12-12 09:07:33 +01:00
2025-12-12 09:11:01 +01:00
# Assertion B1: Zerlegung (Das LLM muss mehr als 1 Chunk liefern)
2025-12-12 08:47:12 +01:00
self . assertTrue ( len ( chunks ) > 1 ,
2025-12-12 09:11:01 +01:00
" Assertion B1 Fehler: LLM hat nicht zerlegt (Fallback aktiv). Prüfe LLM-Stabilität. " )
2025-12-12 08:47:12 +01:00
2025-12-12 09:11:01 +01:00
# --- 2. Injektion prüfen ---
2025-12-12 08:47:12 +01:00
chunk_1_text = chunks [ 0 ] . text
self . assertIn ( " [[rel: " , chunk_1_text ,
2025-12-12 09:07:33 +01:00
" Assertion B2 Fehler: Der Chunk-Text muss die injizierte [[rel: Kante enthalten. " )
2025-12-12 08:47:12 +01:00
# --- 3. Kanten-Derivation (Synchron) ---
edges = derive_edges . build_edges_for_note (
note_id = TEST_NOTE_ID ,
2025-12-12 09:07:33 +01:00
chunks = [ c . __dict__ for c in chunks ]
2025-12-12 08:47:12 +01:00
)
print ( f " --- Edge Derivation Output: { len ( edges ) } Kanten --- " )
2025-12-12 09:11:01 +01:00
# Assertion B3: Mindestens 3 LLM-Kanten (inline:rel)
2025-12-12 08:47:12 +01:00
llm_generated_edges = [
e for e in edges
if e . get ( ' rule_id ' ) == ' inline:rel ' and e . get ( ' source_id ' ) . startswith ( TEST_NOTE_ID + ' #sem ' )
]
self . assertTrue ( len ( llm_generated_edges ) > = 3 ,
2025-12-12 09:11:01 +01:00
" Assertion B3 Fehler: Es wurden weniger als 3 semantische Kanten gefunden. " )
2025-12-12 08:47:12 +01:00
2025-12-12 09:11:01 +01:00
# Assertion B4: Check für die Matrix-Logik / Werte-Kante
2025-12-12 08:47:12 +01:00
has_matrix_kante = any (
2025-12-12 09:07:33 +01:00
e [ ' target_id ' ] . startswith ( ' leitbild-werte ' ) and e [ ' kind ' ] in [ ' based_on ' , ' derived_from ' ]
2025-12-12 08:47:12 +01:00
for e in llm_generated_edges
)
self . assertTrue ( has_matrix_kante ,
2025-12-12 09:07:33 +01:00
" Assertion B4 Fehler: Die Matrix-Logik / Werte-Kante wurde nicht erkannt. " )
2025-12-12 08:47:12 +01:00
print ( " \n ✅ Integrationstest für Semantic Chunking erfolgreich. " )
def test_c_draft_status_prevention ( self ) :
""" Prüft, ob ' draft ' Status semantic_llm auf by_heading überschreibt. """
DRAFT_MARKDOWN = TEST_MARKDOWN . replace ( " status: active " , " status: draft " )
# 1. Chunking mit Draft Status
chunks = asyncio . run ( chunker . assemble_chunks (
note_id = TEST_NOTE_ID ,
md_text = DRAFT_MARKDOWN ,
note_type = TEST_NOTE_TYPE
) )
# 2. Prüfen der Chunker-IDs
2025-12-12 09:07:33 +01:00
# Assertion C1: LLM-Chunking muss verhindert werden (darf NICHT mit '#sem' starten)
2025-12-12 08:47:12 +01:00
self . assertFalse ( chunks [ 0 ] . id . startswith ( TEST_NOTE_ID + ' #sem ' ) ,
2025-12-12 09:07:33 +01:00
" Assertion C1 Fehler: LLM-Chunking wurde für den Status ' draft ' nicht verhindert. " )
2025-12-12 08:47:12 +01:00
2025-12-12 09:07:33 +01:00
# Assertion C2: Fallback-Strategie sollte by_heading sein (ID muss mit '#c' starten)
2025-12-12 08:59:08 +01:00
self . assertTrue ( chunks [ 0 ] . id . startswith ( TEST_NOTE_ID + ' #c ' ) ,
2025-12-12 09:07:33 +01:00
" Assertion C2 Fehler: Fallback-Strategie ' by_heading ' wurde nicht korrekt ausgeführt. " )
2025-12-12 08:47:12 +01:00
2025-12-12 08:59:08 +01:00
print ( f " \n ✅ Prevention Test: Draft-Status hat LLM-Chunking verhindert (Fallback ID: { chunks [ 0 ] . id } ). " )
2025-12-12 09:07:33 +01:00
if __name__ == ' __main__ ' :
2025-12-12 09:11:01 +01:00
print ( " Starte den Semantic Chunking Integrationstest. " )
2025-12-12 09:07:33 +01:00
unittest . main ( )