feat: first-meeting gate on drawer Add-guest form (T72.2)
When a host->candidate edge already exists from a prior chat, the Add-guest form renders the prose textarea disabled with an "already know each other" note. Submission without the explicit "re-seed anyway" toggle skips seed_inter_bot_edges so existing edge content (affinity, trust, knowledge, summaries) survives — guest_added and group_node_initialized still fire. A small inline script enables / disables the textarea per-option based on a pre-computed existing_guest_edges dict surfaced by the GET handler.
This commit is contained in:
@@ -265,6 +265,162 @@ def test_drawer_remove_guest_clears_and_closes_scene(client, tmp_path):
|
||||
assert kinds.index("scene_closed") < kinds.index("guest_removed")
|
||||
|
||||
|
||||
# --- T72.2 first-meeting gate ----------------------------------------------
|
||||
|
||||
|
||||
def _seed_host_to_guest_edge(db: Path) -> None:
|
||||
"""Materialise a bot_a -> bot_b edge so the gate's check fires."""
|
||||
from chat.eventlog.log import append_and_apply
|
||||
|
||||
with open_db(db) as conn:
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="edge_update",
|
||||
payload={
|
||||
"source_id": "bot_a",
|
||||
"target_id": "bot_b",
|
||||
"chat_id": "chat_bot_a",
|
||||
"affinity_delta": 0,
|
||||
"knowledge_facts": ["already met before"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_add_guest_form_disables_prose_when_edge_exists(client, tmp_path):
|
||||
"""When host->candidate edge already exists, the GET partial renders
|
||||
the textarea disabled and surfaces the "already know each other"
|
||||
message so the user knows submitting will skip the seed.
|
||||
"""
|
||||
_seed_chat(tmp_path / "test.db")
|
||||
_seed_host_to_guest_edge(tmp_path / "test.db")
|
||||
|
||||
response = client.get("/chats/chat_bot_a/drawer")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
# Note + disabled state both present. The textarea sits next to the
|
||||
# ``add-guest-prose`` class so we can match it specifically.
|
||||
assert "already know each other" in body
|
||||
assert 'class="add-guest-prose"' in body
|
||||
# The textarea for the first (auto-selected) candidate should be
|
||||
# disabled in the initial markup since an edge exists.
|
||||
assert "disabled" in body.split('class="add-guest-prose"', 1)[1].split(">", 1)[0]
|
||||
# And the option carries the ``data-existing-edge="true"`` attribute
|
||||
# the inline JS uses to flip state on subsequent select changes.
|
||||
assert 'data-existing-edge="true"' in body
|
||||
|
||||
|
||||
def test_add_guest_with_existing_edge_skips_seed_call(client, tmp_path):
|
||||
"""Submitting the Add-guest form WITHOUT toggling re-seed must skip
|
||||
``seed_inter_bot_edges`` entirely. We assert this via an empty mock
|
||||
queue: if the seed function had been called it would have consumed
|
||||
a canned response (or raised because none was available).
|
||||
"""
|
||||
_seed_chat(tmp_path / "test.db")
|
||||
_seed_host_to_guest_edge(tmp_path / "test.db")
|
||||
|
||||
# Empty queue: any classifier call would raise inside MockLLMClient.
|
||||
canned_queue: list[str] = []
|
||||
_override_llm(canned_queue)
|
||||
try:
|
||||
response = client.post(
|
||||
"/chats/chat_bot_a/drawer/guest/add",
|
||||
data={
|
||||
"guest_bot_id": "bot_b",
|
||||
"relationship_prose": "ignored prose",
|
||||
# NO reseed flag — gate should suppress the seed call.
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
from chat.state.edges import get_edge
|
||||
from chat.state.world import get_chat
|
||||
|
||||
chat = get_chat(conn, "chat_bot_a")
|
||||
assert chat["guest_bot_id"] == "bot_b"
|
||||
|
||||
# The pre-seeded knowledge fact survives — proof the seed didn't run
|
||||
# and overwrite the existing edge.
|
||||
edge = get_edge(conn, "bot_a", "bot_b")
|
||||
assert "already met before" in edge["knowledge"]
|
||||
|
||||
# Exactly one guest_added; no new edge_update events between
|
||||
# bot_a and bot_b (the pre-seed edge_update from the test setup
|
||||
# is the only edge_update on this pair).
|
||||
added = conn.execute(
|
||||
"SELECT COUNT(*) FROM event_log WHERE kind = 'guest_added'"
|
||||
).fetchone()[0]
|
||||
assert added == 1
|
||||
|
||||
edge_updates = conn.execute(
|
||||
"SELECT payload_json FROM event_log WHERE kind = 'edge_update'"
|
||||
).fetchall()
|
||||
# Only the pre-seed edge_update from _seed_host_to_guest_edge.
|
||||
ab_updates = [
|
||||
json.loads(p[0])
|
||||
for p in edge_updates
|
||||
if {
|
||||
json.loads(p[0]).get("source_id"),
|
||||
json.loads(p[0]).get("target_id"),
|
||||
}
|
||||
== {"bot_a", "bot_b"}
|
||||
]
|
||||
assert len(ab_updates) == 1
|
||||
assert ab_updates[0]["knowledge_facts"] == ["already met before"]
|
||||
|
||||
|
||||
def test_add_guest_with_existing_edge_and_reseed_runs_seed(client, tmp_path):
|
||||
"""Toggling ``re-seed anyway`` flips the gate off — the existing
|
||||
flow runs (seed produces deltas, two ``edge_update`` events fire).
|
||||
"""
|
||||
_seed_chat(tmp_path / "test.db")
|
||||
_seed_host_to_guest_edge(tmp_path / "test.db")
|
||||
|
||||
canned = json.dumps(
|
||||
{
|
||||
"a_to_b_summary": "reconnected",
|
||||
"a_to_b_knowledge_facts": ["new fact"],
|
||||
"a_to_b_affinity_delta": 2,
|
||||
"a_to_b_trust_delta": 1,
|
||||
"b_to_a_summary": "reconnected",
|
||||
"b_to_a_knowledge_facts": [],
|
||||
"b_to_a_affinity_delta": 1,
|
||||
"b_to_a_trust_delta": 0,
|
||||
}
|
||||
)
|
||||
_override_llm([canned])
|
||||
try:
|
||||
response = client.post(
|
||||
"/chats/chat_bot_a/drawer/guest/add",
|
||||
data={
|
||||
"guest_bot_id": "bot_b",
|
||||
"relationship_prose": "fresh prose",
|
||||
"reseed": "1",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
edge_updates = conn.execute(
|
||||
"SELECT payload_json FROM event_log WHERE kind = 'edge_update'"
|
||||
).fetchall()
|
||||
# Pre-seed (1) + two from the re-seed = 3 edge_updates total.
|
||||
ab_updates = [
|
||||
json.loads(p[0])
|
||||
for p in edge_updates
|
||||
if {
|
||||
json.loads(p[0]).get("source_id"),
|
||||
json.loads(p[0]).get("target_id"),
|
||||
}
|
||||
== {"bot_a", "bot_b"}
|
||||
]
|
||||
assert len(ab_updates) == 3
|
||||
|
||||
|
||||
def test_drawer_with_guest_renders_guest_and_group_sections(client, tmp_path):
|
||||
_seed_chat(tmp_path / "test.db")
|
||||
from chat.eventlog.log import append_and_apply
|
||||
|
||||
Reference in New Issue
Block a user