Files
cowork-local/tests/unit/test_conversation_execution_request.py
T
f9f6bc01fd
CI / test (push) Canceled after 0s
Feature/delta team/epic r04 (#7)
## Summary

epic r04 - begin refactor

## Change Type

- [x] Cowork feature
- [ ] Bug fix
- [ ] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

---------

Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com>
Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com>
Co-authored-by: Vu Dam Tuan <vudt15@fpt.com>
Co-authored-by: Hiep Ha Van <hiephv3@fpt.com>
Co-authored-by: Lam Hoang Van <lamhv7@fpt.com>
Reviewed-on: #7
Co-authored-by: Duy Le Huu <duylh19@fpt.com>
2026-08-31 05:15:13 +00:00

133 lines
4.8 KiB
Python

"""R04-T01 — unit tests for the immutable turn snapshot.
The snapshot exists so a turn already running cannot be altered by the UI the
user keeps clicking on. These tests pin exactly that: the object refuses
mutation, it copies the mutable collections handed to it at submit time, and it
owns the prompt-composition rules that were inline in
``ui/chat_panel.py::_start_turn``'s worker closure (prefix separator, session
notes, model-switch review note).
"""
from __future__ import annotations
from dataclasses import FrozenInstanceError
from pathlib import Path
import pytest
from cowork_local.domain.agents.conversation_execution_request import (
ConversationExecutionRequest,
)
def _request(**overrides) -> ConversationExecutionRequest:
"""A minimal valid request; each test overrides only what it exercises."""
base = {"turn_id": "t1", "session_id": "s1"}
base.update(overrides)
return ConversationExecutionRequest(**base)
# -- immutability ---------------------------------------------------------- #
def test_request_rejects_mutation_after_construction() -> None:
request = _request(model="gpt-4o-mini")
with pytest.raises(FrozenInstanceError):
request.model = "claude-sonnet-4-6"
def test_turn_id_is_required() -> None:
with pytest.raises(ValueError):
ConversationExecutionRequest(turn_id="", session_id="s1")
def test_session_id_is_required() -> None:
with pytest.raises(ValueError):
ConversationExecutionRequest(turn_id="t1", session_id="")
# -- snapshotting mutable UI state ---------------------------------------- #
def test_attachments_are_snapshotted_away_from_the_caller_list() -> None:
picked = ["a.docx"]
request = _request(attachments=picked)
picked.append("b.pdf") # the composer clears/refills its own list next turn
assert request.attachments == ("a.docx",)
def test_messages_are_snapshotted_away_from_the_live_history_list() -> None:
history = [{"role": "user", "content": "earlier"}]
request = _request(messages=history)
history.append({"role": "assistant", "content": "later"})
assert len(request.messages) == 1
assert isinstance(request.messages, tuple)
def test_allowed_tools_none_means_every_tool_stays_available() -> None:
# None and () must stay distinguishable: None = no restriction, () = deny
# every built-in tool. Coercing None to () would silently disarm the agent.
assert _request().allowed_tools is None
assert _request(allowed_tools=[]).allowed_tools == ()
def test_output_paths_accept_strings_and_normalise_to_path() -> None:
request = _request(output_dir="out/t1", home_output_root="out")
assert request.output_dir == Path("out/t1")
assert request.home_output_root == Path("out")
# -- derived turn policy --------------------------------------------------- #
def test_effective_max_steps_uses_the_interactive_cap_by_default() -> None:
assert _request(max_steps=30, completion_max_steps=200).effective_max_steps == 30
def test_effective_max_steps_lifts_the_cap_when_running_to_completion() -> None:
request = _request(max_steps=30, completion_max_steps=200, run_to_completion=True)
assert request.effective_max_steps == 200
def test_permission_gate_is_required_only_in_confirm_mode() -> None:
assert _request(gate_mode="confirm").requires_permission_gate is True
assert _request(gate_mode="auto").requires_permission_gate is False
def test_has_prompt_ignores_whitespace_only_input() -> None:
assert _request(prompt=" \n ").has_prompt is False
assert _request(prompt="do it").has_prompt is True
# -- prompt composition (moved out of the widget's worker closure) --------- #
def test_user_content_returns_the_body_unchanged_without_prefix_or_notes() -> None:
assert _request().user_content("the body") == "the body"
def test_user_content_separates_the_instruction_prefix_from_the_body() -> None:
request = _request(instruction_prefix="SKILL RULES")
assert request.user_content("the body") == "SKILL RULES\n\n---\n\nthe body"
def test_user_content_appends_session_notes_after_the_body() -> None:
request = _request(session_notes="Files produced earlier: a.md")
assert request.user_content("the body") == "the body\n\nFiles produced earlier: a.md"
def test_user_content_falls_back_to_session_notes_when_the_body_is_empty() -> None:
# An attachment-only turn has no typed text, so the notes must not be
# prefixed with a stray blank line.
request = _request(session_notes="Files produced earlier: a.md")
assert request.user_content("") == "Files produced earlier: a.md"
def test_user_content_puts_the_review_note_ahead_of_everything_else() -> None:
request = _request(instruction_prefix="SKILL RULES", review_note="[Note: switched]")
content = request.user_content("the body")
assert content == "[Note: switched]\n\nSKILL RULES\n\n---\n\nthe body"