augment-sdk 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- augment/__init__.py +30 -0
- augment/acp/__init__.py +11 -0
- augment/acp/claude_code_client.py +365 -0
- augment/acp/client.py +640 -0
- augment/acp/test_client_e2e.py +472 -0
- augment/agent.py +1139 -0
- augment/exceptions.py +92 -0
- augment/function_tools.py +265 -0
- augment/listener.py +186 -0
- augment/listener_adapter.py +83 -0
- augment/prompt_formatter.py +343 -0
- augment_sdk-0.1.1.dist-info/METADATA +841 -0
- augment_sdk-0.1.1.dist-info/RECORD +17 -0
- augment_sdk-0.1.1.dist-info/WHEEL +5 -0
- augment_sdk-0.1.1.dist-info/entry_points.txt +2 -0
- augment_sdk-0.1.1.dist-info/licenses/LICENSE +22 -0
- augment_sdk-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""
|
|
2
|
+
End-to-end tests for AuggieACPClient.
|
|
3
|
+
|
|
4
|
+
These tests verify the client works correctly with the actual Augment CLI agent.
|
|
5
|
+
Uses pytest for test framework.
|
|
6
|
+
|
|
7
|
+
By default, only quick sanity check tests run (test_start_and_stop, test_simple_math_query, test_context_manager).
|
|
8
|
+
To run all tests including slow ones, use: pytest -m ""
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import time
|
|
13
|
+
from typing import Optional, Any, List, Dict
|
|
14
|
+
from augment.acp import AuggieACPClient, AgentEventListener
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventListenerForTesting(AgentEventListener):
|
|
18
|
+
"""Event listener that captures all events for testing purposes."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.message_chunks: List[str] = []
|
|
22
|
+
self.complete_messages: List[str] = []
|
|
23
|
+
self.tool_calls: List[Dict[str, Any]] = []
|
|
24
|
+
self.thoughts: List[str] = []
|
|
25
|
+
|
|
26
|
+
def on_agent_message_chunk(self, text: str) -> None:
|
|
27
|
+
self.message_chunks.append(text)
|
|
28
|
+
|
|
29
|
+
def on_agent_message(self, message: str) -> None:
|
|
30
|
+
self.complete_messages.append(message)
|
|
31
|
+
|
|
32
|
+
def on_tool_call(
|
|
33
|
+
self,
|
|
34
|
+
tool_call_id: str,
|
|
35
|
+
title: str,
|
|
36
|
+
kind: Optional[str] = None,
|
|
37
|
+
status: Optional[str] = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.tool_calls.append(
|
|
40
|
+
{
|
|
41
|
+
"type": "call",
|
|
42
|
+
"id": tool_call_id,
|
|
43
|
+
"title": title,
|
|
44
|
+
"kind": kind,
|
|
45
|
+
"status": status,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def on_tool_response(
|
|
50
|
+
self,
|
|
51
|
+
tool_call_id: str,
|
|
52
|
+
status: Optional[str] = None,
|
|
53
|
+
content: Optional[Any] = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
self.tool_calls.append(
|
|
56
|
+
{
|
|
57
|
+
"type": "response",
|
|
58
|
+
"id": tool_call_id,
|
|
59
|
+
"status": status,
|
|
60
|
+
"content": content,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def on_agent_thought(self, text: str) -> None:
|
|
65
|
+
self.thoughts.append(text)
|
|
66
|
+
|
|
67
|
+
def reset(self):
|
|
68
|
+
"""Reset all captured events."""
|
|
69
|
+
self.message_chunks.clear()
|
|
70
|
+
self.complete_messages.clear()
|
|
71
|
+
self.tool_calls.clear()
|
|
72
|
+
self.thoughts.clear()
|
|
73
|
+
|
|
74
|
+
def get_full_message(self) -> str:
|
|
75
|
+
"""Get the complete message from all chunks."""
|
|
76
|
+
return "".join(self.message_chunks)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ============================================================================
|
|
80
|
+
# Error Handling Tests
|
|
81
|
+
# ============================================================================
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_invalid_cli_path_raises_file_not_found():
|
|
85
|
+
"""Test that providing a non-existent CLI path raises FileNotFoundError."""
|
|
86
|
+
with pytest.raises(FileNotFoundError, match="CLI not found at"):
|
|
87
|
+
AuggieACPClient(cli_path="/path/to/nonexistent/cli")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_invalid_cli_path_fails_on_start():
|
|
91
|
+
"""Test that starting with an invalid CLI path raises RuntimeError immediately."""
|
|
92
|
+
# Create a temporary file that exists but isn't a valid Node.js script
|
|
93
|
+
import tempfile
|
|
94
|
+
import time
|
|
95
|
+
|
|
96
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f:
|
|
97
|
+
f.write("not a real CLI")
|
|
98
|
+
temp_path = f.name
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
client = AuggieACPClient(cli_path=temp_path)
|
|
102
|
+
|
|
103
|
+
# Should raise RuntimeError when the process exits immediately
|
|
104
|
+
# This should happen very quickly (< 1 second), not hang
|
|
105
|
+
start_time = time.time()
|
|
106
|
+
with pytest.raises(RuntimeError, match="Agent process exited"):
|
|
107
|
+
client.start()
|
|
108
|
+
elapsed = time.time() - start_time
|
|
109
|
+
|
|
110
|
+
# Verify it failed quickly (within 2 seconds)
|
|
111
|
+
assert elapsed < 2.0, f"Process exit detection took too long: {elapsed:.2f}s"
|
|
112
|
+
finally:
|
|
113
|
+
import os
|
|
114
|
+
|
|
115
|
+
os.unlink(temp_path)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ============================================================================
|
|
119
|
+
# Basic Functionality Tests
|
|
120
|
+
# ============================================================================
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_start_and_stop():
|
|
124
|
+
"""Test basic start and stop functionality."""
|
|
125
|
+
client = AuggieACPClient()
|
|
126
|
+
|
|
127
|
+
# Should not be running initially
|
|
128
|
+
assert not client.is_running
|
|
129
|
+
assert client.session_id is None
|
|
130
|
+
|
|
131
|
+
# Start the client
|
|
132
|
+
client.start()
|
|
133
|
+
assert client.is_running
|
|
134
|
+
assert client.session_id is not None
|
|
135
|
+
session_id = client.session_id
|
|
136
|
+
assert len(session_id) == 36 # UUID format
|
|
137
|
+
|
|
138
|
+
# Stop the client
|
|
139
|
+
client.stop()
|
|
140
|
+
assert not client.is_running
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_simple_math_query():
|
|
144
|
+
"""Test sending a simple math query."""
|
|
145
|
+
with AuggieACPClient() as client:
|
|
146
|
+
response = client.send_message("What is 2 + 2? Answer with just the number.")
|
|
147
|
+
|
|
148
|
+
# Should contain the answer
|
|
149
|
+
assert "4" in response
|
|
150
|
+
assert len(response) > 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.slow
|
|
154
|
+
def test_multiple_messages_same_session():
|
|
155
|
+
"""Test sending multiple messages in the same session."""
|
|
156
|
+
with AuggieACPClient() as client:
|
|
157
|
+
session_id = client.session_id
|
|
158
|
+
|
|
159
|
+
# First message
|
|
160
|
+
response1 = client.send_message("What is 5 + 3?")
|
|
161
|
+
assert "8" in response1
|
|
162
|
+
|
|
163
|
+
# Session should remain the same
|
|
164
|
+
assert client.session_id == session_id
|
|
165
|
+
|
|
166
|
+
# Second message - agent should remember context
|
|
167
|
+
response2 = client.send_message("What is that number times 2?")
|
|
168
|
+
assert "16" in response2
|
|
169
|
+
|
|
170
|
+
# Session should still be the same
|
|
171
|
+
assert client.session_id == session_id
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_context_manager():
|
|
175
|
+
"""Test context manager automatically starts and stops."""
|
|
176
|
+
client = AuggieACPClient()
|
|
177
|
+
assert not client.is_running
|
|
178
|
+
|
|
179
|
+
with client:
|
|
180
|
+
assert client.is_running
|
|
181
|
+
response = client.send_message("What is 10 * 5?")
|
|
182
|
+
assert "50" in response
|
|
183
|
+
|
|
184
|
+
# Should be stopped after exiting context
|
|
185
|
+
assert not client.is_running
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.slow
|
|
189
|
+
def test_clear_context():
|
|
190
|
+
"""Test clearing context creates a new session."""
|
|
191
|
+
client = AuggieACPClient()
|
|
192
|
+
client.start()
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Get initial session
|
|
196
|
+
session1 = client.session_id
|
|
197
|
+
|
|
198
|
+
# Send a message
|
|
199
|
+
client.send_message("Remember the number 42")
|
|
200
|
+
|
|
201
|
+
# Clear context
|
|
202
|
+
client.clear_context()
|
|
203
|
+
|
|
204
|
+
# Should have a new session
|
|
205
|
+
session2 = client.session_id
|
|
206
|
+
assert session1 != session2
|
|
207
|
+
assert client.is_running
|
|
208
|
+
|
|
209
|
+
# Agent should not remember the previous conversation
|
|
210
|
+
response = client.send_message("What number did I tell you to remember?")
|
|
211
|
+
# Response should indicate agent doesn't remember
|
|
212
|
+
assert any(
|
|
213
|
+
word in response.lower()
|
|
214
|
+
for word in ["don't", "no record", "haven't", "didn't"]
|
|
215
|
+
)
|
|
216
|
+
finally:
|
|
217
|
+
client.stop()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ============================================================================
|
|
221
|
+
# Event Listener Tests
|
|
222
|
+
# ============================================================================
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@pytest.mark.slow
|
|
226
|
+
@pytest.mark.skip(reason="CLI does not currently send agent_message_end events")
|
|
227
|
+
def test_event_listener_messages():
|
|
228
|
+
"""Test event listener receives agent messages."""
|
|
229
|
+
listener = EventListenerForTesting()
|
|
230
|
+
|
|
231
|
+
with AuggieACPClient(listener=listener) as client:
|
|
232
|
+
response = client.send_message("What is 7 + 3?")
|
|
233
|
+
|
|
234
|
+
# Give a moment for the agent_message_end event to arrive
|
|
235
|
+
# (it may arrive slightly after send_message returns)
|
|
236
|
+
time.sleep(0.5)
|
|
237
|
+
|
|
238
|
+
# Listener should have received message chunks
|
|
239
|
+
assert len(listener.message_chunks) > 0
|
|
240
|
+
|
|
241
|
+
# Combined message should match response
|
|
242
|
+
full_message = listener.get_full_message()
|
|
243
|
+
assert full_message.strip() == response.strip()
|
|
244
|
+
|
|
245
|
+
# Should also have received the complete message
|
|
246
|
+
assert len(listener.complete_messages) > 0
|
|
247
|
+
assert listener.complete_messages[0].strip() == response.strip()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@pytest.mark.slow
|
|
251
|
+
@pytest.mark.timeout(30)
|
|
252
|
+
@pytest.mark.skip(
|
|
253
|
+
reason="Test times out - tool call events may not be working correctly"
|
|
254
|
+
)
|
|
255
|
+
def test_event_listener_tool_calls():
|
|
256
|
+
"""Test event listener receives tool call events."""
|
|
257
|
+
listener = EventListenerForTesting()
|
|
258
|
+
|
|
259
|
+
with AuggieACPClient(listener=listener) as client:
|
|
260
|
+
# Send a message that will trigger a tool call
|
|
261
|
+
client.send_message(
|
|
262
|
+
"Read the file experimental/guy/auggie_sdk/QUICK_START.md", timeout=30.0
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Should have received tool call events
|
|
266
|
+
assert len(listener.tool_calls) > 0
|
|
267
|
+
|
|
268
|
+
# Should have at least one "call" event
|
|
269
|
+
call_events = [tc for tc in listener.tool_calls if tc["type"] == "call"]
|
|
270
|
+
assert len(call_events) > 0
|
|
271
|
+
|
|
272
|
+
# Should have at least one "response" event
|
|
273
|
+
response_events = [tc for tc in listener.tool_calls if tc["type"] == "response"]
|
|
274
|
+
assert len(response_events) > 0
|
|
275
|
+
|
|
276
|
+
# At least one tool call should be "view" (for reading the file)
|
|
277
|
+
tool_titles = [tc.get("title") for tc in listener.tool_calls]
|
|
278
|
+
assert "view" in tool_titles
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ============================================================================
|
|
282
|
+
# Timeout and Error Handling Tests
|
|
283
|
+
# ============================================================================
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@pytest.mark.slow
|
|
287
|
+
def test_timeout_handling():
|
|
288
|
+
"""Test that timeout parameter works."""
|
|
289
|
+
with AuggieACPClient() as client:
|
|
290
|
+
# Short timeout should still work for simple queries
|
|
291
|
+
response = client.send_message("What is 1 + 1?", timeout=5.0)
|
|
292
|
+
assert "2" in response
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@pytest.mark.slow
|
|
296
|
+
def test_error_when_not_started():
|
|
297
|
+
"""Test that sending message without starting raises error."""
|
|
298
|
+
client = AuggieACPClient()
|
|
299
|
+
|
|
300
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
301
|
+
client.send_message("Hello")
|
|
302
|
+
|
|
303
|
+
assert "not started" in str(exc_info.value).lower()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@pytest.mark.slow
|
|
307
|
+
def test_double_start_raises_error():
|
|
308
|
+
"""Test that starting an already started client raises error."""
|
|
309
|
+
client = AuggieACPClient()
|
|
310
|
+
client.start()
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
314
|
+
client.start()
|
|
315
|
+
|
|
316
|
+
assert "already started" in str(exc_info.value).lower()
|
|
317
|
+
finally:
|
|
318
|
+
client.stop()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ============================================================================
|
|
322
|
+
# Session Management Tests
|
|
323
|
+
# ============================================================================
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@pytest.mark.slow
|
|
327
|
+
def test_multiple_sequential_sessions():
|
|
328
|
+
"""Test starting and stopping multiple times."""
|
|
329
|
+
client = AuggieACPClient()
|
|
330
|
+
|
|
331
|
+
# First session
|
|
332
|
+
client.start()
|
|
333
|
+
session1 = client.session_id
|
|
334
|
+
response1 = client.send_message("What is 2 + 2?")
|
|
335
|
+
assert "4" in response1
|
|
336
|
+
client.stop()
|
|
337
|
+
|
|
338
|
+
# Second session
|
|
339
|
+
client.start()
|
|
340
|
+
session2 = client.session_id
|
|
341
|
+
response2 = client.send_message("What is 3 + 3?")
|
|
342
|
+
assert "6" in response2
|
|
343
|
+
client.stop()
|
|
344
|
+
|
|
345
|
+
# Sessions should be different
|
|
346
|
+
assert session1 != session2
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@pytest.mark.slow
|
|
350
|
+
@pytest.mark.timeout(30)
|
|
351
|
+
def test_long_response():
|
|
352
|
+
"""Test handling longer responses."""
|
|
353
|
+
with AuggieACPClient() as client:
|
|
354
|
+
response = client.send_message(
|
|
355
|
+
"List three programming languages. Be brief.", timeout=30.0
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Should get a response with some content
|
|
359
|
+
assert len(response) > 10
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@pytest.mark.slow
|
|
363
|
+
@pytest.mark.timeout(30)
|
|
364
|
+
@pytest.mark.skip(reason="Tool call events not being received from CLI")
|
|
365
|
+
def test_file_operation_tool_call():
|
|
366
|
+
"""Test that file operations trigger appropriate tool calls."""
|
|
367
|
+
listener = EventListenerForTesting()
|
|
368
|
+
|
|
369
|
+
with AuggieACPClient(listener=listener) as client:
|
|
370
|
+
response = client.send_message(
|
|
371
|
+
"What is in the file experimental/guy/auggie_sdk/QUICK_START.md? Summarize in one sentence.",
|
|
372
|
+
timeout=30.0,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Should have triggered a view tool call
|
|
376
|
+
tool_titles = [
|
|
377
|
+
tc.get("title") for tc in listener.tool_calls if tc["type"] == "start"
|
|
378
|
+
]
|
|
379
|
+
assert "view" in tool_titles
|
|
380
|
+
|
|
381
|
+
# Response should mention something about the file
|
|
382
|
+
assert len(response) > 20
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@pytest.mark.slow
|
|
386
|
+
def test_listener_can_be_none():
|
|
387
|
+
"""Test that listener is optional."""
|
|
388
|
+
# Should work fine without a listener
|
|
389
|
+
with AuggieACPClient(listener=None) as client:
|
|
390
|
+
response = client.send_message("What is 5 * 5?")
|
|
391
|
+
assert "25" in response
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@pytest.mark.slow
|
|
395
|
+
def test_session_persistence():
|
|
396
|
+
"""Test that session persists across multiple messages."""
|
|
397
|
+
listener = EventListenerForTesting()
|
|
398
|
+
|
|
399
|
+
with AuggieACPClient(listener=listener) as client:
|
|
400
|
+
session_id = client.session_id
|
|
401
|
+
|
|
402
|
+
# Send multiple messages
|
|
403
|
+
for i in range(3):
|
|
404
|
+
listener.reset()
|
|
405
|
+
client.send_message(f"What is {i} + 1?")
|
|
406
|
+
|
|
407
|
+
# Session should remain the same
|
|
408
|
+
assert client.session_id == session_id
|
|
409
|
+
|
|
410
|
+
# Should get message chunks each time
|
|
411
|
+
assert len(listener.message_chunks) > 0
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ============================================================================
|
|
415
|
+
# Integration Tests
|
|
416
|
+
# ============================================================================
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@pytest.mark.slow
|
|
420
|
+
def test_conversation_flow():
|
|
421
|
+
"""Test a realistic conversation flow."""
|
|
422
|
+
listener = EventListenerForTesting()
|
|
423
|
+
|
|
424
|
+
with AuggieACPClient(listener=listener) as client:
|
|
425
|
+
# Start a conversation
|
|
426
|
+
response1 = client.send_message("What is 10 + 5?")
|
|
427
|
+
assert "15" in response1
|
|
428
|
+
|
|
429
|
+
# Continue the conversation
|
|
430
|
+
listener.reset()
|
|
431
|
+
response2 = client.send_message("What is that divided by 3?")
|
|
432
|
+
assert "5" in response2
|
|
433
|
+
|
|
434
|
+
# Clear context and start fresh
|
|
435
|
+
client.clear_context()
|
|
436
|
+
listener.reset()
|
|
437
|
+
|
|
438
|
+
# Agent should not remember the previous numbers
|
|
439
|
+
response3 = client.send_message("What was the last number we calculated?")
|
|
440
|
+
assert any(
|
|
441
|
+
word in response3.lower() for word in ["don't", "no record", "haven't"]
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@pytest.mark.slow
|
|
446
|
+
@pytest.mark.timeout(30)
|
|
447
|
+
@pytest.mark.skip(reason="Tool call events not being received from CLI")
|
|
448
|
+
def test_tool_usage_workflow():
|
|
449
|
+
"""Test a workflow that involves tool usage."""
|
|
450
|
+
listener = EventListenerForTesting()
|
|
451
|
+
|
|
452
|
+
with AuggieACPClient(listener=listener) as client:
|
|
453
|
+
# Ask agent to read a file
|
|
454
|
+
response = client.send_message(
|
|
455
|
+
"Read experimental/guy/auggie_sdk/QUICK_START.md and tell me what it's about in 5 words or less.",
|
|
456
|
+
timeout=30.0,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Should have used the view tool
|
|
460
|
+
tool_titles = [
|
|
461
|
+
tc.get("title") for tc in listener.tool_calls if tc["type"] == "start"
|
|
462
|
+
]
|
|
463
|
+
assert "view" in tool_titles
|
|
464
|
+
|
|
465
|
+
# Should have gotten a response
|
|
466
|
+
assert len(response) > 5
|
|
467
|
+
|
|
468
|
+
# Should have received completion status
|
|
469
|
+
statuses = [
|
|
470
|
+
tc.get("status") for tc in listener.tool_calls if tc["type"] == "update"
|
|
471
|
+
]
|
|
472
|
+
assert "completed" in statuses
|