2026-08-25 13:57:23 +02:00
""" Cost-conscious profile review. Run from backend/: python tests/test_profile_review.py """
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
ROOT = Path ( __file__ ) . resolve ( ) . parents [ 1 ]
sys . path . insert ( 0 , str ( ROOT ) )
os . environ [ " KANSHO_DB_PATH " ] = str ( Path ( tempfile . gettempdir ( ) ) / " kansho-profile-review-test.sqlite " )
os . environ [ " KANSHO_FAKE_PROVIDER " ] = " 1 "
os . environ [ " KANSHO_FAKE_DETECT " ] = " 1 "
Path ( os . environ [ " KANSHO_DB_PATH " ] ) . unlink ( missing_ok = True )
from fastapi . testclient import TestClient
from main import app
from privacy_gateway import reset_debug
from profile_review import (
KIND_PACKAGE ,
KIND_RESULT ,
facet_delta ,
is_strong_edit ,
parse_result ,
)
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-20 " ) :
space = client . post ( " /api/journal/spaces " , headers = headers , json = { " title " : title } )
expect ( space . status_code == 200 , f " create space { space . text } " )
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 . text } " )
return space . json ( ) , day . json ( )
def trait_map ( profile : dict ) - > dict :
return { item [ " slug " ] : item for item in profile . get ( " traits " ) or [ ] }
def facet_map ( profile : dict ) - > dict :
return { item [ " facet_key " ] : item for item in profile . get ( " facets " ) or [ ] }
def pending_triggers ( status : dict ) - > set [ str ] :
return { item . get ( " trigger " ) for item in status . get ( " pending_evidence " ) or [ ] }
def main ( ) - > None :
expect ( not is_strong_edit ( " Heute Kirschen am Markt gekauft. " , " Heute Kirschen am Markt gekauft. " ) , " same text is not a strong edit " )
expect (
is_strong_edit (
" Heute war der Markt voll und ich habe Kirschen gekauft. " ,
" Vielleicht bedeutet das weniger. Allerdings wunderte ich mich den ganzen Abend. " ,
) ,
" divergent rewrite is a strong edit " ,
)
expect ( facet_delta ( { " rhythm " : " kurz " } , { " rhythm " : " lang " , " humor " : " trocken " } ) == [ " rhythm " , " humor " ] , " delta lists changed local signals " )
parsed_core = parse_result ( {
" kind " : KIND_RESULT ,
" format_version " : 1 ,
" changes " : [ { " layer " : " core " , " key " : " ignored " , " action " : " keep " , " evidence_ids " : [ ] } ] ,
} )
expect ( parsed_core [ " changes " ] [ 0 ] [ " key " ] == " core " , " core layer maps to core key " )
parsed_create = parse_result ( {
" kind " : KIND_RESULT ,
" format_version " : 1 ,
" changes " : [ {
" layer " : " trait " ,
" slug " : " scene_cuts " ,
" facet_key " : " autobiographical_journal " ,
" action " : " create " ,
" proposed_value " : " wechselt ohne Ankündigung zwischen Szenen " ,
" exemplars " : [ { " excerpt " : " Danach der Hafen. " } ] ,
" evidence_ids " : [ " e1 " ] ,
} ] ,
} )
expect ( parsed_create [ " changes " ] [ 0 ] [ " slug " ] == " scene_cuts " , " dynamic trait slugs are accepted " )
2026-08-25 17:16:35 +02:00
expect ( parsed_create [ " changes " ] [ 0 ] [ " action " ] == " add " , " create maps to add " )
external = parse_result ( {
" kind " : KIND_RESULT ,
" format_version " : 1 ,
" changes " : [ {
" layer " : " trait " ,
" slug " : " dry_humor " ,
" action " : " add " ,
" proposed_value " : " trocken " ,
" evidence_basis " : [ " external_chat_history " ] ,
" exemplars " : [ { " excerpt " : " haha das war irgendwie lustig " , " source_id " : " invented " , " evidence_basis " : [ " external_chat_history " ] } ] ,
} ] ,
} )
expect ( external [ " changes " ] [ 0 ] [ " evidence_basis " ] == [ " external_chat_history " ] , " external basis kept " )
expect ( external [ " changes " ] [ 0 ] [ " exemplars " ] [ 0 ] . get ( " source_id " ) in ( None , " " ) , " external chat does not invent kansho source ids " )
from_proposal = parse_result ( {
" kind " : KIND_RESULT ,
" format_version " : 1 ,
" mode " : " initial_build " ,
" profile " : {
" traits " : [ {
" slug " : " scene_cuts " ,
" facet_key " : " autobiographical_journal " ,
" statement " : " wechselt ohne Ankündigung zwischen Szenen " ,
" evidence_basis " : [ " external_chat_history " ] ,
" exemplars " : [ { " excerpt " : " Danach der Hafen. " , " source_id " : " invented " , " evidence_basis " : [ " external_chat_history " ] } ] ,
} ]
} ,
} )
expect ( from_proposal [ " changes " ] [ 0 ] [ " slug " ] == " scene_cuts " , " full profile proposal becomes changes " )
expect ( from_proposal [ " changes " ] [ 0 ] [ " exemplars " ] [ 0 ] . get ( " source_id " ) in ( None , " " ) , " proposal from external chat has no kansho source id " )
dayone = parse_result ( {
" kind " : KIND_RESULT ,
" format_version " : 1 ,
" mode " : " initial_build " ,
" core " : { " summary " : " direkte Erlebnisrekonstruktion " , " generalizability " : " nur Journal " } ,
" facets " : [ { " key " : " autobiographical_journal " , " summary " : " zeitliche Feinauflösung " } ] ,
" traits " : [ {
" slug " : " mikrochronologische-kausalrekonstruktion " ,
" label " : " Mikrochronologische Rekonstruktion " ,
" scope " : " core " ,
" facet_key " : " core " ,
" statement " : " Erlebnisse in ihrer tatsächlichen Reihenfolge. " ,
" evidence_basis " : [ " dayone_export " ] ,
" exemplars " : [ {
" excerpt " : " Das leichte Kribbeln in der Hand veranlasste mich einen Krankenwagen zu rufen. " ,
" occurred_at " : " 2024-10-04 " ,
" evidence_basis " : " dayone_export " ,
} ] ,
} ] ,
} )
slugs = { item [ " slug " ] for item in dayone [ " changes " ] }
expect ( " core " in slugs or any ( item [ " layer " ] == " core " for item in dayone [ " changes " ] ) , " top-level core becomes a change " )
expect ( any ( item [ " layer " ] == " facet " for item in dayone [ " changes " ] ) , " top-level facet becomes a change " )
expect ( " mikrochronologische_kausalrekonstruktion " in slugs , " hyphen slugs are coerced " )
trait = next ( item for item in dayone [ " changes " ] if item [ " layer " ] == " trait " )
expect ( trait [ " evidence_basis " ] == [ " external_context " ] , " dayone export is external context " )
expect ( trait [ " exemplars " ] [ 0 ] . get ( " source_id " ) in ( None , " " ) , " dayone exemplars have no kansho source id " )
expect ( trait [ " proposed_value " ] . startswith ( " Erlebnisse " ) , " trait statement is kept " )
2026-08-25 13:57:23 +02:00
reset_debug ( )
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 , " setup " )
headers = header ( setup . json ( ) [ " token " ] )
_space , day = open_space_day ( client , headers )
day_id = day [ " day " ] [ " id " ]
first = client . post (
" /api/journal/entries " ,
headers = headers ,
json = {
" journal_day_id " : day_id ,
" title " : " Markt " ,
" body " : " Heute um 7:30 Uhr bin ich zum Markt. Danach habe ich Kirschen gekauft. " ,
" origin " : " user_edit " ,
} ,
)
expect ( first . status_code == 200 , " first save " )
writing = client . get ( " /api/journal/writing-profile " , headers = headers ) . json ( )
expect ( writing . get ( " lifecycle " ) == " uninitialized " , " first save leaves profile unconfirmed " )
expect ( " rhythm " not in { item . get ( " facet_key " ) for item in writing . get ( " facets " ) or [ ] } , " no heuristic facet bootstrap " )
expect ( not writing . get ( " traits " ) , " no inferred traits " )
status = client . get ( " /api/journal/writing-profile/review " , headers = headers ) . json ( )
expect ( status [ " pending_evidence " ] == [ ] , " first save does not enqueue a review " )
second = client . post (
" /api/journal/entries " ,
headers = headers ,
json = {
" journal_day_id " : day_id ,
" title " : " Anders " ,
" body " : (
" Haha das war irgendwie lustig, lol. Vielleicht bedeutet das weniger, als es scheint. "
" Allerdings wunderte ich mich. Im Grunde war der Abend merkwürdig und offen. "
) ,
" origin " : " user_edit " ,
} ,
)
expect ( second . status_code == 200 , " second save " )
status = client . get ( " /api/journal/writing-profile/review " , headers = headers ) . json ( )
expect ( status [ " pending_evidence " ] == [ ] , " continuous queue waits for confirmed profile " )
frozen = client . patch ( " /api/journal/writing-profile " , headers = headers , json = { " governance " : " frozen " } )
expect ( frozen . status_code == 200 , " frozen " )
client . post (
" /api/journal/entries " ,
headers = headers ,
json = {
" journal_day_id " : day_id ,
" title " : " Frozen " ,
" body " : " Ganz anderer Ton, witzig, lol, haha, und trotzdem keine Review. " ,
" origin " : " user_edit " ,
} ,
)
status = client . get ( " /api/journal/writing-profile/review " , headers = headers ) . json ( )
expect ( status [ " pending_evidence " ] == [ ] , " unconfirmed frozen still has no queue " )
client . patch ( " /api/journal/writing-profile " , headers = headers , json = { " governance " : " learning " } )
draft = client . post (
" /api/journal/entries " ,
headers = headers ,
json = {
" journal_day_id " : day_id ,
" title " : " Draft " ,
" body " : (
" Heute war der Markt voll und ich bin früh gegangen und habe Kirschen gekauft "
" und danach den Hafen gesehen, ganz ruhig und ohne Eile. "
) ,
" origin " : " accepted_draft " ,
} ,
)
expect ( draft . status_code == 200 , " accepted draft stored " )
edited = client . post (
" /api/journal/entries " ,
headers = headers ,
json = {
" journal_day_id " : draft . json ( ) [ " journal_day_id " ] if False else day_id ,
" entry_id " : draft . json ( ) [ " id " ] ,
" title " : " Draft " ,
" body " : (
" Der Nachmittag kippte. Ich blieb länger als geplant, sprach kaum, "
" und schrieb später nur zwei knappe Zeilen ohne Markt und ohne Hafen. "
) ,
" origin " : " user_edit " ,
} ,
)
expect ( edited . status_code == 200 , " strong user edit " )
status = client . get ( " /api/journal/writing-profile/review " , headers = headers ) . json ( )
expect ( " user_edit_of_draft " not in pending_triggers ( status ) , " draft edit waits until the profile is confirmed " )
conv = client . post ( f " /api/journal/days/ { day_id } /conversations " , headers = headers , json = { " title " : " Chat " } )
long_body = (
" Heute früh bin ich zum Markt gegangen und habe Kirschen gekauft, danach den Hafen gesehen, "
" später noch Brot geholt, mit jemandem gesprochen, Tee getrunken, den Wind bemerkt, "
" und am Abend den Tag in Ruhe nachvollzogen, ohne etwas zu erzwingen oder nach einem Muster zu suchen. "
" Zwischendurch war eine Pause, dann noch ein kurzer Blick aufs Wasser, und zum Schluss bin ich langsam heimgegangen, "
" ohne den Faden zu verlieren und ohne aus dem Gesprächsverlauf eine dauerhafte Vorliebe zu machen. "
)
expect ( len ( long_body . split ( ) ) > = 60 , " sample text is long enough " )
reset_debug ( )
long_turn = client . post (
f " /api/journal/conversations/ { conv . json ( ) [ ' id ' ] } /turn " ,
headers = headers ,
json = { " body " : long_body } ,
)
expect ( long_turn . status_code == 200 , " long turn " )
2026-08-26 10:42:20 +02:00
last_long = long_turn . json ( ) . get ( " trace " ) or { }
2026-08-25 13:57:23 +02:00
2026-08-26 10:42:20 +02:00
expect ( last_long . get ( " purpose " ) == " dialogue_turn " , " long turn still has no review call " )
2026-08-25 13:57:23 +02:00
status = client . get ( " /api/journal/writing-profile/review " , headers = headers ) . json ( )
expect ( " dialogue_sample " not in pending_triggers ( status ) , " dialogue is not sampled before confirm " )
corpus = client . post (
" /api/journal/writing-profile/corpus " ,
headers = headers ,
json = {
" items " : [
{
" body " : " Am ersten Urlaubstag um 6:00 Uhr Tee auf dem Balkon, danach der Hafen. Kirschen vom Markt. " ,
" occurred_at " : " 2018-07-02 " ,
" context_hint " : " vacation_diary " ,
} ,
{
" body " : " Haha das war irgendwie lustig, lol. Der Abend kippte, ohne dass ich es erklärt hätte. " ,
" occurred_at " : " 2024-08-11 " ,
" context_hint " : " vacation_diary " ,
} ,
{
" body " : " Vielleicht bedeutet das weniger. Allerdings wunderte ich mich über die Stille nach dem Markt. " ,
" occurred_at " : " 2025-03-04 " ,
" context_hint " : " autobiographical_journal " ,
} ,
]
} ,
)
expect ( corpus . status_code == 200 , f " corpus import { corpus . text } " )
dated = [ item for item in corpus . json ( ) [ " sources " ] if item . get ( " kind " ) == " imported_text " ]
expect ( len ( dated ) > = 3 , " historical corpus is more than two entries " )
expect ( any ( ( item . get ( " occurred_at " ) or " " ) . startswith ( " 2018-07-02 " ) for item in dated ) , " older source keeps time " )
expect ( any ( item . get ( " recency_role " ) == " long_term " for item in dated ) , " older text is long-term evidence " )
paste = client . post ( " /api/journal/writing-profile/initial-build/paste " , headers = headers )
expect ( paste . status_code == 200 , f " initial paste { paste . text } " )
package = paste . json ( ) [ " package " ]
2026-08-25 17:16:35 +02:00
expect ( package [ " kind " ] == KIND_PACKAGE , " paste uses analysis package contract " )
2026-08-25 13:57:23 +02:00
expect ( package [ " format_version " ] == 1 , " package version " )
expect ( package [ " mode " ] == " initial_build " , " unconfirmed profile opens an initial build " )
expect ( package [ " existing_before_new " ] , " package names Existing-before-New " )
expect ( any ( item . get ( " kind " ) == " trait_hint " for item in package [ " seed_catalog " ] ) , " seed catalog is ordering help " )
expect ( len ( package . get ( " corpus " ) or [ ] ) > = 3 , " initial package includes historical corpus " )
expect ( any ( item . get ( " occurred_at " ) for item in package [ " corpus " ] ) , " corpus items keep time " )
2026-08-25 17:16:35 +02:00
instruction = ( package . get ( " instruction " ) or " " ) + ( paste . json ( ) . get ( " prompt " ) or " " )
2026-08-25 13:57:23 +02:00
expect ( " bestehenden Trait bestätigen " in instruction , " instruction starts with confirm " )
expect ( " erst dann einen neuen Trait anlegen " in instruction , " instruction defers create " )
expect ( " autobiographical_journal " in instruction , " vacation/journal facet is named " )
2026-08-25 17:16:35 +02:00
expect ( " external_chat_history " in instruction , " external chat history is named as provenance " )
expect ( " Wenn du aus dem bisherigen Verlauf dieses Chats bereits weitere authentische Texte " in instruction , " paste prompt may use existing chat knowledge " )
expect ( " Expliziter Initial Profile Build " not in instruction , " trigger meta is not style evidence " )
expect ( " Explizite Nutzer-Review " not in ( paste . json ( ) . get ( " prompt " ) or " " ) , " ui trigger text is not in prompt " )
expect ( package . get ( " compiled_brief_excluded " ) is True , " compiled brief is not the package " )
expect ( " kansho.profile_analysis_result " in ( paste . json ( ) . get ( " prompt " ) or " " ) , " copy prompt names the result contract " )
expect ( " profile " in ( package . get ( " expected_result " ) or { } ) , " initial_build expects a full proposal " )
2026-08-25 13:57:23 +02:00
evidence_id = ( package . get ( " evidences " ) or [ { } ] ) [ 0 ] . get ( " id " )
imported = client . post (
" /api/journal/writing-profile/review/import " ,
headers = headers ,
json = {
" review_id " : paste . json ( ) [ " id " ] ,
" result " : {
" kind " : KIND_RESULT ,
" format_version " : 1 ,
" target " : " writing " ,
" mode " : " initial_build " ,
" changes " : [
{
" layer " : " trait " ,
" slug " : " dry_humor " ,
" facet_key " : " autobiographical_journal " ,
2026-08-25 17:16:35 +02:00
" action " : " add " ,
2026-08-25 13:57:23 +02:00
" label " : " Trockener Humor " ,
" proposed_value " : " gelegentlich trocken, aus Paste-Review " ,
" rationale " : " Journal-Evidenz, kein globaler Core. " ,
2026-08-25 17:16:35 +02:00
" evidence_basis " : [ " kansho_sources " ] ,
2026-08-25 13:57:23 +02:00
" evidence_ids " : [ evidence_id ] if evidence_id else [ ] ,
2026-08-25 17:16:35 +02:00
" exemplars " : [ { " excerpt " : " haha das war irgendwie lustig " , " occurred_at " : " 2024-08-11 " , " evidence_basis " : [ " kansho_sources " ] } ] ,
2026-08-25 13:57:23 +02:00
}
] ,
} ,
} ,
)
expect ( imported . status_code == 200 , f " import result { imported . text } " )
2026-08-25 17:16:35 +02:00
expect ( imported . json ( ) [ " applied " ] == 0 , " import does not apply the current profile " )
expect ( imported . json ( ) . get ( " accepted " ) is False , " proposal waits for user review " )
expect ( " dry_humor " not in trait_map ( imported . json ( ) [ " profile " ] ) , " proposal is not auto-merged " )
accepted = client . post (
" /api/journal/writing-profile/review/accept " ,
headers = headers ,
json = { " review_id " : imported . json ( ) [ " review_id " ] , " baseline " : True } ,
)
expect ( accepted . status_code == 200 , f " accept baseline { accepted . text } " )
expect ( accepted . json ( ) [ " applied " ] > = 1 , " baseline applies accepted traits " )
profile = accepted . json ( ) [ " profile " ]
2026-08-25 13:57:23 +02:00
expect ( profile . get ( " lifecycle " ) == " confirmed " , " successful initial build confirms the profile " )
expect ( trait_map ( profile ) [ " dry_humor " ] [ " statement " ] . startswith ( " gelegentlich trocken " ) , " created trait is stored " )
expect ( trait_map ( profile ) [ " dry_humor " ] [ " facet_key " ] == " autobiographical_journal " , " vacation evidence stays in journal facet " )
expect ( any ( " haha " in ( ref . get ( " excerpt " ) or " " ) for ref in trait_map ( profile ) [ " dry_humor " ] . get ( " exemplars " ) or [ ] ) , " trait keeps exemplar " )
expect ( " core " not in facet_map ( profile ) or not ( facet_map ( profile ) . get ( " core " ) or { } ) . get ( " value " ) , " single source kind does not invent a global core " )
2026-08-25 17:16:35 +02:00
review_paste = client . post ( " /api/journal/writing-profile/review/paste " , headers = headers )
expect ( review_paste . status_code == 200 , f " review paste { review_paste . text } " )
review_pkg = review_paste . json ( ) [ " package " ]
expect ( review_pkg [ " mode " ] == " review " , " confirmed profile opens incremental review " )
expect ( " nicht bei jeder Review ein komplett neues Profil " in ( ( review_pkg . get ( " instruction " ) or " " ) + ( review_paste . json ( ) . get ( " prompt " ) or " " ) ) , " review stays incremental " )
expect ( review_pkg [ " profile " ] . get ( " traits " ) , " review package includes existing traits " )
expect ( " profile " not in ( review_pkg . get ( " expected_result " ) or { } ) , " review result is incremental " )
from writing_profile_store import compile_task_brief
task_brief = compile_task_brief ( profile [ " profile_id " ] , " journal_generate " )
expect ( " trocken " in task_brief , " journal generate uses relevant traits " )
expect ( " Tee auf dem Balkon " not in task_brief , " task brief does not dump historical full texts " )
two_changes = client . post (
" /api/journal/writing-profile/review/import " ,
headers = headers ,
json = {
" review_id " : review_paste . json ( ) [ " id " ] ,
" result " : {
" kind " : KIND_RESULT ,
" format_version " : 1 ,
" mode " : " review " ,
" changes " : [
{
" layer " : " trait " ,
" slug " : " dry_humor " ,
" action " : " update " ,
" proposed_value " : " sollte einzeln ablehnbar bleiben " ,
" evidence_basis " : [ " kansho_sources " ] ,
} ,
{
" layer " : " trait " ,
" slug " : " scene_cuts " ,
" facet_key " : " autobiographical_journal " ,
" action " : " add " ,
" proposed_value " : " wechselt ohne Ankündigung zwischen Szenen " ,
" evidence_basis " : [ " external_chat_history " ] ,
" exemplars " : [ { " excerpt " : " Danach der Hafen. " , " evidence_basis " : [ " external_chat_history " ] } ] ,
} ,
] ,
} ,
} ,
)
expect ( two_changes . status_code == 200 , f " review import { two_changes . text } " )
expect ( two_changes . json ( ) [ " applied " ] == 0 , " review import stays a proposal " )
accepted_one = client . post (
" /api/journal/writing-profile/review/accept " ,
headers = headers ,
json = { " review_id " : two_changes . json ( ) [ " review_id " ] , " indexes " : [ 1 ] } ,
)
expect ( accepted_one . status_code == 200 , f " accept one { accepted_one . text } " )
expect ( " scene_cuts " in trait_map ( accepted_one . json ( ) [ " profile " ] ) , " accepted change is stored " )
expect (
trait_map ( accepted_one . json ( ) [ " profile " ] ) [ " dry_humor " ] [ " statement " ] . startswith ( " gelegentlich trocken " ) ,
" unaccepted change is not applied " ,
)
expect ( len ( ( accepted_one . json ( ) . get ( " proposal " ) or { } ) . get ( " changes " ) or [ ] ) == 1 , " remaining proposal stays " )
rejected_one = client . post (
" /api/journal/writing-profile/review/reject " ,
headers = headers ,
json = { " review_id " : accepted_one . json ( ) [ " review_id " ] , " indexes " : [ 0 ] } ,
)
expect ( rejected_one . status_code == 200 , f " reject remaining { rejected_one . text } " )
expect (
trait_map ( client . get ( " /api/journal/writing-profile " , headers = headers ) . json ( ) ) [ " dry_humor " ] [ " statement " ] . startswith ( " gelegentlich trocken " ) ,
" rejected change never reaches the current profile " ,
)
2026-08-25 13:57:23 +02:00
drift = client . post (
" /api/journal/entries " ,
headers = headers ,
json = {
" journal_day_id " : day_id ,
" title " : " Drift " ,
" body " : (
" Haha das war irgendwie lustig, lol. Vielleicht bedeutet das weniger, als es scheint. "
" Allerdings wunderte ich mich. Im Grunde war der Abend merkwürdig und offen. "
) ,
" origin " : " user_edit " ,
} ,
)
expect ( drift . status_code == 200 , " drift save after confirm " )
status = client . get ( " /api/journal/writing-profile/review " , headers = headers ) . json ( )
expect ( " heuristic_drift " in pending_triggers ( status ) , " after confirm, local heuristic queues evidence " )
draft_after = client . post (
" /api/journal/entries " ,
headers = headers ,
json = {
" journal_day_id " : day_id ,
" title " : " Draft nach Confirm " ,
" body " : (
" Heute war der Markt voll und ich bin früh gegangen und habe Kirschen gekauft "
" und danach den Hafen gesehen, ganz ruhig und ohne Eile. "
) ,
" origin " : " accepted_draft " ,
} ,
)
expect ( draft_after . status_code == 200 , " accepted draft after confirm " )
edited = client . post (
" /api/journal/entries " ,
headers = headers ,
json = {
" journal_day_id " : day_id ,
" entry_id " : draft_after . json ( ) [ " id " ] ,
" title " : " Draft nach Confirm " ,
" body " : (
" Der Nachmittag kippte noch einmal. Ich blieb, sprach kaum, "
" und schrieb später nur zwei knappe Zeilen ohne Markt und ohne Hafen und ohne Kirschen. "
) ,
" origin " : " user_edit " ,
} ,
)
expect ( edited . status_code == 200 , " strong user edit after confirm " )
status = client . get ( " /api/journal/writing-profile/review " , headers = headers ) . json ( )
expect ( " user_edit_of_draft " in pending_triggers ( status ) , " strong draft edit is a review trigger " )
reset_debug ( )
long_turn = client . post (
f " /api/journal/conversations/ { conv . json ( ) [ ' id ' ] } /turn " ,
headers = headers ,
json = { " body " : long_body } ,
)
expect ( long_turn . status_code == 200 , " long turn after confirm " )
expect ( long_turn . json ( ) [ " conversation " ] . get ( " preferences " ) in ( None , [ ] ) , " turn does not write interaction prefs " )
status = client . get ( " /api/journal/writing-profile/review " , headers = headers ) . json ( )
expect ( " dialogue_sample " in pending_triggers ( status ) , " long dialogue may be sampled locally " )
pending_samples = [ item for item in status [ " pending_evidence " ] if item [ " trigger " ] == " dialogue_sample " ]
second_long = client . post (
f " /api/journal/conversations/ { conv . json ( ) [ ' id ' ] } /turn " ,
headers = headers ,
json = { " body " : long_body + " Noch ein Nachsatz über den Abend und die Kirschen. " } ,
)
expect ( second_long . status_code == 200 , " second long turn " )
status = client . get ( " /api/journal/writing-profile/review " , headers = headers ) . json ( )
samples_now = [ item for item in status [ " pending_evidence " ] if item [ " trigger " ] == " dialogue_sample " ]
expect ( len ( samples_now ) == len ( pending_samples ) , " dialogue sample has a cooldown " )
interaction = client . get ( " /api/journal/interaction-profile " , headers = headers ) . json ( )
expect ( interaction [ " preferences " ] == [ ] , " dialogue sample is not an interaction preference " )
expect ( interaction [ " suggestions " ] == [ ] , " silence and flow are not interaction learning " )
client . patch ( " /api/journal/writing-profile " , headers = headers , json = { " governance " : " advising " } )
client . post (
" /api/journal/writing-profile/import " ,
headers = headers ,
json = { " body " : " Witzig, wirklich witzig, haha das war irgendwie lustig, lol, und trotzdem nur Candidate. " } ,
)
reset_debug ( )
api_review = client . post ( " /api/journal/writing-profile/review/api " , headers = headers )
expect ( api_review . status_code == 200 , f " api review { api_review . text } " )
2026-08-26 10:42:20 +02:00
api_trace = api_review . json ( ) . get ( " trace " ) or { }
2026-08-25 13:57:23 +02:00
2026-08-26 10:42:20 +02:00
expect ( api_trace . get ( " purpose " ) == " profile_review " , " api review uses dedicated purpose " )
expect ( api_trace . get ( " layer " ) == " profilreview " , " trace is not a dialogue turn " )
2026-08-25 17:16:35 +02:00
expect ( api_review . json ( ) [ " applied " ] == 0 , " api result is a proposal, not an auto-apply " )
2026-08-25 13:57:23 +02:00
expect ( api_review . json ( ) [ " suggested " ] > = 1 , " advising queues review as suggestion " )
expect (
any ( " trocken " in ( item . get ( " proposed_value " ) or " " ) for item in api_review . json ( ) [ " profile " ] [ " suggestions " ] ) ,
" api result is a suggestion in advising " ,
)
expect (
" aus Paste-Review " in trait_map ( api_review . json ( ) [ " profile " ] ) [ " dry_humor " ] [ " statement " ] ,
" advising does not silently overwrite " ,
)
client . patch ( " /api/journal/writing-profile " , headers = headers , json = { " governance " : " frozen " } )
client . post ( " /api/journal/writing-profile/review/paste " , headers = headers )
humor_before = trait_map ( client . get ( " /api/journal/writing-profile " , headers = headers ) . json ( ) ) [ " dry_humor " ] [ " statement " ]
frozen_import = client . post (
" /api/journal/writing-profile/review/import " ,
headers = headers ,
json = {
" result " : {
" kind " : KIND_RESULT ,
" format_version " : 1 ,
" changes " : [
{
" layer " : " trait " ,
" slug " : " dry_humor " ,
" action " : " precisify " ,
" proposed_value " : " sollte nicht greifen " ,
" rationale " : " frozen " ,
" evidence_ids " : [ ] ,
}
] ,
}
} ,
)
expect ( frozen_import . status_code == 200 , " frozen import accepted as document " )
2026-08-25 17:16:35 +02:00
expect ( frozen_import . json ( ) [ " applied " ] == 0 , " frozen import stays a proposal " )
expect ( trait_map ( client . get ( " /api/journal/writing-profile " , headers = headers ) . json ( ) ) [ " dry_humor " ] [ " statement " ] == humor_before , " frozen keeps trait " )
brief_as_profile = client . post (
" /api/journal/writing-profile/review/import " ,
headers = headers ,
json = { " result " : { " kind " : KIND_RESULT , " format_version " : 1 , " compiled_brief " : " Das ist nur der Brief. " } } ,
)
expect ( brief_as_profile . status_code == 400 , " compiled brief is not an import format " )
empty_result = client . post (
" /api/journal/writing-profile/review/import " ,
headers = headers ,
json = { " result " : { " kind " : KIND_RESULT , " format_version " : 1 , " mode " : " review " } } ,
)
expect ( empty_result . status_code == 400 , " empty analysis result is rejected " )
2026-08-25 13:57:23 +02:00
bad = client . post (
" /api/journal/writing-profile/review/import " ,
headers = headers ,
json = { " result " : { " kind " : " kansho.writing_profile " , " format_version " : 1 , " changes " : [ ] } } ,
)
expect ( bad . status_code == 400 , " wrong kind is rejected " )
print ( " profile review ok " )
if __name__ == " __main__ " :
main ( )