585 lines
29 KiB
Python
585 lines
29 KiB
Python
"""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")
|
|
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")
|
|
|
|
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")
|
|
last_long = long_turn.json().get("trace") or {}
|
|
|
|
expect(last_long.get("purpose") == "dialogue_turn", "long turn still has no review call")
|
|
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"]
|
|
expect(package["kind"] == KIND_PACKAGE, "paste uses analysis package contract")
|
|
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")
|
|
instruction = (package.get("instruction") or "") + (paste.json().get("prompt") or "")
|
|
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")
|
|
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")
|
|
|
|
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",
|
|
"action": "add",
|
|
"label": "Trockener Humor",
|
|
"proposed_value": "gelegentlich trocken, aus Paste-Review",
|
|
"rationale": "Journal-Evidenz, kein globaler Core.",
|
|
"evidence_basis": ["kansho_sources"],
|
|
"evidence_ids": [evidence_id] if evidence_id else [],
|
|
"exemplars": [{"excerpt": "haha das war irgendwie lustig", "occurred_at": "2024-08-11", "evidence_basis": ["kansho_sources"]}],
|
|
}
|
|
],
|
|
},
|
|
},
|
|
)
|
|
expect(imported.status_code == 200, f"import result {imported.text}")
|
|
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"]
|
|
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")
|
|
|
|
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",
|
|
)
|
|
|
|
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}")
|
|
api_trace = api_review.json().get("trace") or {}
|
|
|
|
expect(api_trace.get("purpose") == "profile_review", "api review uses dedicated purpose")
|
|
expect(api_trace.get("layer") == "profilreview", "trace is not a dialogue turn")
|
|
expect(api_review.json()["applied"] == 0, "api result is a proposal, not an auto-apply")
|
|
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")
|
|
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")
|
|
|
|
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()
|