R04-T03 — the turn lifecycle, extracted from `core/chat_agent.py::run_cowork` into `application/conversations/`. The 260-line body mixed the lifecycle (step budget, cancel checks, guard -> preview -> gate -> execute ordering, sandbox tidy-up) with the machinery doing each step, and reaching any of it meant standing up a Qt widget and a worker thread. It is now a plain object driven through two Protocols and six callables (`turn_runtime.py`), with the concrete `core/*` wiring confined to `core_runtime_adapter.py` — the same shape R03 used for routing. Faithful port, not an improvement pass: where the original had a quirk (the step-ceiling note only merges into the answer when the last message is the assistant's) the quirk is preserved and commented. R04-T04 — `ui/cowork_tab.py::build_job` no longer calls run_cowork. It captures the widget's state at submit time, builds the request via the new `cowork_turn_request.py` and executes it. `execute(..., messages=...)` hands the widget's own list over because `_reattach_running_turn` replays from it WHILE the worker appends and `_finalize_turn` slices it afterwards — a private list would break both silently. R04-T05 — `core/task_executors.py`'s cowork branch shares the same engine. All five unattended-run behaviours stay put (plan reminder, history_ready, History autosave per assistant message, timeout notice, plan_incomplete_reason), and `_unattended_prompt` now expresses the load-bearing prefix order in one readable call instead of three successive rebindings. Verification: 74 new tests (364 passed, 1 skipped overall; check_imports PASS). The two that matter most: - `test_conversation_service_parity.py` runs the same scripted turn through run_cowork AND the service and compares the event stream, the resulting conversation and the advertised tool list across 7 scenarios; - `test_task_executor_turn.py` was written BEFORE the migration and passed 8/8 against the old code, then unchanged against the new. Known: `ui/cowork_tab.py` (416 -> 455) and `core/task_executors.py` (476 -> 524) stay above the 400-LOC limit. Both were already over it before this change; bringing them under needs the R08 / R07 decompositions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
"""R04-T04 — unit tests for the UI-state -> request mapping.
|
|
|
|
Three small rules used to sit inline in ``ui/cowork_tab.py::build_job``, where no
|
|
test could reach them: the turn's prompt is the last message in the working list,
|
|
the history is everything before it, and the confirm-commands flag becomes a gate
|
|
mode. Getting any of them wrong is silent (a duplicated user message, a command
|
|
that stops asking for approval), so they are pinned here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from cowork_local.application.conversations.cowork_turn_request import (
|
|
build_cowork_turn_request,
|
|
)
|
|
|
|
|
|
def _build(**overrides):
|
|
base = {
|
|
"turn_id": "t3",
|
|
"session_id": "s1",
|
|
"messages": [{"role": "user", "content": "make me a report"}],
|
|
}
|
|
base.update(overrides)
|
|
return build_cowork_turn_request(**base)
|
|
|
|
|
|
def test_the_last_message_becomes_the_prompt_and_the_rest_the_history() -> None:
|
|
request = _build(messages=[
|
|
{"role": "user", "content": "earlier"},
|
|
{"role": "assistant", "content": "sure"},
|
|
{"role": "user", "content": "now this"},
|
|
])
|
|
|
|
assert request.prompt == "now this"
|
|
assert request.messages == ({"role": "user", "content": "earlier"},
|
|
{"role": "assistant", "content": "sure"})
|
|
|
|
|
|
def test_an_empty_working_list_yields_an_empty_prompt() -> None:
|
|
# Defensive: a turn with no message at all must not raise on messages[-1].
|
|
request = _build(messages=[])
|
|
|
|
assert request.prompt == ""
|
|
assert request.messages == ()
|
|
|
|
|
|
def test_confirming_commands_puts_the_turn_in_confirm_gate_mode() -> None:
|
|
assert _build(confirm_commands=True).gate_mode == "confirm"
|
|
assert _build(confirm_commands=False).gate_mode == "auto"
|
|
assert _build().gate_mode == "auto" # auto-run is the default
|
|
|
|
|
|
def test_the_captured_widget_state_is_carried_into_the_request() -> None:
|
|
request = _build(
|
|
surface="cowork", project_id="p7", title="Weekly report",
|
|
provider_id="anthropic", model="claude-sonnet-4-6",
|
|
instructions="PROJECT RULES", output_dir="out/.turns/t3",
|
|
home_output_root="out", agent_role="cowork",
|
|
)
|
|
|
|
assert (request.turn_id, request.session_id) == ("t3", "s1")
|
|
assert (request.surface, request.project_id, request.title) == \
|
|
("cowork", "p7", "Weekly report")
|
|
assert (request.provider_id, request.model) == ("anthropic", "claude-sonnet-4-6")
|
|
assert request.project_context == "PROJECT RULES"
|
|
assert request.output_dir == Path("out/.turns/t3")
|
|
assert request.home_output_root == Path("out")
|
|
assert request.agent_role == "cowork"
|
|
|
|
|
|
def test_the_prompt_survives_a_message_whose_content_is_missing() -> None:
|
|
request = _build(messages=[{"role": "user"}])
|
|
|
|
assert request.prompt == ""
|