mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
fix(chat): the pointer row is complete, live wait facts win, one status word leads
Roast findings (codex gpt-6-astra) folded in: - The pointer row carries the question, option labels, recorded answer and the wait facts from history, the live delivery and the activity census (`project_question_pointer`, `owner_wait_projection`; both contract mirrors), so Main paints it from the row alone. The IntersectionObserver hydration, the settled-source cache and the Retry state are gone: freshness is the ordinary history reconciliation plus the `quiz_state` frame, and task detail is read only to open the original form. - Lifecycle observation precedence: once a live frame closed a wait, an older history row or a detail read begun before it cannot reopen «Waiting»; an unavailable row keeps what is known; a settled question never reopens. The production timeout frame (`wait_for_answer:false`, no `wait_ended_at`) is the shape the tests use. - Parity: history attaches the task's wait record to the Project room's quiz rows (a wait the owner resumed by ordinary input leaves no frame behind), the quiz card reads those facts, and the parity fixture now pins the rows Python emits against what the browser reads from them. - Wording leads with one word — «Waiting for your answer» / «Unanswered · …» / «You answered» / «Replaced by a newer question» / «Status unavailable» — with three action labels; status and source lines use meta ink (DESIGN: the owner reads them to act). The wait-ended line names the default path the task took and that silence was not consent; a late answer's toast says where it went. - Previews bound the option and the comment separately and never cut for less than the marker costs. - Module map row for `question_presentation.js`; DEVELOPMENT 11 points at the ARCHITECTURE data flow instead of restating it; a stale comment in `owner_quiz.py`; `contracts.py` pays its 1600-line gate down by compacting five comment blocks.
This commit is contained in:
parent
1fdfc722b5
commit
eda1181f46
20 changed files with 611 additions and 340 deletions
|
|
@ -41,7 +41,10 @@ def test_real_escalation_bridge_history_detail_and_answer(tmp_path, monkeypatch)
|
|||
assert [row["type"] for row in frames] == ["quiz", "chat"]
|
||||
pointer = frames[1]
|
||||
assert pointer["system_type"] == "project_question_pointer" and pointer["chat_id"] == 1
|
||||
assert not {"options", "question", "comment", "answered_index"} & pointer.keys()
|
||||
# The live pointer frame is complete for display: the browser paints it without a detail read.
|
||||
assert pointer["question"] == "Which storage?" and pointer["options"] == ["Local", "Shared"]
|
||||
assert pointer["wait_for_answer"] is True and pointer["content"] == "Waiting for your answer in Question Project"
|
||||
assert not {"comment", "answered_index"} & pointer.keys()
|
||||
stored = (tmp_path / "logs/chat.jsonl").read_text().splitlines()
|
||||
assert len(stored) == 1 and json.loads(stored[0])["chat_id"] == project["chat_id"]
|
||||
main = json.loads(_assemble_history_response(tmp_path, 1, 10, 0))["messages"]
|
||||
|
|
@ -58,7 +61,9 @@ def test_real_escalation_bridge_history_detail_and_answer(tmp_path, monkeypatch)
|
|||
{"request_id": "answer-1", "decision_id": f"quiz:task-1:{qid}", "option_index": 1})
|
||||
assert answered.status_code == 200, answered.text
|
||||
main = json.loads(_assemble_history_response(tmp_path, 1, 10, 0))["messages"]
|
||||
assert main[0]["quiz_state"] == "answered"
|
||||
assert main[0]["quiz_state"] == "answered" and main[0]["answered_index"] == 1
|
||||
assert main[0]["question"] == "Which storage?" and main[0]["options"] == ["Local", "Shared"]
|
||||
assert main[0]["text"] == "You answered in Question Project"
|
||||
assert reconcile_terminal(tmp_path, "task-1") == []
|
||||
assert quiz_states(tmp_path, "task-1")[qid]["option_details"][1] == "Multiple writers over network"
|
||||
|
||||
|
|
@ -100,12 +105,13 @@ def test_activity_question_uses_same_memo_and_preserves_wait_semantics(tmp_path,
|
|||
monkeypatch.setattr(utils, "read_json_dict", lambda path: (reads.append(str(path)), real(path))[1])
|
||||
rows = gs._chat_activities_snapshot_safe(tmp_path, direct_turns=[])
|
||||
assert rows[0]["required_question"]["quiz_state"] == "open"
|
||||
assert rows[0]["required_question"]["text"] == "Waiting for your answer in Waiting Project"
|
||||
assert reads.count(str(tmp_path / "task_results/t1.json")) == 1
|
||||
gs._chat_activities_snapshot_safe(tmp_path, direct_turns=[])
|
||||
assert reads.count(str(tmp_path / "task_results/t1.json")) == 1
|
||||
set_owner_wait(tmp_path, "t1", {"quiz_id": "q1", "wait_id": "w1", "state": "resumed"}, "w1")
|
||||
rows = gs._chat_activities_snapshot_safe(tmp_path, direct_turns=[])
|
||||
assert rows[0]["required_question"]["text"] == "Task continued — you can still answer in Waiting Project"
|
||||
assert rows[0]["required_question"]["text"] == "Unanswered · the task continued; an answer is still accepted in Waiting Project"
|
||||
assert quiz_states(tmp_path, "t1")["q1"]["state"] == "open"
|
||||
# A wait that ended on its OWN bound resumed without an answer, so the
|
||||
# question is still wanted: no new wait state, just the additive reason.
|
||||
|
|
@ -114,7 +120,7 @@ def test_activity_question_uses_same_memo_and_preserves_wait_semantics(tmp_path,
|
|||
gs._FINALIZING_MEMO.clear()
|
||||
rows = gs._chat_activities_snapshot_safe(tmp_path, direct_turns=[])
|
||||
pointer = rows[0]["required_question"]
|
||||
assert pointer["text"] == "Task continued — you can still answer in Waiting Project"
|
||||
assert pointer["text"] == "Unanswered · the task continued; an answer is still accepted in Waiting Project"
|
||||
assert pointer["owner_wait_resume_reason"] == "timeout"
|
||||
|
||||
|
||||
|
|
@ -131,15 +137,47 @@ def test_details_are_immutable_optional_and_length_checked(tmp_path):
|
|||
|
||||
|
||||
def test_question_presentation_shared_fixture():
|
||||
"""Both sides of the parity fixture: Python emits exactly the fixture row for each case, and the
|
||||
browser (web/tests/question_presentation.test.js) reads the fixture status out of that row."""
|
||||
from pathlib import Path
|
||||
from ouroboros.project_dialogue import project_question_pointer
|
||||
|
||||
cases = json.loads((Path(__file__).resolve().parents[1] /
|
||||
'web/tests/fixtures/question_presentation_parity.json').read_text())
|
||||
for row in cases:
|
||||
keys = ("quiz_state", "owner_wait_state", "owner_wait_resume_reason", "wait_for_answer",
|
||||
"wait_ended_at", "answered_index", "comment", "source_status")
|
||||
for case in cases:
|
||||
block = {"quiz_id": "q", **case["block"]} if case["block"] is not None else None
|
||||
wait = {"quiz_id": "q", **case["owner_wait"]} if case["owner_wait"] is not None else None
|
||||
pointer = project_question_pointer(
|
||||
{'task_id': 'task', 'quiz': {'quiz_id': 'q', 'wait_for_answer': True}},
|
||||
{'quiz_id': 'q', **row}, {'id': 'p', 'chat_id': 12, 'name': 'Project'},
|
||||
{'quiz_id': 'q', 'state': row.get('owner_wait_state', ''),
|
||||
'resume_reason': row.get('owner_wait_resume_reason', '')})
|
||||
assert pointer['text'] == row['status'] + ' in Project'
|
||||
{"task_id": "task", "text": "Which?", "quiz": {"quiz_id": "q", "wait_for_answer": True, "options": ["a", "b"]}},
|
||||
block, {"id": "p", "chat_id": 12, "name": "Project"}, wait)
|
||||
assert pointer["text"] == case["status"] + " in Project", case["case"]
|
||||
assert {key: pointer[key] for key in keys if key in pointer} == case["row"], case["case"]
|
||||
assert pointer["question"] == "Which?" and pointer["options"] == ["a", "b"]
|
||||
|
||||
|
||||
def test_project_room_quiz_row_carries_the_wait_facts(tmp_path):
|
||||
"""A wait the owner resumed by ordinary input leaves no frame behind: the replayed card
|
||||
reads the task's wait record like the Main pointer does."""
|
||||
from ouroboros.utils import append_jsonl
|
||||
|
||||
project = create_project(tmp_path, "resumed-project", name="Resumed Project")
|
||||
write_task_result(tmp_path, "t1", STATUS_RUNNING, project_id=project["id"], chat_id=project["chat_id"])
|
||||
record_asked(tmp_path, "t1", quiz_id="q1", question="Which?", options=["a", "b"], wait_for_answer=True)
|
||||
append_jsonl(tmp_path / "logs/chat.jsonl", {"type": "quiz", "direction": "out", "chat_id": project["chat_id"],
|
||||
"task_id": "t1", "ts": "2026-09-16T00:00:00Z", "text": "Which?",
|
||||
"quiz": {"quiz_id": "q1", "options": ["a", "b"], "wait_for_answer": True}})
|
||||
room = json.loads(_assemble_history_response(tmp_path, project["chat_id"], 10, 0))["messages"]
|
||||
assert "owner_wait_state" not in room[0]["quiz"]
|
||||
set_owner_wait(tmp_path, "t1", {"quiz_id": "q1", "wait_id": "w1", "state": "waiting"})
|
||||
room = json.loads(_assemble_history_response(tmp_path, project["chat_id"], 10, 0))["messages"]
|
||||
assert room[0]["quiz"]["owner_wait_state"] == "waiting"
|
||||
set_owner_wait(tmp_path, "t1", {"quiz_id": "q1", "wait_id": "w1", "state": "resumed"})
|
||||
room = json.loads(_assemble_history_response(tmp_path, project["chat_id"], 10, 0))["messages"]
|
||||
assert room[0]["quiz"]["owner_wait_state"] == "resumed" and room[0]["quiz"]["wait_for_answer"] is True
|
||||
main = json.loads(_assemble_history_response(tmp_path, 1, 10, 0))["messages"]
|
||||
assert main[0]["text"] == "Unanswered · the task continued; an answer is still accepted in Resumed Project"
|
||||
set_owner_wait(tmp_path, "t1", {"quiz_id": "q2", "wait_id": "w2", "state": "waiting"})
|
||||
room = json.loads(_assemble_history_response(tmp_path, project["chat_id"], 10, 0))["messages"]
|
||||
assert room[0]["quiz"]["owner_wait_state"] == "resumed"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue