CI / test (push) Canceled after 0s
## 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>
133 lines
4.8 KiB
Python
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"
|