tylor-mcp 1.0.0
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.
- package/.aws-setup.sh +25 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.mcp.json +12 -0
- package/AGENTS.md +93 -0
- package/CLAUDE.md +99 -0
- package/CLAUDE_PLATFORM_AWS_SETUP.md +105 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/assets/tylor_logo.png +0 -0
- package/assets/tylor_threads_concept.png +0 -0
- package/bin/tylor.js +23 -0
- package/hooks/kill-thread-trigger.sh +7 -0
- package/hooks/post-tool-use-code-index.sh +7 -0
- package/hooks/session-checkpoint.sh +7 -0
- package/hooks/session-start.sh +7 -0
- package/install.py +401 -0
- package/install.sh +260 -0
- package/package.json +24 -0
- package/pytest.ini +2 -0
- package/registry.json +26 -0
- package/server/.env.example +24 -0
- package/server/__init__.py +0 -0
- package/server/config.py +89 -0
- package/server/main.py +93 -0
- package/server/personas/analyst.md +15 -0
- package/server/personas/ceo.md +14 -0
- package/server/personas/code_agent.md +15 -0
- package/server/personas/cto.md +14 -0
- package/server/provision.py +260 -0
- package/server/provision_opensearch.py +154 -0
- package/server/requirements.txt +26 -0
- package/server/storage/__init__.py +0 -0
- package/server/storage/dynamo.py +399 -0
- package/server/storage/json_store.py +359 -0
- package/server/storage/opensearch.py +194 -0
- package/server/storage/s3.py +96 -0
- package/server/storage/tests/__init__.py +0 -0
- package/server/storage/tests/test_dynamo.py +452 -0
- package/server/storage/tests/test_json_store.py +226 -0
- package/server/storage/tests/test_opensearch.py +270 -0
- package/server/storage/tests/test_s3.py +125 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/test_install.py +606 -0
- package/server/tests/test_isolation.py +90 -0
- package/server/tests/test_ui_server.py +385 -0
- package/server/tests/test_ui_shader_background.py +52 -0
- package/server/tests/test_ui_story_6_3.py +105 -0
- package/server/tools/__init__.py +0 -0
- package/server/tools/_mcp.py +4 -0
- package/server/tools/agents.py +160 -0
- package/server/tools/ecc/__init__.py +1 -0
- package/server/tools/ecc/data.py +35 -0
- package/server/tools/ecc/diagrams.py +23 -0
- package/server/tools/ecc/pipeline.py +24 -0
- package/server/tools/ecc/presentation.py +24 -0
- package/server/tools/ecc/web.py +23 -0
- package/server/tools/executor.py +880 -0
- package/server/tools/harness.py +330 -0
- package/server/tools/help.py +162 -0
- package/server/tools/hooks.py +357 -0
- package/server/tools/personas.py +110 -0
- package/server/tools/registry.py +195 -0
- package/server/tools/router.py +117 -0
- package/server/tools/skill_installer.py +230 -0
- package/server/tools/summarizer.py +168 -0
- package/server/tools/tests/__init__.py +0 -0
- package/server/tools/tests/test_agents.py +246 -0
- package/server/tools/tests/test_code_index.py +108 -0
- package/server/tools/tests/test_ecc_tools.py +51 -0
- package/server/tools/tests/test_executor.py +584 -0
- package/server/tools/tests/test_help_agent101.py +149 -0
- package/server/tools/tests/test_hooks.py +124 -0
- package/server/tools/tests/test_kill_thread.py +125 -0
- package/server/tools/tests/test_new_thread_list_threads.py +293 -0
- package/server/tools/tests/test_personas.py +52 -0
- package/server/tools/tests/test_recall_memory.py +55 -0
- package/server/tools/tests/test_registry_client.py +308 -0
- package/server/tools/tests/test_router.py +263 -0
- package/server/tools/tests/test_skill_installer.py +174 -0
- package/server/tools/tests/test_switch_thread.py +163 -0
- package/server/tools/tests/test_thread_command_skills.py +54 -0
- package/server/tools/tests/test_thread_resolver.py +165 -0
- package/server/tools/tests/test_tier1_schema.py +296 -0
- package/server/tools/thread_resolver.py +75 -0
- package/server/tools/tylor.py +374 -0
- package/server/tools/ui.py +38 -0
- package/server/ui_server.py +292 -0
- package/server/validate.py +237 -0
- package/skills/add-skill/SKILL.md +37 -0
- package/skills/afk-status/SKILL.md +20 -0
- package/skills/bmad/SKILL.md +14 -0
- package/skills/help-agent101/SKILL.md +48 -0
- package/skills/kill-thread/SKILL.md +35 -0
- package/skills/list-threads/SKILL.md +35 -0
- package/skills/new-thread/SKILL.md +35 -0
- package/skills/recall/SKILL.md +39 -0
- package/skills/run/SKILL.md +33 -0
- package/skills/set-sandbox/SKILL.md +38 -0
- package/skills/switch-thread/SKILL.md +38 -0
- package/ui/claude-logo.png +0 -0
- package/ui/index.html +1314 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Story 2.2: OpenSearch Vector Client
|
|
3
|
+
Run: pytest server/storage/tests/test_opensearch.py -v
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest.mock import MagicMock, patch
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
PLUGIN_DIR = Path(__file__).parent.parent.parent.parent
|
|
13
|
+
sys.path.insert(0, str(PLUGIN_DIR))
|
|
14
|
+
|
|
15
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
16
|
+
from server.storage.opensearch import OpenSearchClient, INDEX, TITAN_MODEL, VECTOR_DIM
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Helpers
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
FAKE_EMBEDDING = [0.1] * VECTOR_DIM
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_os_client():
|
|
27
|
+
"""Return OpenSearchClient with mocked OpenSearch + Bedrock."""
|
|
28
|
+
with (
|
|
29
|
+
patch("server.storage.opensearch.OpenSearch") as mock_os_cls,
|
|
30
|
+
patch("boto3.Session") as mock_session_cls,
|
|
31
|
+
):
|
|
32
|
+
mock_os = MagicMock()
|
|
33
|
+
mock_os_cls.return_value = mock_os
|
|
34
|
+
|
|
35
|
+
mock_session = MagicMock()
|
|
36
|
+
mock_session_cls.return_value = mock_session
|
|
37
|
+
mock_bedrock = MagicMock()
|
|
38
|
+
mock_session.client.return_value = mock_bedrock
|
|
39
|
+
|
|
40
|
+
client = OpenSearchClient(host="localhost", port=9200)
|
|
41
|
+
client._os = mock_os
|
|
42
|
+
client._bedrock = mock_bedrock
|
|
43
|
+
return client, mock_os, mock_bedrock
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _mock_embed(mock_bedrock):
|
|
47
|
+
"""Configure mock_bedrock to return a valid 1536-dim embedding."""
|
|
48
|
+
mock_body = MagicMock()
|
|
49
|
+
mock_body.read.return_value = json.dumps({"embedding": FAKE_EMBEDDING}).encode()
|
|
50
|
+
mock_bedrock.invoke_model.return_value = {"body": mock_body}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# AC3: index_memory embeds via Titan and writes to agent-memories
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def test_index_memory_calls_bedrock_with_correct_model():
|
|
58
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
59
|
+
_mock_embed(mock_bedrock)
|
|
60
|
+
mock_os.index.return_value = {"result": "created"}
|
|
61
|
+
|
|
62
|
+
client.index_memory(thread_id="t1", fact="The project uses FastMCP v1.")
|
|
63
|
+
|
|
64
|
+
call_kwargs = mock_bedrock.invoke_model.call_args.kwargs
|
|
65
|
+
assert call_kwargs["modelId"] == TITAN_MODEL
|
|
66
|
+
body = json.loads(call_kwargs["body"])
|
|
67
|
+
assert body["inputText"] == "The project uses FastMCP v1."
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_index_memory_writes_to_agent_memories_index():
|
|
71
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
72
|
+
_mock_embed(mock_bedrock)
|
|
73
|
+
mock_os.index.return_value = {"result": "created"}
|
|
74
|
+
|
|
75
|
+
client.index_memory(thread_id="t1", fact="fact text")
|
|
76
|
+
|
|
77
|
+
call_kwargs = mock_os.index.call_args.kwargs
|
|
78
|
+
assert call_kwargs["index"] == INDEX # "agent-memories"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_index_memory_stores_thread_id_and_content():
|
|
82
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
83
|
+
_mock_embed(mock_bedrock)
|
|
84
|
+
mock_os.index.return_value = {"result": "created"}
|
|
85
|
+
|
|
86
|
+
client.index_memory(thread_id="t1", fact="important fact", metadata={"source": "user"})
|
|
87
|
+
|
|
88
|
+
doc = mock_os.index.call_args.kwargs["body"]
|
|
89
|
+
assert doc["thread_id"] == "t1"
|
|
90
|
+
assert doc["content"] == "important fact"
|
|
91
|
+
assert doc["source"] == "user"
|
|
92
|
+
assert len(doc["embedding"]) == VECTOR_DIM
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_index_memory_returns_doc_id():
|
|
96
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
97
|
+
_mock_embed(mock_bedrock)
|
|
98
|
+
mock_os.index.return_value = {"result": "created"}
|
|
99
|
+
|
|
100
|
+
doc_id = client.index_memory(thread_id="t1", fact="fact")
|
|
101
|
+
assert isinstance(doc_id, str)
|
|
102
|
+
assert len(doc_id) == 32 # uuid4().hex
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_index_memory_raises_tool_error_on_bedrock_failure():
|
|
106
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
107
|
+
mock_bedrock.invoke_model.side_effect = Exception("Bedrock throttled")
|
|
108
|
+
|
|
109
|
+
with pytest.raises(ToolError, match="embedding failed"):
|
|
110
|
+
client.index_memory(thread_id="t1", fact="fact")
|
|
111
|
+
|
|
112
|
+
mock_os.index.assert_not_called()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_index_memory_raises_tool_error_on_opensearch_failure():
|
|
116
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
117
|
+
_mock_embed(mock_bedrock)
|
|
118
|
+
mock_os.index.side_effect = Exception("connection refused")
|
|
119
|
+
|
|
120
|
+
with pytest.raises(ToolError, match="index_memory failed"):
|
|
121
|
+
client.index_memory(thread_id="t1", fact="fact")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# AC4: search_memory returns thread-scoped results
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def test_search_memory_query_includes_thread_id_filter():
|
|
129
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
130
|
+
_mock_embed(mock_bedrock)
|
|
131
|
+
mock_os.search.return_value = {"hits": {"hits": []}}
|
|
132
|
+
|
|
133
|
+
client.search_memory(thread_id="t1", query="project name")
|
|
134
|
+
|
|
135
|
+
os_query = mock_os.search.call_args.kwargs["body"]
|
|
136
|
+
bool_query = os_query["query"]["bool"]
|
|
137
|
+
filters = bool_query["must"]
|
|
138
|
+
term_filters = [f for f in filters if "term" in f]
|
|
139
|
+
assert any(f["term"].get("thread_id") == "t1" for f in term_filters)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_search_memory_query_includes_type_filter_when_provided():
|
|
143
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
144
|
+
_mock_embed(mock_bedrock)
|
|
145
|
+
mock_os.search.return_value = {"hits": {"hits": []}}
|
|
146
|
+
|
|
147
|
+
client.search_memory(thread_id="t1", query="SignIn", type="code_index")
|
|
148
|
+
|
|
149
|
+
os_query = mock_os.search.call_args.kwargs["body"]
|
|
150
|
+
filters = os_query["query"]["bool"]["must"]
|
|
151
|
+
assert any(f.get("term", {}).get("type") == "code_index" for f in filters)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_search_memory_returns_correct_shape():
|
|
155
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
156
|
+
_mock_embed(mock_bedrock)
|
|
157
|
+
mock_os.search.return_value = {
|
|
158
|
+
"hits": {
|
|
159
|
+
"hits": [
|
|
160
|
+
{
|
|
161
|
+
"_id": "abc123",
|
|
162
|
+
"_score": 0.95,
|
|
163
|
+
"_source": {
|
|
164
|
+
"thread_id": "t1",
|
|
165
|
+
"content": "FastMCP is great",
|
|
166
|
+
"created_at": "2026-05-12T10:00:00Z",
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
results = client.search_memory(thread_id="t1", query="MCP")
|
|
174
|
+
|
|
175
|
+
assert len(results) == 1
|
|
176
|
+
r = results[0]
|
|
177
|
+
assert r["id"] == "abc123"
|
|
178
|
+
assert r["content"] == "FastMCP is great"
|
|
179
|
+
assert r["thread_id"] == "t1"
|
|
180
|
+
assert r["score"] == 0.95
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_search_memory_filters_out_wrong_thread_results(caplog):
|
|
184
|
+
"""Defence-in-depth: results with wrong thread_id are dropped at client layer."""
|
|
185
|
+
import logging
|
|
186
|
+
|
|
187
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
188
|
+
_mock_embed(mock_bedrock)
|
|
189
|
+
mock_os.search.return_value = {
|
|
190
|
+
"hits": {
|
|
191
|
+
"hits": [
|
|
192
|
+
{
|
|
193
|
+
"_id": "doc1",
|
|
194
|
+
"_score": 0.9,
|
|
195
|
+
"_source": {
|
|
196
|
+
"thread_id": "t2", # wrong thread
|
|
197
|
+
"content": "other thread fact",
|
|
198
|
+
"created_at": "2026-05-12T10:00:00Z",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"_id": "doc2",
|
|
203
|
+
"_score": 0.8,
|
|
204
|
+
"_source": {
|
|
205
|
+
"thread_id": "t1", # correct
|
|
206
|
+
"content": "correct fact",
|
|
207
|
+
"created_at": "2026-05-12T10:00:00Z",
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
with caplog.at_level(logging.WARNING, logger="server.storage.opensearch"):
|
|
215
|
+
results = client.search_memory(thread_id="t1", query="something")
|
|
216
|
+
|
|
217
|
+
assert len(results) == 1
|
|
218
|
+
assert results[0]["content"] == "correct fact"
|
|
219
|
+
assert any("wrong thread_id" in r.message for r in caplog.records)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_search_memory_uses_k_parameter():
|
|
223
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
224
|
+
_mock_embed(mock_bedrock)
|
|
225
|
+
mock_os.search.return_value = {"hits": {"hits": []}}
|
|
226
|
+
|
|
227
|
+
client.search_memory(thread_id="t1", query="q", k=10)
|
|
228
|
+
|
|
229
|
+
os_query = mock_os.search.call_args.kwargs["body"]
|
|
230
|
+
assert os_query["size"] == 10
|
|
231
|
+
knn_field = os_query["query"]["bool"]["must"][0]["knn"]["embedding"]
|
|
232
|
+
assert knn_field["k"] == 10
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# AC5: Empty thread returns empty list
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
def test_search_memory_returns_empty_list_for_empty_thread():
|
|
240
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
241
|
+
_mock_embed(mock_bedrock)
|
|
242
|
+
mock_os.search.return_value = {"hits": {"hits": []}}
|
|
243
|
+
|
|
244
|
+
results = client.search_memory(thread_id="t1", query="anything")
|
|
245
|
+
|
|
246
|
+
assert results == []
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_search_memory_raises_tool_error_on_opensearch_failure():
|
|
250
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
251
|
+
_mock_embed(mock_bedrock)
|
|
252
|
+
mock_os.search.side_effect = Exception("index not found")
|
|
253
|
+
|
|
254
|
+
with pytest.raises(ToolError, match="search_memory failed"):
|
|
255
|
+
client.search_memory(thread_id="t1", query="q")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# Embedding dimension validation
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
def test_embed_raises_on_wrong_dimension():
|
|
263
|
+
client, mock_os, mock_bedrock = make_os_client()
|
|
264
|
+
# Return wrong-dimension embedding
|
|
265
|
+
mock_body = MagicMock()
|
|
266
|
+
mock_body.read.return_value = json.dumps({"embedding": [0.1] * 768}).encode()
|
|
267
|
+
mock_bedrock.invoke_model.return_value = {"body": mock_body}
|
|
268
|
+
|
|
269
|
+
with pytest.raises(ToolError, match="dimension"):
|
|
270
|
+
client.index_memory(thread_id="t1", fact="fact")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Story 2.2: S3 Blob Client
|
|
3
|
+
Run: pytest server/storage/tests/test_s3.py -v
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
PLUGIN_DIR = Path(__file__).parent.parent.parent.parent
|
|
12
|
+
sys.path.insert(0, str(PLUGIN_DIR))
|
|
13
|
+
|
|
14
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
15
|
+
from server.storage.s3 import S3Client, _s3_path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Helpers
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def make_s3_client(mock_s3=None):
|
|
23
|
+
with patch("boto3.Session") as mock_session_cls:
|
|
24
|
+
mock_session = MagicMock()
|
|
25
|
+
mock_session_cls.return_value = mock_session
|
|
26
|
+
s3_client = mock_s3 or MagicMock()
|
|
27
|
+
mock_session.client.return_value = s3_client
|
|
28
|
+
client = S3Client(bucket="agent101-blobs-123", user_id="testuser")
|
|
29
|
+
client._s3 = s3_client
|
|
30
|
+
return client, s3_client
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# AC1: put_blob stores at correct path and returns s3:// URI
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def test_put_blob_returns_s3_uri():
|
|
38
|
+
client, s3 = make_s3_client()
|
|
39
|
+
s3.put_object.return_value = {}
|
|
40
|
+
|
|
41
|
+
uri = client.put_blob(thread_id="t1", key="summary", content="hello world")
|
|
42
|
+
|
|
43
|
+
assert uri == "s3://agent101-blobs-123/testuser/threads/t1/summary"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_put_blob_calls_put_object_with_correct_key():
|
|
47
|
+
client, s3 = make_s3_client()
|
|
48
|
+
s3.put_object.return_value = {}
|
|
49
|
+
|
|
50
|
+
client.put_blob(thread_id="t1", key="msg_001", content=b"raw bytes")
|
|
51
|
+
|
|
52
|
+
s3.put_object.assert_called_once_with(
|
|
53
|
+
Bucket="agent101-blobs-123",
|
|
54
|
+
Key="testuser/threads/t1/msg_001",
|
|
55
|
+
Body=b"raw bytes",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_put_blob_encodes_string_to_bytes():
|
|
60
|
+
client, s3 = make_s3_client()
|
|
61
|
+
s3.put_object.return_value = {}
|
|
62
|
+
|
|
63
|
+
client.put_blob(thread_id="t1", key="note", content="unicode: 🎉")
|
|
64
|
+
|
|
65
|
+
call_kwargs = s3.put_object.call_args.kwargs
|
|
66
|
+
assert isinstance(call_kwargs["Body"], bytes)
|
|
67
|
+
assert "unicode: 🎉".encode("utf-8") == call_kwargs["Body"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_s3_path_embeds_thread_id():
|
|
71
|
+
path = _s3_path("u1", "thread_abc123", "summary")
|
|
72
|
+
assert path == "u1/threads/thread_abc123/summary"
|
|
73
|
+
assert "thread_abc123" in path # isolation: path encodes thread_id
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_put_blob_raises_tool_error_on_s3_failure():
|
|
77
|
+
client, s3 = make_s3_client()
|
|
78
|
+
s3.put_object.side_effect = Exception("NoSuchBucket")
|
|
79
|
+
|
|
80
|
+
with pytest.raises(ToolError, match="put_blob failed"):
|
|
81
|
+
client.put_blob(thread_id="t1", key="k", content="data")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# AC2: get_blob returns full content
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def test_get_blob_returns_content():
|
|
89
|
+
client, s3 = make_s3_client()
|
|
90
|
+
mock_body = MagicMock()
|
|
91
|
+
mock_body.read.return_value = b"stored content here"
|
|
92
|
+
s3.get_object.return_value = {"Body": mock_body}
|
|
93
|
+
|
|
94
|
+
result = client.get_blob("s3://agent101-blobs-123/testuser/threads/t1/summary")
|
|
95
|
+
|
|
96
|
+
assert result == b"stored content here"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_get_blob_parses_uri_correctly():
|
|
100
|
+
client, s3 = make_s3_client()
|
|
101
|
+
mock_body = MagicMock()
|
|
102
|
+
mock_body.read.return_value = b"data"
|
|
103
|
+
s3.get_object.return_value = {"Body": mock_body}
|
|
104
|
+
|
|
105
|
+
client.get_blob("s3://agent101-blobs-123/testuser/threads/t1/key")
|
|
106
|
+
|
|
107
|
+
s3.get_object.assert_called_once_with(
|
|
108
|
+
Bucket="agent101-blobs-123",
|
|
109
|
+
Key="testuser/threads/t1/key",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_get_blob_raises_on_invalid_scheme():
|
|
114
|
+
client, s3 = make_s3_client()
|
|
115
|
+
|
|
116
|
+
with pytest.raises(ToolError, match="Invalid S3 URI scheme"):
|
|
117
|
+
client.get_blob("https://example.com/file")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_get_blob_raises_tool_error_on_s3_failure():
|
|
121
|
+
client, s3 = make_s3_client()
|
|
122
|
+
s3.get_object.side_effect = Exception("NoSuchKey")
|
|
123
|
+
|
|
124
|
+
with pytest.raises(ToolError, match="get_blob failed"):
|
|
125
|
+
client.get_blob("s3://bucket/key")
|
|
File without changes
|