2026-08-29 11:04:34 +02:00
""" Opt-in debug persist for the admin test spur. Run from backend/: python tests/test_debug_persist.py """
from __future__ import annotations
import json
import os
import sys
import tempfile
from pathlib import Path
ROOT = Path ( __file__ ) . resolve ( ) . parents [ 1 ]
REPO = ROOT . parent
sys . path . insert ( 0 , str ( ROOT ) )
2026-09-07 14:28:17 +02:00
from tests . harness import configure_test_engine
configure_test_engine ( )
2026-08-29 11:04:34 +02:00
os . environ [ " KANSHO_MEDIA_ROOT " ] = str ( Path ( tempfile . gettempdir ( ) ) / " kansho-debug-persist-media " )
os . environ [ " KANSHO_FAKE_PROVIDER " ] = " 1 "
os . environ [ " KANSHO_FAKE_DETECT " ] = " 1 "
Path ( os . environ [ " KANSHO_MEDIA_ROOT " ] ) . mkdir ( parents = True , exist_ok = True )
from fastapi . testclient import TestClient
from identity_store import remember_mapping
from main import app
from privacy_gateway import reset_debug
def expect ( ok : bool , message : str ) - > None :
if not ok :
raise SystemExit ( f " FAIL: { message } " )
print ( f " OK { message } " )
def header ( token : str ) - > dict :
return { " X-Auth-Token " : token }
def open_space_day ( client , headers , title = " Alltag " , date = " 2026-08-28 " ) :
space = client . post ( " /api/journal/spaces " , headers = headers , json = { " title " : title } )
expect ( space . status_code == 200 , f " create space { space . status_code } " )
day = client . post (
f " /api/journal/spaces/ { space . json ( ) [ ' id ' ] } /days " ,
headers = headers ,
json = { " calendar_date " : date } ,
)
expect ( day . status_code == 200 , f " open day { day . status_code } " )
return space . json ( ) , day . json ( )
def blob ( data ) - > str :
return json . dumps ( data , ensure_ascii = False )
def main ( ) - > None :
reset_debug ( )
ui = ( REPO / " frontend " / " src " / " pages " / " AdminDebugPage.jsx " ) . read_text ( encoding = " utf-8 " )
expect ( " Debug persistieren " in ui , " admin page has the persist toggle " )
expect ( " Space / Tag / Gespräche " in ui , " admin page uses space/day/conversation structure " )
expect ( " Gespräch-JSON " in ui , " admin page can download one conversation " )
expect ( " JSON herunterladen " not in ui , " admin page no longer uses a flat download label as primary " )
nav = ( REPO / " frontend " / " src " / " config " / " adminNav.js " ) . read_text ( encoding = " utf-8 " )
expect ( " /admin/debug " in nav , " admin nav lists debug " )
dialogue_ui = ( REPO / " frontend " / " src " / " pages " / " DialoguePage.jsx " ) . read_text ( encoding = " utf-8 " )
expect ( " CallTrace " not in dialogue_ui , " dialogue page no longer shows the live test spur " )
expect ( " Debug herunterladen " in dialogue_ui , " dialogue page can download the conversation debug log " )
day_ui = ( REPO / " frontend " / " src " / " pages " / " JournalDayPage.jsx " ) . read_text ( encoding = " utf-8 " )
expect ( " import CallTrace " not in day_ui , " journal day does not render CallTrace under the thread " )
expect ( " Debug herunterladen " in day_ui , " journal day can download the conversation debug log " )
editor_ui = ( REPO / " frontend " / " src " / " pages " / " JournalEditorPage.jsx " ) . read_text ( encoding = " utf-8 " )
expect ( " import CallTrace " in editor_ui , " journal editor still shows the generate trace " )
expect ( " <h2>Verlauf</h2> " in editor_ui , " journal editor names the generate trace " )
expect ( " Debug herunterladen " in editor_ui , " journal editor can download the generate debug log " )
expect ( " purpose=journal_generate " in editor_ui , " journal editor downloads the generate scope " )
with TestClient ( app ) as client :
setup = client . post (
" /api/auth/setup " ,
json = { " email " : " lars@example.test " , " name " : " Lars " , " password " : " test-pass " } ,
)
expect ( setup . status_code == 200 , f " setup { setup . status_code } " )
headers = header ( setup . json ( ) [ " token " ] )
profile_id = setup . json ( ) [ " profile_id " ]
other = client . post (
" /api/users " ,
headers = headers ,
json = { " email " : " ute@example.test " , " name " : " Ute " , " password " : " user-pass " , " role " : " user " } ,
)
expect ( other . status_code == 200 , " second profile " )
other_headers = header (
client . post ( " /api/auth/login " , json = { " email " : " ute@example.test " , " password " : " user-pass " } ) . json ( ) [ " token " ]
)
settings = client . get ( " /api/admin/debug " , headers = headers )
expect ( settings . status_code == 200 , " admin can read debug settings " )
expect ( settings . json ( ) [ " persist_enabled " ] is False , " persist is off by default " )
expect ( settings . json ( ) [ " run_count " ] == 0 , " no runs at start " )
denied = client . get ( " /api/admin/debug " , headers = other_headers )
expect ( denied . status_code == 403 , " non-admin cannot read debug settings " )
space , day = open_space_day ( client , headers )
day_id = day [ " day " ] [ " id " ]
conv = client . post (
f " /api/journal/days/ { day_id } /conversations " ,
headers = headers ,
json = { " title " : " Gespräch " } ,
)
expect ( conv . status_code == 200 , f " start conversation { conv . status_code } " )
conv_id = conv . json ( ) [ " id " ]
reset_debug ( )
turn = client . post (
f " /api/journal/conversations/ { conv_id } /turn " ,
headers = headers ,
json = { " body " : " Heute war der Markt voll. Ich habe Kirschen gekauft. " } ,
)
expect ( turn . status_code == 200 , f " turn while persist off { turn . status_code } " )
listed = client . get ( " /api/admin/debug/runs " , headers = headers )
expect ( listed . json ( ) [ " runs " ] == [ ] , " off means no persisted steps " )
enabled = client . put ( " /api/admin/debug " , headers = headers , json = { " persist_enabled " : True } )
expect ( enabled . status_code == 200 , " enable persist " )
expect ( enabled . json ( ) [ " persist_enabled " ] is True , " persist flag stored " )
remember_mapping ( profile_id , " Anna " )
reset_debug ( )
first = client . post (
f " /api/journal/conversations/ { conv_id } /turn " ,
headers = headers ,
json = { " body " : " Anna war am Stand. Die Kirschen waren reif. " } ,
)
expect ( first . status_code == 200 , f " first persisted turn { first . status_code } " )
reset_debug ( )
second = client . post (
f " /api/journal/conversations/ { conv_id } /turn " ,
headers = headers ,
json = { " body " : " Später noch der Hafen, ungefähr 18:00 Uhr. " } ,
)
expect ( second . status_code == 200 , f " second persisted turn { second . status_code } " )
listed = client . get ( " /api/admin/debug/runs " , headers = headers )
expect ( listed . status_code == 200 , " list runs " )
runs = listed . json ( ) [ " runs " ]
expect ( len ( runs ) > = 2 , " each turn is stored " )
expect ( all ( item . get ( " purpose " ) == " dialogue_turn " for item in runs [ : 2 ] ) , " listed steps are dialogue turns " )
newest = client . get ( f " /api/admin/debug/runs/ { runs [ 0 ] [ ' id ' ] } " , headers = headers )
expect ( newest . status_code == 200 , " read run detail " )
payload = newest . json ( ) . get ( " payload " ) or { }
stored = blob ( payload )
expect ( " local_label " not in stored , " mapping labels are not persisted " )
expect ( " mapping_table " not in stored , " mapping table is not persisted " )
expect ( " replacements " not in stored , " replacement objects are not persisted " )
details = [
client . get ( f " /api/admin/debug/runs/ { item [ ' id ' ] } " , headers = headers ) . json ( )
for item in runs [ : 2 ]
]
expect (
any ( " Hafen " in blob ( item . get ( " payload " ) or { } ) for item in details ) ,
" intern keeps the step text " ,
)
hafen = next ( item . get ( " payload " ) or { } for item in details if " Hafen " in blob ( item . get ( " payload " ) or { } ) )
expect ( ( hafen . get ( " trace " ) or { } ) . get ( " egress " ) or ( hafen . get ( " call_traces " ) ) , " egress of the step is stored " )
expect ( ( hafen . get ( " user " ) or { } ) . get ( " body " ) , " user line of the step is stored " )
expect ( ( hafen . get ( " assistant " ) or { } ) . get ( " body " ) , " assistant line of the step is stored " )
expect ( hafen . get ( " call_traces " ) or ( hafen . get ( " trace " ) or { } ) . get ( " egress " ) , " gateway calls of the step are stored " )
user_space = client . post ( " /api/journal/spaces " , headers = other_headers , json = { " title " : " Utes Tag " } )
user_day = client . post (
f " /api/journal/spaces/ { user_space . json ( ) [ ' id ' ] } /days " ,
headers = other_headers ,
json = { " calendar_date " : " 2026-08-28 " } ,
)
user_conv = client . post (
f " /api/journal/days/ { user_day . json ( ) [ ' day ' ] [ ' id ' ] } /conversations " ,
headers = other_headers ,
json = { " title " : " Gespräch " } ,
)
user_turn = client . post (
f " /api/journal/conversations/ { user_conv . json ( ) [ ' id ' ] } /turn " ,
headers = other_headers ,
json = { " body " : " Utes privater Satz bleibt ohne Debug-Speicher. " } ,
)
expect ( user_turn . status_code == 200 , " non-admin can still talk " )
after_user = client . get ( " /api/admin/debug/runs " , headers = headers ) . json ( ) [ " runs " ]
expect ( len ( after_user ) == len ( runs ) , " non-admin turns are not persisted " )
expect (
not any ( " Utes privater Satz " in blob ( item ) for item in after_user ) ,
" admin list does not contain the other profile " ,
)
reset_debug ( )
gen = client . post (
f " /api/journal/days/ { day_id } /generate " ,
headers = headers ,
json = { " conversation_ids " : [ conv_id ] } ,
)
expect ( gen . status_code == 200 , f " generate { gen . status_code } " )
after_gen = client . get ( " /api/admin/debug/runs " , headers = headers ) . json ( ) [ " runs " ]
expect ( any ( item . get ( " purpose " ) == " journal_generate " for item in after_gen ) , " journal generate is a stored step " )
exported = client . get ( " /api/admin/debug/export?format=json " , headers = headers )
expect ( exported . status_code == 200 , " json export " )
expect ( " attachment " in ( exported . headers . get ( " content-disposition " ) or " " ) , " json is a download " )
document = exported . json ( )
expect ( document . get ( " kind " ) == " kansho.debug_export " , " export names the contract " )
expect ( document . get ( " version " ) == 2 , " export version is nested tree " )
expect ( document . get ( " scope " ) == " all " , " full export is unscoped " )
expect ( document . get ( " run_count " ) == len ( document . get ( " runs " ) or [ ] ) , " run_count matches runs " )
expect ( len ( document . get ( " runs " ) or [ ] ) > = 3 , " export contains the dialogue and generate steps " )
expect ( ( document . get ( " spaces " ) or [ ] ) , " full export has space tree " )
space = ( document . get ( " spaces " ) or [ { } ] ) [ 0 ]
expect ( space . get ( " title " ) == " Alltag " , " tree names the space " )
day_node = ( space . get ( " days " ) or [ { } ] ) [ 0 ]
expect ( day_node . get ( " calendar_date " ) == " 2026-08-28 " , " tree names the day " )
conv_node = ( day_node . get ( " conversations " ) or [ { } ] ) [ 0 ]
expect ( len ( conv_node . get ( " steps " ) or [ ] ) > = 2 , " tree keeps dialogue steps on the conversation " )
expect ( day_node . get ( " generates " ) , " journal generate sits on the day " )
expect ( " local_label " not in blob ( document ) , " export has no mapping labels " )
created = [ item . get ( " created " ) for item in document [ " runs " ] ]
expect ( created == sorted ( created ) , " export is chronological " )
tree = client . get ( " /api/admin/debug/tree " , headers = headers )
expect ( tree . status_code == 200 , " tree endpoint " )
tree_space = ( tree . json ( ) . get ( " spaces " ) or [ { } ] ) [ 0 ]
expect ( tree_space . get ( " title " ) == " Alltag " , " live tree names the space " )
tree_conv = ( ( tree_space . get ( " days " ) or [ { } ] ) [ 0 ] . get ( " conversations " ) or [ { } ] ) [ 0 ]
expect ( tree_conv . get ( " id " ) == conv_id , " live tree points at the conversation " )
expect ( tree_conv . get ( " step_count " ) > = 2 , " live tree counts steps " )
conv_export = client . get (
f " /api/admin/debug/export?format=json&conversation_id= { conv_id } " ,
headers = headers ,
)
expect ( conv_export . status_code == 200 , " conversation export " )
conv_doc = conv_export . json ( )
expect ( conv_doc . get ( " scope " ) == " conversation " , " conversation export is scoped " )
expect ( conv_doc . get ( " conversation " , { } ) . get ( " id " ) == conv_id , " conversation export names the dialogue " )
expect (
all ( item . get ( " purpose " ) == " dialogue_turn " for item in conv_doc . get ( " runs " ) or [ ] ) ,
" conversation export is the dialogue steps, not the day generate " ,
)
expect ( len ( conv_doc . get ( " runs " ) or [ ] ) > = 2 , " conversation export has both turns " )
expect ( " conversation " in ( conv_export . headers . get ( " content-disposition " ) or " " ) , " conversation filename is distinct " )
latest = client . get (
f " /api/admin/debug/latest?journal_day_id= { day_id } &purpose=journal_generate " ,
headers = headers ,
)
expect ( latest . status_code == 200 , " latest generate endpoint " )
latest_run = latest . json ( ) . get ( " run " ) or { }
expect ( latest_run . get ( " purpose " ) == " journal_generate " , " latest run is the journal generate " )
expect ( latest_run . get ( " journal_day_id " ) == day_id , " latest generate belongs to the day " )
expect ( ( latest_run . get ( " trace " ) or { } ) . get ( " purpose " ) == " journal_generate " or ( latest_run . get ( " trace " ) or { } ) . get ( " stages " ) , " latest generate keeps the trace " )
gen_export = client . get (
f " /api/admin/debug/export?format=json&journal_day_id= { day_id } &purpose=journal_generate " ,
headers = headers ,
)
expect ( gen_export . status_code == 200 , " generate export " )
gen_doc = gen_export . json ( )
expect ( gen_doc . get ( " scope " ) == " journal_generate " , " generate export is scoped " )
expect (
all ( item . get ( " purpose " ) == " journal_generate " for item in gen_doc . get ( " runs " ) or [ ] ) ,
" generate export is only the journal draft steps " ,
)
expect ( len ( gen_doc . get ( " runs " ) or [ ] ) > = 1 , " generate export has the draft run " )
expect ( " journal " in ( gen_export . headers . get ( " content-disposition " ) or " " ) , " generate filename is distinct " )
markdown = client . get ( " /api/admin/debug/export?format=markdown " , headers = headers )
expect ( markdown . status_code == 200 , " markdown export " )
text = markdown . text
expect ( " Kanshō Debug-Export " in text , " markdown has a title " )
expect ( " Intern (Klartext-Vorlage, nicht gesendet) " in text , " markdown has intern section " )
expect ( " Egress (maskiert, gesendet) " in text , " markdown has egress section " )
expect ( " dialogue_turn " in text , " markdown lists dialogue steps " )
expect ( " journal_generate " in text , " markdown lists generate steps " )
bad = client . get ( " /api/admin/debug/export?format=csv " , headers = headers )
expect ( bad . status_code == 400 , " unknown export format is rejected " )
disabled = client . put ( " /api/admin/debug " , headers = headers , json = { " persist_enabled " : False } )
expect ( disabled . json ( ) [ " persist_enabled " ] is False , " persist can be turned off " )
count_before = len ( client . get ( " /api/admin/debug/runs " , headers = headers ) . json ( ) [ " runs " ] )
reset_debug ( )
later = client . post (
f " /api/journal/conversations/ { conv_id } /turn " ,
headers = headers ,
json = { " body " : " Noch ein Satz ohne Persistenz. " } ,
)
expect ( later . status_code == 200 , " turn after disable " )
count_after = len ( client . get ( " /api/admin/debug/runs " , headers = headers ) . json ( ) [ " runs " ] )
expect ( count_after == count_before , " disable stops new writes and keeps history " )
run_id = client . get ( " /api/admin/debug/runs " , headers = headers ) . json ( ) [ " runs " ] [ 0 ] [ " id " ]
removed = client . delete ( f " /api/admin/debug/runs/ { run_id } " , headers = headers )
expect ( removed . status_code == 200 , " delete one run " )
cleared = client . post ( " /api/admin/debug/runs/clear " , headers = headers )
expect ( cleared . status_code == 200 , " clear remaining runs " )
expect ( client . get ( " /api/admin/debug/runs " , headers = headers ) . json ( ) [ " runs " ] == [ ] , " history empty after clear " )
health = client . get ( " /api/admin/health " , headers = headers )
expect ( health . json ( ) [ " inventory " ] [ " debug_persist_enabled " ] is False , " health shows persist off " )
expect ( health . json ( ) [ " inventory " ] [ " debug_runs " ] == 0 , " health shows empty debug count " )
print ( " All debug persist tests passed. " )
if __name__ == " __main__ " :
main ( )