2026-08-28 08:40:28 +02:00
""" Named journal generation guidelines: selection, snapshot, mixed sources. """
from __future__ import annotations
import os
import re
import sys
import tempfile
from pathlib import Path
from unittest . mock import patch
ROOT = Path ( __file__ ) . resolve ( ) . parents [ 1 ]
FRONTEND = ROOT . parent / " frontend "
sys . path . insert ( 0 , str ( ROOT ) )
os . environ [ " KANSHO_DB_PATH " ] = str ( Path ( tempfile . gettempdir ( ) ) / " kansho-journal-guidelines-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 db import get_db , init_db
from engine import load_active_prompt
from identity_store import confirm_identity , remember_mapping
from journal_editorial import GENERATE_SEED_REVISION
from journal_generation_policy import (
PURPOSE_JOURNAL ,
SELECTION_KEYS ,
SLOT_TO_ID_KEY ,
CatalogError ,
GenerationPolicyError ,
archive_guideline ,
clone_guideline ,
compile_selection ,
create_guideline ,
default_selection_ids ,
get_guideline ,
get_or_create_selection ,
list_guidelines ,
load_seed_document ,
load_selection ,
overview_payload ,
publish_guideline ,
save_selection ,
seed_generation_instructions ,
2026-08-29 11:04:34 +02:00
set_default_guideline ,
2026-08-28 08:40:28 +02:00
snapshot_summary ,
update_guideline ,
validate_selection ,
)
from journal_generate import unattested_journal_content
from journal_store import current_draft
from main import app
from privacy_gateway import install_test_recorder , reset_debug
from providers import ChatResult
PREVIOUS_PROMPT_CHARS = 3468
HARD_FACTS = (
" keine neuen Tatsachen " ,
" Plan und Vollzug " ,
" Verneinungen " ,
" Unsicherheiten " ,
" [[PERSON:01]] " ,
)
BANNED_CODE_PHRASES = (
" Überarbeite den Text substanziell " ,
" Erhalte sämtliche belegten Ereignisse " ,
" künstlich zu literarisieren " ,
" Modus prose_edit " ,
" Modus notes_to_journal " ,
" Korrigiere nur Rechtschreibung, Grammatik und Zeichensetzung " ,
)
BANNED_RUNTIME_TOKENS = (
" transformation_strength " ,
" detail_retention " ,
" voice_strength " ,
" narrative_shaping " ,
" min_value " ,
" max_value " ,
" remember_generation_policy " ,
" generation_policy " ,
)
SEED = load_seed_document ( )
MIXED_SOURCES_INSTRUCTION = (
" Die Quellen können aus Fließtext, Stichpunkten und Satzfragmenten bestehen. "
" Behandle jede Passage entsprechend ihrer Form: Redigiere vorhandenen Fließtext, "
" verwandle Stichpunkte und Fragmente in vollständige Prosa und verbinde beides zu einem einheitlichen Eintrag. "
" Zwinge nicht den gesamten Tag in einen einzigen Quellenmodus. "
)
REQUIRED_KEYS = {
( " transformation " , " correction " ) ,
( " transformation " , " copyedit " ) ,
( " transformation " , " reshape " ) ,
( " transformation " , " substantial " ) ,
( " detail " , " compact " ) ,
( " detail " , " selected " ) ,
( " detail " , " broad " ) ,
( " detail " , " complete " ) ,
( " voice " , " neutral " ) ,
( " voice " , " light " ) ,
( " voice " , " noticeable " ) ,
( " voice " , " clear " ) ,
2026-08-29 20:28:59 +02:00
( " voice " , " with_examples " ) ,
( " voice " , " legacy_neutral " ) ,
( " voice " , " legacy_light " ) ,
( " voice " , " legacy_noticeable " ) ,
( " voice " , " legacy_clear " ) ,
2026-08-28 08:40:28 +02:00
( " narrative " , " chronicle " ) ,
( " narrative " , " structured " ) ,
( " narrative " , " weighted " ) ,
( " narrative " , " emphasized " ) ,
}
def has_token ( text : str , token : str ) - > bool :
return re . search ( rf " (?<![A-Za-z0-9_]) { re . escape ( token ) } (?![A-Za-z0-9_]) " , text ) is not None
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 intern_of ( payload : dict ) - > str :
for stage in ( payload . get ( " trace " ) or { } ) . get ( " stages " ) or [ ] :
if stage . get ( " purpose " ) == " journal_generate " :
return stage . get ( " intern " ) or " "
return ( payload . get ( " trace " ) or { } ) . get ( " intern " ) or " "
def sources_block ( intern : str ) - > str :
parts = ( intern or " " ) . split ( " \n CURRENT_DAY_SOURCES \n " )
if len ( parts ) < 2 :
return intern or " "
rest = parts [ - 1 ]
for marker in ( " \n EXISTING_TEXT \n " , " \n AUSGABE \n " ) :
if marker in rest :
rest = rest . split ( marker , 1 ) [ 0 ]
break
return rest
def seed_item ( slot : str , key : str ) - > dict :
for item in ( SEED . get ( " slots " ) or { } ) . get ( slot , { } ) . get ( " variants " ) or [ ] :
if item . get ( " guideline_key " ) == key :
return item
raise SystemExit ( f " FAIL: missing seed { slot } / { key } " )
def seed_id ( slot : str , key : str ) - > str :
return seed_item ( slot , key ) [ " id " ]
def seed_instruction ( slot : str , key : str ) - > str :
return seed_item ( slot , key ) . get ( " instruction " ) or " "
def selection_of ( * * keys : str ) - > dict [ str , str ] :
return { SLOT_TO_ID_KEY [ slot ] : seed_id ( slot , key ) for slot , key in keys . items ( ) }
def default_ids ( ) - > dict [ str , str ] :
return selection_of (
transformation = " substantial " ,
detail = " complete " ,
voice = " clear " ,
narrative = " weighted " ,
)
def low_ids ( ) - > dict [ str , str ] :
return selection_of (
transformation = " correction " ,
detail = " compact " ,
voice = " neutral " ,
narrative = " chronicle " ,
)
def high_ids ( ) - > dict [ str , str ] :
return selection_of (
transformation = " substantial " ,
detail = " complete " ,
voice = " clear " ,
narrative = " emphasized " ,
)
def selection_meta ( payload : dict ) - > dict :
return ( payload . get ( " trace " ) or { } ) . get ( " generation_selection " ) or { }
def test_runtime_has_no_numeric_policy ( ) - > None :
files = [
ROOT / " journal_generation_policy.py " ,
ROOT / " journal_generate.py " ,
ROOT / " routers " / " journal.py " ,
ROOT / " routers " / " generation_instructions.py " ,
FRONTEND / " src " / " pages " / " JournalDayPage.jsx " ,
FRONTEND / " src " / " pages " / " AdminGenerationPage.jsx " ,
]
for path in files :
text = path . read_text ( encoding = " utf-8 " )
for token in BANNED_RUNTIME_TOKENS :
if token == " generation_policy " and path . name == " journal_generation_policy.py " :
continue
if token == " generation_policy " and path . name == " generation_instructions.py " :
continue
expect ( not has_token ( text , token ) , f " { path . name } has no { token } " )
day = ( FRONTEND / " src " / " pages " / " JournalDayPage.jsx " ) . read_text ( encoding = " utf-8 " )
2026-08-29 20:28:59 +02:00
expect ( " Stilquellen " in day , " voice select can show which style sources apply " )
expect ( " type= \" range \" " not in day , " journal page has no extra sliders for style context " )
expect ( day . count ( " <select " ) == 1 , " journal page maps four slots onto one select, not extra style-context fields " )
2026-08-28 08:40:28 +02:00
expect ( " policy-slider " not in day , " journal page has no slider class " )
expect ( " SLOT_SELECTS " in day , " journal page names the four selects " )
expect ( day . count ( " slot: " ) > = 4 , " journal page has four independent slots " )
expect ( " <select " in day , " journal page uses select fields " )
expect ( " Diese Auswahl als Standard merken " in day , " journal page can remember explicitly " )
2026-08-29 11:04:34 +02:00
expect ( " if (filled) return current " in day , " settings load does not overwrite a filled selection " )
expect ( " Auswahl nicht mehr aktiv " in day , " stale guideline ids stay visible as invalid " )
expect ( " Rev. $ {option.revision} " in day , " options show the guideline revision " )
2026-08-28 08:40:28 +02:00
expect ( " Faktenregeln und Datenschutz bleiben unabhängig " in day , " facts/privacy note remains " )
editor = ( FRONTEND / " src " / " pages " / " JournalEditorPage.jsx " ) . read_text ( encoding = " utf-8 " )
expect ( " generation_summary " in editor , " editor can show the generation snapshot " )
admin = ( FRONTEND / " src " / " pages " / " AdminGenerationPage.jsx " ) . read_text ( encoding = " utf-8 " )
expect ( " <textarea " not in admin . split ( " generation-admin-editor " ) [ 0 ] , " admin overview has no textareas " )
expect ( admin . count ( " <textarea " ) == 1 , " admin opens one instruction editor at a time " )
2026-08-29 20:28:59 +02:00
expect ( " Stilquellen für diese Ausprägung " in admin , " admin edits style context with the instruction " )
expect ( " Vorgänger " in admin , " admin shows predecessor or clone origin " )
expect ( " include_core " in admin , " admin names core inclusion " )
expect ( " Writing-Profile-Core " in admin , " admin labels core in German product terms " )
2026-08-28 08:40:28 +02:00
expect ( " backend/config/ " not in admin , " admin copy does not advertise seed file paths " )
expect ( " Quellenmodus " not in admin , " admin has no source-mode management " )
expect ( " source_mode " not in admin , " admin source has no source_mode slot " )
expect ( " prose_edit " not in admin , " admin has no prose_edit control " )
expect ( " notes_to_journal " not in admin , " admin has no notes_to_journal control " )
expect ( " Quellenmodus " not in day , " journal page has no source-mode control " )
expect ( " source_mode " not in day , " journal page has no source_mode field " )
expect ( " editorial_mode " not in day , " journal page has no editorial_mode field " )
trace_ui = ( FRONTEND / " src " / " components " / " CallTrace.jsx " ) . read_text ( encoding = " utf-8 " )
expect ( " Editorial Mode " not in trace_ui , " trace UI does not present source mode as a run decision " )
expect ( " source_mode " not in trace_ui , " trace UI has no source_mode label " )
2026-08-29 11:04:34 +02:00
expect ( " Chunks technisch verarbeitet " in trace_ui , " trace separates technical chunk coverage " )
expect ( " Semantische Identitätserkennung " in trace_ui , " trace names semantic uncertainty " )
expect ( " nicht garantiert " in trace_ui , " trace does not claim semantic completeness " )
2026-08-29 20:28:59 +02:00
expect ( " Stilanwendung " in trace_ui , " trace shows requested versus effective style application " )
expect ( " cloned_from " in trace_ui , " trace can show the predecessor id " )
2026-08-29 11:04:34 +02:00
expect ( " Gespeicherter Entwurf " in trace_ui , " trace shows the stored draft " )
expect ( " Technisch verarbeitete Chunks bedeuten keine vollständige " in trace_ui , " trace explains coverage is not identity certainty " )
2026-08-28 08:40:28 +02:00
def test_seed_and_compiler ( ) - > None :
init_db ( )
for rel in ( " journal_generation_policy.py " , " journal_editorial.py " ) :
source = ( ROOT / rel ) . read_text ( encoding = " utf-8 " )
for phrase in BANNED_CODE_PHRASES :
expect ( phrase not in source , f " { rel } does not hardcode { phrase !r} " )
items = list_guidelines ( PURPOSE_JOURNAL , include_instruction = True )
got = { ( item [ " slot " ] , item [ " guideline_key " ] ) for item in items }
expect ( REQUIRED_KEYS < = got , " seed creates every required guideline " )
expect ( " source_mode " not in ( SEED . get ( " slots " ) or { } ) , " seed document has no source_mode slot " )
expect ( sum ( 1 for item in items if item [ " slot " ] == " source_mode " ) == 0 , " source modes are not seeded " )
expect ( all ( item [ " status " ] == " active " for item in items if item [ " is_system_seed " ] ) , " seed rows start active " )
defaults = compile_selection ( default_selection_ids ( ) )
expect ( defaults . ids [ " transformation " ] == seed_id ( " transformation " , " substantial " ) , " default transformation is substantial " )
expect (
defaults . instructions [ " transformation_instructions " ] == seed_instruction ( " transformation " , " substantial " ) ,
" default transformation instruction matches the seed " ,
)
expect (
defaults . instructions [ " detail_instructions " ] == seed_instruction ( " detail " , " complete " ) ,
" default detail instruction matches the seed " ,
)
expect (
defaults . instructions [ " voice_instructions " ] == seed_instruction ( " voice " , " clear " ) ,
" default voice instruction matches the seed " ,
)
expect (
defaults . instructions [ " narrative_instructions " ] == seed_instruction ( " narrative " , " weighted " ) ,
" default narrative instruction matches the seed " ,
)
expect ( defaults . seed_revision == SEED [ " seed_revision " ] , " compiled policy names the seed revision " )
2026-08-29 20:28:59 +02:00
expect ( defaults . style_context [ " include_core " ] is True , " default voice includes core " )
expect ( defaults . style_context [ " include_facet " ] is True , " default voice includes facet " )
expect ( defaults . style_context [ " include_traits " ] is True , " default voice includes traits " )
expect ( defaults . style_context [ " include_style_examples " ] is False , " default voice omits style examples " )
2026-08-28 08:40:28 +02:00
expect ( " source_mode_instructions " not in defaults . instructions , " compiler has no source-mode instruction " )
2026-08-29 20:28:59 +02:00
free = compile_selection ( { * * default_selection_ids ( ) , " voice_id " : seed_id ( " voice " , " neutral " ) } )
expect ( free . style_context == {
" include_core " : False ,
" include_facet " : False ,
" include_traits " : False ,
" include_style_examples " : False ,
} , " profile-free voice selects no style sources " )
examples = compile_selection ( { * * default_selection_ids ( ) , " voice_id " : seed_id ( " voice " , " with_examples " ) } )
expect ( all ( examples . style_context . values ( ) ) , " with-examples voice selects all style sources " )
2026-08-28 08:40:28 +02:00
low = compile_selection ( low_ids ( ) )
high = compile_selection ( high_ids ( ) )
expect ( low . keys [ " transformation " ] == " correction " , " low transformation is named " )
expect ( high . keys [ " narrative " ] == " emphasized " , " high narrative is named " )
expect ( " source_mode_instructions " not in low . instructions , " low compile has no source mode " )
expect ( " source_mode_instructions " not in high . instructions , " high compile has no source mode " )
expect ( " {{ " not in " " . join ( low . instructions . values ( ) ) , " compiled instructions contain no placeholders " )
for bad in ( None , [ ] , " 75 " , True , { " transformation_strength " : 75 } , { " transformation_id " : seed_id ( " transformation " , " substantial " ) } ) :
try :
validate_selection ( bad )
expect ( False , " incomplete or numeric selection must be rejected " )
except GenerationPolicyError as exc :
expect ( exc . code == " invalid_generation_selection " , " rejection uses invalid_generation_selection " )
draft = create_guideline (
PURPOSE_JOURNAL ,
" transformation " ,
{
" guideline_key " : " drafty " ,
" label " : " Entwurf " ,
" summary " : " nicht wählbar " ,
" instruction " : " Nur ein Draft. " ,
} ,
)
try :
validate_selection ( { * * default_ids ( ) , " transformation_id " : draft [ " id " ] } )
expect ( False , " drafts must not be selectable " )
except GenerationPolicyError :
expect ( True , " drafts are not selectable " )
cloned = clone_guideline ( seed_id ( " transformation " , " correction " ) )
original = get_guideline ( seed_id ( " transformation " , " correction " ) , include_instruction = True )
update_guideline (
cloned [ " id " ] ,
{
" guideline_key " : original [ " guideline_key " ] ,
" label " : original [ " label " ] ,
" summary " : original [ " summary " ] ,
" instruction " : " CHANGED CLONE TEXT " ,
} ,
)
expect (
get_guideline ( original [ " id " ] , include_instruction = True ) [ " instruction " ] == original [ " instruction " ] ,
" cloning does not change the original " ,
)
try :
update_guideline ( original [ " id " ] , { " label " : " still active " , " instruction " : original [ " instruction " ] , " guideline_key " : original [ " guideline_key " ] } )
expect ( False , " published guidelines must be immutable " )
except CatalogError as exc :
expect ( exc . code == " guideline_immutable " , " published guidelines are not silently overwritten " )
published_clone = publish_guideline ( cloned [ " id " ] )
archived = archive_guideline ( published_clone [ " id " ] )
expect ( archived [ " status " ] == " archived " , " published clones can be archived " )
try :
validate_selection ( { * * default_ids ( ) , " transformation_id " : published_clone [ " id " ] } )
expect ( False , " archived guidelines must not be selectable for new runs " )
except GenerationPolicyError :
expect ( True , " archived guidelines are not selectable for new runs " )
with get_db ( ) as conn :
seed_generation_instructions ( conn )
expect (
get_guideline ( original [ " id " ] , include_instruction = True ) [ " instruction " ] == original [ " instruction " ] ,
" seed does not overwrite published guidelines " ,
)
with get_db ( ) as conn :
conn . execute (
"""
INSERT INTO generation_guidelines (
id , purpose , slot , guideline_key , label , summary , instruction , sort_order ,
status , revision , cloned_from , is_default , is_system_seed , seed_id , seed_revision ,
created , updated
)
VALUES (
' legacy-source-mode ' , ? , ' source_mode ' , ' prose_edit ' , ' Fließtext ' , ' ' ,
' LEGACY MODE TEXT ' , 0 , ' active ' , 1 , NULL , 0 , 0 , ' ' , ' ' , datetime ( ' now ' ) , datetime ( ' now ' )
)
""" ,
( PURPOSE_JOURNAL , ) ,
)
seed_generation_instructions ( conn )
archived_legacy = conn . execute (
" SELECT status FROM generation_guidelines WHERE id = ' legacy-source-mode ' "
) . fetchone ( )
expect ( archived_legacy [ " status " ] == " archived " , " persisted source modes are archived " )
expect ( " source_mode " not in overview_payload ( ) [ " slots " ] , " admin overview hides archived source modes " )
def test_prompt_contract ( ) - > None :
init_db ( )
prompt = load_active_prompt ( " mvp.journal_generate " )
text = prompt . get ( " template " ) or " "
expect ( len ( text ) < PREVIOUS_PROMPT_CHARS , " new prompt is shorter than the previous default " )
print ( f " OK prompt length { len ( text ) } after, { PREVIOUS_PROMPT_CHARS } before " )
for key in (
" {{ transformation_instructions}} " ,
" {{ detail_instructions}} " ,
" {{ voice_instructions}} " ,
" {{ narrative_instructions}} " ,
" {{ writing_profile}} " ,
" {{ style_examples}} " ,
" {{ reconstruction}} " ,
" {{ existing_text}} " ,
) :
expect ( key in text , f " prompt contains { key } " )
expect ( " {{ source_mode_instructions}} " not in text , " prompt has no source-mode placeholder " )
expect ( " {{ editorial_mode}} " not in text , " prompt has no editorial_mode placeholder " )
expect ( " {{ editorial_instructions}} " not in text , " prompt has no editorial_instructions placeholder " )
expect ( MIXED_SOURCES_INSTRUCTION in text , " prompt contains the mixed-source instruction " )
2026-08-29 20:28:59 +02:00
expect ( " STYLE_EXAMPLES dienen ausschließlich " not in text , " standing prompt has no orphaned STYLE_EXAMPLES sentence " )
2026-08-28 08:40:28 +02:00
for fact in HARD_FACTS :
expect ( fact in text , f " hard fact rule remains: { fact } " )
expect ( " ich gieng zum laden " not in text , " no synthetic prose example " )
expect ( " nachbarhund im garten " not in text , " no synthetic notes example " )
expect ( " EDITORIAL_MODE " not in text , " source mode is not a user-facing prompt heading " )
2026-08-29 20:28:59 +02:00
expect ( GENERATE_SEED_REVISION == " 2026-08-29-voice-legacy-immutable-v1 " , " seed revision constant tracks immutable voice catalog " )
2026-08-28 08:40:28 +02:00
with get_db ( ) as conn :
row = conn . execute (
" SELECT seed_revision, template, default_template FROM ai_prompts WHERE slug = ? " ,
( " mvp.journal_generate " , ) ,
) . fetchone ( )
2026-08-29 20:28:59 +02:00
expect ( row [ " seed_revision " ] == " 2026-08-29-voice-legacy-immutable-v1 " , " untouched default is updated to immutable voice catalog " )
2026-08-28 08:40:28 +02:00
expect ( row [ " template " ] == row [ " default_template " ] , " untouched template matches the seed " )
expect ( " source_mode " not in ( SEED . get ( " slots " ) or { } ) , " source modes stay out of the seed " )
def test_unattested_alias ( ) - > None :
mappings = [
{
" local_label " : " Clarissa " ,
" canonical_label " : " Clarissa " ,
" token " : " PERSON:01 " ,
" aliases " : [ " Sushi " ] ,
" demask_label " : " Clarissa " ,
}
]
expect (
unattested_journal_content (
" Clarissa kam ins Wohnzimmer. " ,
[ " Heute war Sushi im Wohnzimmer. " ] ,
mappings ,
[ " PERSON:01 " ] ,
)
is None ,
" canonical demask of an attested alias is not unattested_identity " ,
)
expect (
unattested_journal_content (
" Hanna kam ins Wohnzimmer. " ,
[ " Heute war Sushi im Wohnzimmer. " ] ,
mappings + [ { " local_label " : " Hanna " , " token " : " PERSON:99 " } ] ,
[ " PERSON:01 " ] ,
)
== " unattested_identity " ,
" a different person remains unattested " ,
)
def main ( ) - > None :
test_runtime_has_no_numeric_policy ( )
test_seed_and_compiler ( )
test_prompt_contract ( )
test_unattested_alias ( )
reset_debug ( )
with TestClient ( app ) as client :
setup = client . post (
" /api/auth/setup " ,
json = { " email " : " ada@example.test " , " name " : " Ada " , " password " : " test-pass " } ,
)
headers = header ( setup . json ( ) [ " token " ] )
profile_id = setup . json ( ) [ " profile_id " ]
settings = client . get ( " /api/journal/generation-settings " , headers = headers )
expect ( settings . status_code == 200 , f " generation-settings { settings . text } " )
body = settings . json ( )
expect ( body [ " selection " ] == default_ids ( ) , " profile defaults are the named system standards " )
expect ( set ( body [ " selection " ] ) == set ( SELECTION_KEYS ) , " settings expose the four guideline ids " )
for slot , options in body [ " options " ] . items ( ) :
expect ( options , f " { slot } has active options " )
expect ( all ( " instruction " not in item for item in options ) , f " { slot } options hide prompt text " )
expect ( all ( item [ " id " ] != " draft " for item in options ) , f " { slot } options are real ids " )
stored = get_or_create_selection ( profile_id )
expect ( { key : stored [ key ] for key in SELECTION_KEYS } == default_ids ( ) , " persisted defaults match system standards " )
with get_db ( ) as conn :
conn . execute (
" INSERT INTO profiles (id, email, name, password_hash, role) VALUES (?, ?, ?, ?, ' user ' ) " ,
( " other-profile " , " other@example.test " , " Other " , " x " ) ,
)
save_selection ( " other-profile " , high_ids ( ) )
save_selection ( profile_id , low_ids ( ) )
expect ( load_selection ( profile_id ) [ " transformation_id " ] == seed_id ( " transformation " , " correction " ) , " profile A keeps its own selection " )
expect ( load_selection ( " other-profile " ) [ " transformation_id " ] == seed_id ( " transformation " , " substantial " ) , " profile B is isolated " )
save_selection ( profile_id , default_ids ( ) )
space = client . post ( " /api/journal/spaces " , headers = headers , json = { " title " : " Policy " } )
day = client . post (
f " /api/journal/spaces/ { space . json ( ) [ ' id ' ] } /days " ,
headers = headers ,
json = { " calendar_date " : " 2026-08-27 " } ,
)
conv = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /conversations " ,
headers = headers ,
json = { " title " : " Tag " } ,
)
turn = client . post (
f " /api/journal/conversations/ { conv . json ( ) [ ' id ' ] } /turn " ,
headers = headers ,
json = { " body " : " Ich war am Markt. Vielleicht bleibe ich kürzer. Brot holen wollte ich noch, habe es aber nicht gemacht. " } ,
)
expect ( turn . status_code == 200 , f " turn { turn . status_code } " )
rejected = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = { " conversation_ids " : [ conv . json ( ) [ " id " ] ] , " generation_selection " : { " transformation_id " : " missing " } } ,
)
expect ( rejected . status_code == 400 , " unknown selection is rejected " )
expect ( ( rejected . json ( ) . get ( " detail " ) or { } ) . get ( " code " ) == " invalid_generation_selection " , " rejection uses the selection code " )
after_reject = client . get ( " /api/journal/generation-settings " , headers = headers ) . json ( )
expect ( after_reject [ " selection " ] == default_ids ( ) , " rejected values are not stored " )
2026-08-29 11:04:34 +02:00
save_selection ( profile_id , low_ids ( ) )
set_default_guideline ( seed_id ( " transformation " , " substantial " ) )
expect ( load_selection ( profile_id ) [ " transformation_id " ] == seed_id ( " transformation " , " correction " ) , " setting a catalog default does not rewrite a stored selection " )
save_selection ( profile_id , default_ids ( ) )
blocked_calls = { " n " : 0 }
def no_provider ( * _a , * * _k ) :
blocked_calls [ " n " ] + = 1
raise AssertionError ( " invalid selection must not call a provider " )
with patch ( " privacy_gateway.complete_model " , no_provider ) , patch ( " entity_detect.complete_chat " , no_provider ) :
before_provider = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = {
" conversation_ids " : [ conv . json ( ) [ " id " ] ] ,
" generation_selection " : { * * default_ids ( ) , " transformation_id " : " missing " } ,
} ,
)
expect ( before_provider . status_code == 400 , " invalid selection still fails closed " )
expect ( blocked_calls [ " n " ] == 0 , " invalid selection aborts before detect and generate " )
2026-08-28 08:40:28 +02:00
draft = client . post (
" /api/admin/generation-instructions/journal_generate " ,
headers = headers ,
json = {
" slot " : " voice " ,
" guideline_key " : " hidden_draft " ,
" label " : " versteckt " ,
" summary " : " Draft " ,
" instruction " : " Nicht für neue Läufe. " ,
} ,
)
expect ( draft . status_code == 200 , f " admin draft { draft . text } " )
blocked_draft = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = {
" conversation_ids " : [ conv . json ( ) [ " id " ] ] ,
" generation_selection " : { * * default_ids ( ) , " voice_id " : draft . json ( ) [ " id " ] } ,
} ,
)
expect ( blocked_draft . status_code == 400 , " drafts are rejected for new runs " )
reset_debug ( )
recorder = install_test_recorder ( )
snapshot_run = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = {
" conversation_ids " : [ conv . json ( ) [ " id " ] ] ,
" generation_selection " : low_ids ( ) ,
" remember_generation_selection " : False ,
} ,
)
expect ( snapshot_run . status_code == 200 , f " low generate { snapshot_run . text } " )
intern = intern_of ( snapshot_run . json ( ) )
meta = selection_meta ( snapshot_run . json ( ) )
expect ( meta . get ( " source " ) == " request " , " trace names the request snapshot " )
expect ( meta . get ( " remembered " ) is False , " unremembered snapshot is marked " )
expect ( meta . get ( " ids " ) == {
" transformation " : seed_id ( " transformation " , " correction " ) ,
" detail " : seed_id ( " detail " , " compact " ) ,
" voice " : seed_id ( " voice " , " neutral " ) ,
" narrative " : seed_id ( " narrative " , " chronicle " ) ,
} , " trace keeps the used ids " )
expect ( meta . get ( " keys " , { } ) . get ( " transformation " ) == " correction " , " trace keeps guideline keys " )
expect ( meta . get ( " revisions " , { } ) . get ( " transformation " ) == 1 , " trace keeps revisions " )
expect ( " editorial_mode " not in meta , " trace has no abandoned editorial mode " )
expect ( " source_mode " not in meta , " trace has no abandoned source mode " )
expect ( " Korrigiere nur " not in str ( meta ) , " trace does not store compiled instruction text " )
expect (
client . get ( " /api/journal/generation-settings " , headers = headers ) . json ( ) [ " selection " ] == default_ids ( ) ,
" unremembered snapshot does not change profile defaults " ,
)
snapshot = snapshot_run . json ( ) . get ( " generation_snapshot " ) or { }
expect ( snapshot . get ( " transformation " , { } ) . get ( " key " ) == " correction " , " draft stores transformation key " )
expect ( snapshot . get ( " prompt_slug " ) == " mvp.journal_generate " , " draft stores prompt slug " )
expect ( " instruction " not in str ( snapshot ) , " draft snapshot omits instruction text " )
expect ( " source_mode " not in snapshot , " draft snapshot has no source_mode " )
expect ( " editorial_mode " not in snapshot , " draft snapshot has no editorial_mode " )
summary = snapshot_run . json ( ) . get ( " generation_summary " ) or " "
expect ( summary . startswith ( " Erzeugt mit: " ) , " draft summary is user-readable " )
expect ( " Quellenmodus: " not in summary , " draft summary does not name a source mode " )
expect ( " {{ transformation_instructions}} " not in intern , " transformation placeholder is resolved " )
expect ( " {{ detail_instructions}} " not in intern , " detail placeholder is resolved " )
expect ( " {{ voice_instructions}} " not in intern , " voice placeholder is resolved " )
expect ( " {{ narrative_instructions}} " not in intern , " narrative placeholder is resolved " )
expect ( " {{ source_mode_instructions}} " not in intern , " source-mode placeholder is absent " )
expect ( MIXED_SOURCES_INSTRUCTION in intern , " mixed-source instruction reaches the prompt " )
expect ( seed_instruction ( " transformation " , " correction " ) in intern , " selected transformation reaches the prompt " )
expect ( seed_instruction ( " detail " , " compact " ) in intern , " selected detail reaches the prompt " )
expect ( seed_instruction ( " voice " , " neutral " ) in intern , " selected voice reaches the prompt " )
expect ( seed_instruction ( " narrative " , " chronicle " ) in intern , " selected narrative reaches the prompt " )
expect ( [ item . get ( " purpose " ) for item in recorder ] == [ " journal_generate " ] , " exactly one generate call " )
expect ( sum ( 1 for item in ( snapshot_run . json ( ) . get ( " run_log " ) or [ ] ) if item . get ( " kind " ) == " model_call " ) == 1 , " run log records one model call " )
expect (
not any ( " Korrigiere nur Rechtschreibung " in str ( item ) for item in ( snapshot_run . json ( ) . get ( " run_log " ) or [ ] ) ) ,
" run log omits compiled prompt instructions " ,
)
for fact in HARD_FACTS :
expect ( fact in intern , f " hard fact remains at low policy: { fact } " )
2026-08-29 11:04:34 +02:00
copyedit_ids = selection_of (
transformation = " copyedit " ,
detail = " complete " ,
voice = " clear " ,
narrative = " weighted " ,
)
reset_debug ( )
copyedit_recorder = install_test_recorder ( )
copyedit_run = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = {
" conversation_ids " : [ conv . json ( ) [ " id " ] ] ,
" generation_selection " : copyedit_ids ,
" remember_generation_selection " : False ,
} ,
)
expect ( copyedit_run . status_code == 200 , f " copyedit generate { copyedit_run . text } " )
copyedit_meta = selection_meta ( copyedit_run . json ( ) )
copyedit_intern = intern_of ( copyedit_run . json ( ) )
copyedit_snap = copyedit_run . json ( ) . get ( " generation_snapshot " ) or { }
expect ( copyedit_meta . get ( " ids " ) == {
" transformation " : seed_id ( " transformation " , " copyedit " ) ,
" detail " : seed_id ( " detail " , " complete " ) ,
" voice " : seed_id ( " voice " , " clear " ) ,
" narrative " : seed_id ( " narrative " , " weighted " ) ,
} , " copyedit request ids reach the trace " )
expect ( copyedit_meta . get ( " keys " , { } ) . get ( " transformation " ) == " copyedit " , " copyedit key reaches the trace " )
expect ( copyedit_meta . get ( " revisions " , { } ) . get ( " transformation " ) == 1 , " copyedit revision reaches the trace " )
expect ( copyedit_meta . get ( " revisions " , { } ) . get ( " narrative " ) == 1 , " weighted revision of the selected id reaches the trace " )
expect ( copyedit_snap . get ( " transformation " , { } ) . get ( " id " ) == seed_id ( " transformation " , " copyedit " ) , " snapshot stores the selected copyedit id " )
expect ( copyedit_snap . get ( " transformation " , { } ) . get ( " revision " ) == 1 , " snapshot stores the selected copyedit revision " )
expect ( copyedit_snap . get ( " narrative " , { } ) . get ( " id " ) == seed_id ( " narrative " , " weighted " ) , " snapshot stores the selected weighted id " )
expect ( seed_instruction ( " transformation " , " copyedit " ) in copyedit_intern , " selected copyedit instruction reaches the prompt " )
expect ( seed_instruction ( " narrative " , " weighted " ) in copyedit_intern , " selected weighted instruction reaches the prompt " )
expect ( seed_instruction ( " transformation " , " substantial " ) not in copyedit_intern , " unselected substantial instruction is absent " )
expect ( [ item . get ( " purpose " ) for item in copyedit_recorder ] == [ " journal_generate " ] , " copyedit still uses one generate call " )
preview = client . post (
" /api/admin/generation-instructions/journal_generate/preview " ,
headers = headers ,
json = { " generation_selection " : copyedit_ids } ,
)
expect ( preview . status_code == 200 , f " copyedit preview { preview . text } " )
expect ( preview . json ( ) [ " transformation_instructions " ] == seed_instruction ( " transformation " , " copyedit " ) , " preview compiles the same copyedit instruction " )
expect ( preview . json ( ) . get ( " selection " , { } ) . get ( " ids " , { } ) . get ( " transformation " ) == seed_id ( " transformation " , " copyedit " ) , " preview reports the same copyedit id " )
expect ( preview . json ( ) . get ( " selection " , { } ) . get ( " revisions " , { } ) . get ( " narrative " ) == 1 , " preview reports the same weighted revision " )
2026-08-28 08:40:28 +02:00
notes_day = client . post (
f " /api/journal/spaces/ { space . json ( ) [ ' id ' ] } /days " ,
headers = headers ,
json = { " calendar_date " : " 2026-08-28 " } ,
)
notes_conv = client . post (
f " /api/journal/days/ { notes_day . json ( ) [ ' day ' ] [ ' id ' ] } /conversations " ,
headers = headers ,
json = { " title " : " Notizen " } ,
)
client . post (
f " /api/journal/conversations/ { notes_conv . json ( ) [ ' id ' ] } /turn " ,
headers = headers ,
json = { " body " : " - Markt \n - Brot nicht geholt \n - vielleicht kürzer bleiben " } ,
)
notes_run = client . post (
f " /api/journal/days/ { notes_day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = { " conversation_ids " : [ notes_conv . json ( ) [ " id " ] ] , " generation_selection " : default_ids ( ) } ,
)
expect ( notes_run . status_code == 200 , f " notes generate { notes_run . text } " )
notes_intern = intern_of ( notes_run . json ( ) )
notes_snap = notes_run . json ( ) . get ( " generation_snapshot " ) or { }
expect ( " source_mode " not in notes_snap , " notes snapshot has no source mode " )
expect ( " editorial_mode " not in ( notes_run . json ( ) . get ( " trace " ) or { } ) , " notes trace has no editorial mode " )
expect ( MIXED_SOURCES_INSTRUCTION in notes_intern , " notes use the unified mixed-source instruction " )
expect ( " Markt " in sources_block ( notes_intern ) , " notes sources remain complete " )
def generate_shapes ( date : str , bodies : list [ str ] , label : str ) - > None :
shaped_day = client . post (
f " /api/journal/spaces/ { space . json ( ) [ ' id ' ] } /days " ,
headers = headers ,
json = { " calendar_date " : date } ,
)
shaped_conv = client . post (
f " /api/journal/days/ { shaped_day . json ( ) [ ' day ' ] [ ' id ' ] } /conversations " ,
headers = headers ,
json = { " title " : label } ,
)
for body in bodies :
client . post (
f " /api/journal/conversations/ { shaped_conv . json ( ) [ ' id ' ] } /turn " ,
headers = headers ,
json = { " body " : body } ,
)
reset_debug ( )
rec = install_test_recorder ( )
run = client . post (
f " /api/journal/days/ { shaped_day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = { " conversation_ids " : [ shaped_conv . json ( ) [ " id " ] ] , " generation_selection " : default_ids ( ) } ,
)
expect ( run . status_code == 200 , f " { label } generate { run . text } " )
shaped_intern = intern_of ( run . json ( ) )
shaped_sources = sources_block ( shaped_intern )
shaped_snap = run . json ( ) . get ( " generation_snapshot " ) or { }
expect ( MIXED_SOURCES_INSTRUCTION in shaped_intern , f " { label } has mixed-source instruction " )
expect ( " {{ source_mode_instructions}} " not in shaped_intern , f " { label } has no source-mode placeholder " )
expect ( " source_mode " not in shaped_snap , f " { label } snapshot has no source_mode " )
expect ( " editorial_mode " not in shaped_snap , f " { label } snapshot has no editorial_mode " )
expect ( " editorial_mode " not in ( run . json ( ) . get ( " trace " ) or { } ) , f " { label } trace has no editorial_mode " )
expect ( [ item . get ( " purpose " ) for item in rec ] == [ " journal_generate " ] , f " { label } is exactly one generate call " )
for body in bodies :
for line in body . splitlines ( ) :
token = line . lstrip ( " - " ) . strip ( )
if token :
expect ( token in shaped_sources , f " { label } keeps source { token !r} " )
generate_shapes ( " 2026-08-11 " , [ " Ich ging zum Markt. Es war voll. Vielleicht bleibe ich kürzer. " ] , " prose-only " )
generate_shapes ( " 2026-08-12 " , [ " - Kirschen \n - Brot nicht geholt \n - später Hafen " ] , " notes-only " )
generate_shapes (
" 2026-08-13 " ,
[ " Ich ging zum Markt. Es war voll. " , " - Kirschen \n - später Hafen " ] ,
" prose-then-notes " ,
)
generate_shapes (
" 2026-08-14 " ,
[ " - Kirschen \n - später Hafen " , " Ich ging zum Markt. Es war voll. " ] ,
" notes-then-prose " ,
)
generate_shapes (
" 2026-08-15 " ,
[ " Ich ging zum Markt. Es war voll. \n - Kirschen \n - später Hafen " ] ,
" mixed-in-message " ,
)
high_run = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = {
" conversation_ids " : [ conv . json ( ) [ " id " ] ] ,
" generation_selection " : high_ids ( ) ,
" remember_generation_selection " : True ,
} ,
)
expect ( high_run . status_code == 200 , f " high generate { high_run . text } " )
high_intern = intern_of ( high_run . json ( ) )
expect ( " Überarbeite den Text substanziell " in high_intern , " high transformation reaches the prompt " )
expect ( " Korrigiere nur Rechtschreibung " not in high_intern , " low and high prompt blocks differ " )
expect ( intern != high_intern , " low and high snapshots render different prompt blocks " )
expect ( selection_meta ( high_run . json ( ) ) . get ( " remembered " ) is True , " remembered snapshot is marked " )
remembered = client . get ( " /api/journal/generation-settings " , headers = headers ) . json ( )
expect ( remembered [ " selection " ] == high_ids ( ) , " remembered snapshot becomes the profile default " )
for fact in HARD_FACTS :
expect ( fact in high_intern , f " hard fact remains at high policy: { fact } " )
profile_run = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = { " conversation_ids " : [ conv . json ( ) [ " id " ] ] } ,
)
expect ( profile_run . status_code == 200 , f " profile generate { profile_run . text } " )
expect ( selection_meta ( profile_run . json ( ) ) . get ( " source " ) == " profile " , " omitted snapshot uses stored values " )
expect ( seed_instruction ( " narrative " , " emphasized " ) in intern_of ( profile_run . json ( ) ) , " stored high narrative is reused " )
confirm_identity ( profile_id , " Clarissa " , aliases = [ " Sushi " ] )
alias_day = client . post (
f " /api/journal/spaces/ { space . json ( ) [ ' id ' ] } /days " ,
headers = headers ,
json = { " calendar_date " : " 2026-08-21 " } ,
)
alias_conv = client . post (
f " /api/journal/days/ { alias_day . json ( ) [ ' day ' ] [ ' id ' ] } /conversations " ,
headers = headers ,
json = { " title " : " Alias " } ,
)
client . post (
f " /api/journal/conversations/ { alias_conv . json ( ) [ ' id ' ] } /turn " ,
headers = headers ,
json = { " body " : " Sushi kam ins Wohnzimmer. " } ,
)
def canonical_reply ( _messages , _policy ) :
return ChatResult (
content = " Wohnzimmer \n \n Clarissa kam ins Wohnzimmer. " ,
model = " fake " ,
usage = { } ,
context_compression = " disabled " ,
)
with patch ( " privacy_gateway.complete_model " , canonical_reply ) :
alias_out = client . post (
f " /api/journal/days/ { alias_day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = { " conversation_ids " : [ alias_conv . json ( ) [ " id " ] ] } ,
)
expect ( alias_out . status_code == 200 , f " confirmed alias generate { alias_out . text } " )
expect ( " Clarissa " in ( alias_out . json ( ) . get ( " body " ) or " " ) , " canonical spelling is kept after demask " )
expect ( ( alias_out . json ( ) . get ( " trace " ) or { } ) . get ( " model_text_accepted " ) is True , " alias demask is not rejected as unattested " )
remember_mapping ( profile_id , " Hanna " , " PERSON:99 " )
def invent ( _messages , _policy ) :
return ChatResult (
content = " Hanna am Hafen \n \n Ich war am Markt. " ,
model = " fake " ,
usage = { } ,
context_compression = " disabled " ,
)
with patch ( " privacy_gateway.complete_model " , invent ) :
blocked = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = { " conversation_ids " : [ conv . json ( ) [ " id " ] ] } ,
)
expect ( blocked . status_code == 409 , " unattested person is still rejected " )
expect ( ( blocked . json ( ) . get ( " detail " ) or { } ) . get ( " code " ) == " journal_generation_not_accepted " , " 409 provenance rule remains " )
created_user = client . post (
" /api/users " ,
headers = headers ,
json = { " email " : " policy-user@example.test " , " name " : " User " , " password " : " user-pass " , " role " : " user " } ,
)
expect ( created_user . status_code == 200 , f " create user { created_user . text } " )
user_login = client . post (
" /api/auth/login " ,
json = { " email " : " policy-user@example.test " , " password " : " user-pass " } ,
)
user_headers = header ( user_login . json ( ) [ " token " ] )
denied = client . get ( " /api/admin/generation-instructions/journal_generate " , headers = user_headers )
expect ( denied . status_code == 403 , " admin catalog is protected " )
denied_put = client . put (
" /api/admin/generation-instructions/journal_generate/ " + seed_id ( " transformation " , " substantial " ) ,
headers = user_headers ,
json = { " label " : " nein " } ,
)
expect ( denied_put . status_code == 403 , " admin catalog write is protected " )
catalog = client . get ( " /api/admin/generation-instructions/journal_generate " , headers = headers )
expect ( catalog . status_code == 200 , f " admin catalog { catalog . text } " )
expect ( " source_mode " not in catalog . json ( ) [ " slots " ] , " admin catalog has no source-mode slot " )
expect ( " instruction " not in str ( catalog . json ( ) [ " slots " ] [ " transformation " ] ) , " admin overview omits prompt bodies " )
cloned = client . post (
f " /api/admin/generation-instructions/journal_generate/ { seed_id ( ' transformation ' , ' substantial ' ) } /clone " ,
headers = headers ,
)
expect ( cloned . status_code == 200 , f " clone { cloned . text } " )
saved = client . put (
f " /api/admin/generation-instructions/journal_generate/ { cloned . json ( ) [ ' id ' ] } " ,
headers = headers ,
json = {
" guideline_key " : " substantial " ,
" label " : " substanziell " ,
" summary " : " Klon " ,
" instruction " : " ADMIN_CUSTOM_TRANSFORMATION_BLOCK " ,
} ,
)
expect ( saved . status_code == 200 , f " admin draft save { saved . text } " )
published = client . post (
f " /api/admin/generation-instructions/journal_generate/ { cloned . json ( ) [ ' id ' ] } /publish " ,
headers = headers ,
)
expect ( published . status_code == 200 , f " publish { published . text } " )
2026-08-29 11:04:34 +02:00
expect ( load_selection ( profile_id ) [ " transformation_id " ] == seed_id ( " transformation " , " substantial " ) , " publishing a clone does not rewrite the stored selection " )
2026-08-28 08:40:28 +02:00
immutable = client . put (
f " /api/admin/generation-instructions/journal_generate/ { seed_id ( ' transformation ' , ' substantial ' ) } " ,
headers = headers ,
json = { " label " : " überschreiben " , " instruction " : " should not stick " , " guideline_key " : " substantial " } ,
)
expect ( immutable . status_code == 409 , " active guidelines cannot be overwritten " )
preview = client . post (
" /api/admin/generation-instructions/journal_generate/preview " ,
headers = headers ,
json = {
" generation_selection " : { * * high_ids ( ) , " transformation_id " : cloned . json ( ) [ " id " ] } ,
} ,
)
expect ( preview . status_code == 200 , f " admin preview { preview . text } " )
expect ( preview . json ( ) [ " transformation_instructions " ] == " ADMIN_CUSTOM_TRANSFORMATION_BLOCK " , " preview compiles locally " )
expect ( " (nicht enthalten) " in ( preview . json ( ) . get ( " rendered " ) or " " ) , " preview has no personal sources " )
expect ( " {{ transformation_instructions}} " not in ( preview . json ( ) . get ( " rendered " ) or " " ) , " preview resolves placeholders " )
custom_run = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = {
" conversation_ids " : [ conv . json ( ) [ " id " ] ] ,
" generation_selection " : { * * high_ids ( ) , " transformation_id " : cloned . json ( ) [ " id " ] } ,
" remember_generation_selection " : False ,
} ,
)
expect ( custom_run . status_code == 200 , f " custom generate { custom_run . text } " )
expect ( " ADMIN_CUSTOM_TRANSFORMATION_BLOCK " in intern_of ( custom_run . json ( ) ) , " admin change reaches the next rendered prompt " )
init_db ( )
after_seed = overview_payload ( )
substantial = next (
item for item in after_seed [ " slots " ] [ " transformation " ] if item [ " id " ] == seed_id ( " transformation " , " substantial " )
)
expect ( get_guideline ( substantial [ " id " ] , include_instruction = True ) [ " instruction " ] == seed_instruction ( " transformation " , " substantial " ) , " seed does not overwrite the original " )
expect ( get_guideline ( cloned . json ( ) [ " id " ] , include_instruction = True ) [ " instruction " ] == " ADMIN_CUSTOM_TRANSFORMATION_BLOCK " , " seed does not overwrite admin clones " )
restored = client . post (
" /api/admin/generation-instructions/journal_generate/reset " ,
headers = headers ,
)
expect ( restored . status_code == 200 , f " reset { restored . text } " )
drafts = [ item for item in restored . json ( ) [ " slots " ] [ " transformation " ] if item [ " status " ] == " draft " ]
expect ( drafts , " reset adds seed drafts " )
expect (
get_guideline ( cloned . json ( ) [ " id " ] , include_instruction = True ) [ " instruction " ] == " ADMIN_CUSTOM_TRANSFORMATION_BLOCK " ,
" reset does not overwrite historical variants " ,
)
archived = client . post (
f " /api/admin/generation-instructions/journal_generate/ { cloned . json ( ) [ ' id ' ] } /archive " ,
headers = headers ,
)
expect ( archived . status_code == 200 , f " archive { archived . text } " )
archived_run = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = {
" conversation_ids " : [ conv . json ( ) [ " id " ] ] ,
" generation_selection " : { * * high_ids ( ) , " transformation_id " : cloned . json ( ) [ " id " ] } ,
} ,
)
expect ( archived_run . status_code == 400 , " archived guidelines are rejected for new runs " )
fresh_day = client . post (
f " /api/journal/spaces/ { space . json ( ) [ ' id ' ] } /days " ,
headers = headers ,
json = { " calendar_date " : " 2026-08-16 " } ,
)
fresh_conv = client . post (
f " /api/journal/days/ { fresh_day . json ( ) [ ' day ' ] [ ' id ' ] } /conversations " ,
headers = headers ,
json = { " title " : " Legacy " } ,
)
client . post (
f " /api/journal/conversations/ { fresh_conv . json ( ) [ ' id ' ] } /turn " ,
headers = headers ,
json = { " body " : " Ich war am Markt. " } ,
)
with get_db ( ) as conn :
current_template = conn . execute (
" SELECT template FROM ai_prompts WHERE slug = ? " ,
( " mvp.journal_generate " , ) ,
) . fetchone ( ) [ " template " ]
conn . execute (
" UPDATE ai_prompts SET template = ? WHERE slug = ? " ,
( ( current_template or " " ) + " \n {{ source_mode_instructions}} \n " , " mvp.journal_generate " ) ,
)
reset_debug ( )
legacy_recorder = install_test_recorder ( )
legacy = client . post (
f " /api/journal/days/ { fresh_day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = { " conversation_ids " : [ fresh_conv . json ( ) [ " id " ] ] , " generation_selection " : default_ids ( ) } ,
)
expect ( legacy . status_code == 409 , " legacy custom prompt is refused before the provider " )
legacy_detail = legacy . json ( ) . get ( " detail " ) or { }
expect ( legacy_detail . get ( " code " ) == " prompt_contract_incompatible " , " legacy prompt uses prompt_contract_incompatible " )
expect ( " veralteten Platzhalter " in ( legacy_detail . get ( " message " ) or " " ) , " admin hint names the retired placeholder " )
expect ( legacy_recorder == [ ] , " legacy custom prompt does not call the provider " )
expect ( current_draft ( profile_id , fresh_day . json ( ) [ " day " ] [ " id " ] ) is None , " legacy custom prompt inserts no raw draft " )
with get_db ( ) as conn :
conn . execute (
" UPDATE ai_prompts SET template = default_template WHERE slug = ? " ,
( " mvp.journal_generate " , ) ,
)
with get_db ( ) as conn :
conn . execute (
"""
UPDATE generation_guidelines
SET instruction = ' '
WHERE id = ?
""" ,
( seed_id ( " transformation " , " substantial " ) , ) ,
)
reset_debug ( )
broken_recorder = install_test_recorder ( )
broken = client . post (
f " /api/journal/days/ { day . json ( ) [ ' day ' ] [ ' id ' ] } /generate " ,
headers = headers ,
json = { " conversation_ids " : [ conv . json ( ) [ " id " ] ] , " generation_selection " : default_ids ( ) } ,
)
expect ( broken . status_code == 409 , " invalid catalog refuses generate " )
expect ( ( broken . json ( ) . get ( " detail " ) or { } ) . get ( " code " ) == " generation_policy_invalid " , " invalid catalog uses generation_policy_invalid " )
expect ( broken_recorder == [ ] , " invalid catalog does not call the provider " )
with get_db ( ) as conn :
conn . execute (
"""
UPDATE generation_guidelines
SET instruction = ?
WHERE id = ?
""" ,
( seed_instruction ( " transformation " , " substantial " ) , seed_id ( " transformation " , " substantial " ) ) ,
)
with get_db ( ) as conn :
conn . execute (
" UPDATE ai_prompts SET template = ' CUSTOM POLICY PROMPT {{ reconstruction}} ' WHERE slug = ? " ,
( " mvp.journal_generate " , ) ,
)
init_db ( )
custom = load_active_prompt ( " mvp.journal_generate " )
expect ( custom [ " template " ] == " CUSTOM POLICY PROMPT {{ reconstruction}} " , " independently edited prompt is not overwritten " )
with get_db ( ) as conn :
row = conn . execute (
" SELECT default_template, seed_revision FROM ai_prompts WHERE slug = ? " ,
( " mvp.journal_generate " , ) ,
) . fetchone ( )
expect ( " {{ transformation_instructions}} " in ( row [ " default_template " ] or " " ) , " default template still tracks mixed sources " )
expect ( MIXED_SOURCES_INSTRUCTION in ( row [ " default_template " ] or " " ) , " default template has mixed-source instruction " )
2026-08-29 20:28:59 +02:00
expect ( row [ " seed_revision " ] == " 2026-08-29-voice-legacy-immutable-v1 " , " revision updates even when template is custom " )
2026-08-28 08:40:28 +02:00
readable = snapshot_summary (
{
" transformation " : { " label " : " spürbar " } ,
" detail " : { " label " : " weitgehend " } ,
" voice " : { " label " : " deutlich " } ,
" narrative " : { " label " : " gewichtet " } ,
}
)
expect ( " Erzeugt mit: \n Spürbar · Weitgehend · Deutlich · Gewichtet " in readable , " snapshot summary is readable " )
expect ( " Quellenmodus " not in readable , " snapshot summary does not name a source mode " )
print ( " journal generation policy tests passed. " )
if __name__ == " __main__ " :
main ( )