File size: 8,138 Bytes
b484b4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
"""
Launch Desk — Agent & Tool Tests
================================
Pytest tests that verify the agent has 4 tools registered and that each
tool returns the expected structure.

Because @function_tool wraps functions in a FunctionTool dataclass, we
access the original callable via `tool.on_invoke_tool._get_wrapped_callable()`.
"""

import pytest

from server.agent import agent
from server.tools import (
    extract_tasks,
    check_readiness,
    generate_checklist,
    draft_copy,
)


# ---------------------------------------------------------------------------
# Helper: get the original wrapped callable from a FunctionTool
# ---------------------------------------------------------------------------

def _get_callable(tool):
    """Extract the original Python function from a @function_tool wrapper."""
    return tool.on_invoke_tool._get_wrapped_callable()


# ---------------------------------------------------------------------------
# Test: agent has 4 tools registered
# ---------------------------------------------------------------------------

def test_agent_has_four_tools():
    """The Launch Planner agent should have exactly 4 tools registered."""
    assert agent is not None
    assert agent.name == "Launch Planner"
    assert agent.tools is not None
    assert len(agent.tools) == 4, f"Expected 4 tools, got {len(agent.tools)}"


def test_agent_has_expected_tool_names():
    """The agent should have tools with the expected names."""
    tool_names = {t.name for t in agent.tools}
    expected = {"extract_tasks", "check_readiness", "generate_checklist", "draft_copy"}
    assert tool_names == expected, f"Expected {expected}, got {tool_names}"


# ---------------------------------------------------------------------------
# Test: extract_tasks returns expected structure
# ---------------------------------------------------------------------------

def test_extract_tasks_returns_list_of_dicts():
    """extract_tasks should return a list of dicts with task/priority/owner/deadline."""
    fn = _get_callable(extract_tasks)
    brief = "We are launching a new API with backend, QA testing, and marketing campaign"
    result = fn(brief)

    assert isinstance(result, list)
    assert len(result) > 0

    for task in result:
        assert isinstance(task, dict)
        assert "task" in task
        assert "priority" in task
        assert "owner" in task
        assert "deadline" in task


def test_extract_tasks_handles_empty_brief():
    """extract_tasks should handle an empty brief gracefully."""
    fn = _get_callable(extract_tasks)
    result = fn("")
    assert isinstance(result, list)
    assert len(result) > 0
    assert "task" in result[0]


def test_extract_tasks_priority_values():
    """All priorities should be high, medium, or low."""
    fn = _get_callable(extract_tasks)
    result = fn("API, QA, docs, marketing, support, security, pricing, analytics, design, legal")
    valid_priorities = {"high", "medium", "low"}
    for task in result:
        assert task["priority"] in valid_priorities


# ---------------------------------------------------------------------------
# Test: check_readiness returns expected structure
# ---------------------------------------------------------------------------

def test_check_readiness_returns_dict():
    """check_readiness should return a dict with ready/score/gaps."""
    fn = _get_callable(check_readiness)
    tasks = "code freeze, QA testing, marketing campaign, documentation, support staffing"
    result = fn(tasks, "2026-10-01")

    assert isinstance(result, dict)
    assert "ready" in result
    assert "score" in result
    assert "gaps" in result
    assert isinstance(result["ready"], bool)
    assert isinstance(result["score"], int)
    assert isinstance(result["gaps"], list)


def test_check_readiness_high_score_when_covered():
    """check_readiness should give a high score when all rubric areas are covered."""
    fn = _get_callable(check_readiness)
    tasks = "code freeze, QA testing, marketing campaign, documentation, support staffing"
    result = fn(tasks, "2026-10-01")
    assert result["score"] >= 80, f"Expected high score, got {result['score']}"


def test_check_readiness_low_score_when_gaps():
    """check_readiness should give a low score when rubric areas are missing."""
    fn = _get_callable(check_readiness)
    tasks = "just some random text with no keywords"
    result = fn(tasks, "2026-10-01")
    assert result["score"] < 100
    assert len(result["gaps"]) > 0


# ---------------------------------------------------------------------------
# Test: generate_checklist returns expected structure
# ---------------------------------------------------------------------------

def test_generate_checklist_returns_list_of_dicts():
    """generate_checklist should return a list of {owner, items} dicts."""
    fn = _get_callable(generate_checklist)
    import json
    tasks_json = json.dumps([
        {"task": "Freeze code", "owner": "Engineering", "priority": "high", "deadline": "T-14"},
        {"task": "QA testing", "owner": "QA", "priority": "high", "deadline": "T-7"},
        {"task": "Marketing campaign", "owner": "Marketing", "priority": "high", "deadline": "T-5"},
    ])
    result = fn(tasks_json)

    assert isinstance(result, list)
    assert len(result) > 0

    for checklist in result:
        assert isinstance(checklist, dict)
        assert "owner" in checklist
        assert "items" in checklist
        assert isinstance(checklist["items"], list)
        assert len(checklist["items"]) > 0
        for item in checklist["items"]:
            assert isinstance(item, str)


def test_generate_checklist_includes_launch_lead():
    """generate_checklist should include a Launch Lead checklist."""
    fn = _get_callable(generate_checklist)
    result = fn("[]")
    owners = [c["owner"] for c in result]
    assert "Launch Lead" in owners


def test_generate_checklist_groups_by_owner():
    """generate_checklist should group tasks by owner."""
    import json
    fn = _get_callable(generate_checklist)
    tasks_json = json.dumps([
        {"task": "Task A", "owner": "Engineering"},
        {"task": "Task B", "owner": "Engineering"},
        {"task": "Task C", "owner": "Marketing"},
    ])
    result = fn(tasks_json)
    owners = {c["owner"] for c in result}
    assert "Engineering" in owners
    assert "Marketing" in owners


# ---------------------------------------------------------------------------
# Test: draft_copy returns expected structure
# ---------------------------------------------------------------------------

def test_draft_copy_returns_dict():
    """draft_copy should return a dict with headline/body/cta."""
    fn = _get_callable(draft_copy)
    result = fn("email", "LaunchDesk Pro", "enterprise DevOps teams")

    assert isinstance(result, dict)
    assert "headline" in result
    assert "body" in result
    assert "cta" in result
    assert isinstance(result["headline"], str)
    assert isinstance(result["body"], str)
    assert isinstance(result["cta"], str)


def test_draft_copy_channel_specific():
    """draft_copy should produce different copy for different channels."""
    fn = _get_callable(draft_copy)
    email = fn("email", "ProductX", "developers")
    twitter = fn("twitter", "ProductX", "developers")
    assert email["headline"] != twitter["headline"], "Copy should differ by channel"


def test_draft_copy_includes_product_and_audience():
    """draft_copy should reference the product name and audience."""
    fn = _get_callable(draft_copy)
    result = fn("email", "MyProduct", "data scientists")
    assert "MyProduct" in result["headline"] or "MyProduct" in result["body"]
    assert "data scientists" in result["headline"] or "data scientists" in result["body"]


def test_draft_copy_handles_unknown_channel():
    """draft_copy should handle an unknown channel gracefully."""
    fn = _get_callable(draft_copy)
    result = fn("radio", "ProductX", "general audience")
    assert isinstance(result, dict)
    assert "headline" in result
    assert "body" in result
    assert "cta" in result