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>
35 lines
1.3 KiB
Python
35 lines
1.3 KiB
Python
"""R04-T05 — unit tests for the unattended-run prompt assembly.
|
|
|
|
``_run_agent`` used to build this by rebinding ``prompt`` three times, each with
|
|
its own ``f"{block}\n\n{prompt}"``. The ORDER that produced is load-bearing (the
|
|
plan reminder has to lead, the task's own words have to trail) and it was
|
|
readable only by replaying the rebindings in your head.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from cowork_local.core.task_executors import _unattended_prompt
|
|
|
|
|
|
def test_the_plan_reminder_leads_and_the_task_prompt_trails() -> None:
|
|
built = _unattended_prompt("write the report")
|
|
|
|
assert built.startswith("This runs unattended (Schedule Task)")
|
|
assert built.endswith("write the report")
|
|
|
|
|
|
def test_a_skill_block_sits_between_the_reminder_and_the_agent_persona() -> None:
|
|
built = _unattended_prompt("write the report", skill_text="SKILL",
|
|
agent_instructions="PERSONA")
|
|
|
|
assert built.index("This runs unattended") < built.index("SKILL")
|
|
assert built.index("SKILL") < built.index("PERSONA")
|
|
assert built.index("PERSONA") < built.index("write the report")
|
|
|
|
|
|
def test_absent_blocks_leave_no_extra_blank_lines() -> None:
|
|
built = _unattended_prompt("do it", skill_text="", agent_instructions=None)
|
|
|
|
assert "\n\n\n" not in built
|
|
assert built.count("do it") == 1
|