tep 0.11.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
data/test/test_mcp.rb
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::MCP -- Battery 5 chunk 5.1. Tool DSL + JSON-RPC dispatcher
|
|
4
|
+
# + HTTP-direct route + llms.txt. The translator-emitted classes
|
|
5
|
+
# are exercised via real HTTP against the fixture app.
|
|
6
|
+
class TestMCP < TepTest
|
|
7
|
+
app_source <<~RB
|
|
8
|
+
require 'sinatra'
|
|
9
|
+
|
|
10
|
+
# Grant capabilities via an X-Test-Cap header for the auth
|
|
11
|
+
# gating tests. No real auth provider needed for the dispatch
|
|
12
|
+
# paths -- we just override req.identity with a synthetic one
|
|
13
|
+
# so req.identity.may?(:admin) returns true on demand.
|
|
14
|
+
before do
|
|
15
|
+
if req.req_headers["x-test-cap-admin"].length > 0
|
|
16
|
+
req.identity = Tep::Identity.new(
|
|
17
|
+
"user:42", nil, [:admin])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
mcp_tool 'greet', "Say hi to someone" do
|
|
22
|
+
param :name, String, "person to greet"
|
|
23
|
+
|
|
24
|
+
on_call do |name:|
|
|
25
|
+
if name.length == 0
|
|
26
|
+
Tep::MCP.error("name required")
|
|
27
|
+
else
|
|
28
|
+
Tep::MCP.text("hello " + name)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
mcp_tool 'add', "Add two integers" do
|
|
34
|
+
param :a, Integer, "left operand"
|
|
35
|
+
param :b, Integer, "right operand"
|
|
36
|
+
|
|
37
|
+
on_call do |a:, b:|
|
|
38
|
+
Tep::MCP.text((a + b).to_s)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Capped tool -- requires :admin in the calling identity.
|
|
43
|
+
mcp_tool 'wipe_db', "Drop everything (requires :admin)", caps: [:admin] do
|
|
44
|
+
on_call do
|
|
45
|
+
Tep::MCP.text("wiped")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
mcp_resource 'server/status', "Current server status" do
|
|
50
|
+
on_read do
|
|
51
|
+
Tep::MCP.resource_text("server/status", "uptime: 42")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
mcp_resource 'server/version', "Server build version" do
|
|
56
|
+
on_read do
|
|
57
|
+
Tep::MCP.resource_text("server/version", "1.0.0-test")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
RB
|
|
61
|
+
|
|
62
|
+
# ---- HTTP-direct invocation ----
|
|
63
|
+
|
|
64
|
+
def test_http_direct_tool_call_returns_text
|
|
65
|
+
res = post("/tools/greet", "{\"name\":\"alice\"}",
|
|
66
|
+
"Content-Type" => "application/json")
|
|
67
|
+
assert_equal "200", res.code
|
|
68
|
+
assert_equal "hello alice", res.body
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_http_direct_tool_error_returns_400
|
|
72
|
+
res = post("/tools/greet", "{\"name\":\"\"}",
|
|
73
|
+
"Content-Type" => "application/json")
|
|
74
|
+
assert_equal "400", res.code
|
|
75
|
+
assert_equal "name required", res.body
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_http_direct_integer_param_round_trip
|
|
79
|
+
res = post("/tools/add", "{\"a\":2,\"b\":40}",
|
|
80
|
+
"Content-Type" => "application/json")
|
|
81
|
+
assert_equal "200", res.code
|
|
82
|
+
assert_equal "42", res.body
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ---- JSON-RPC dispatch over /mcp ----
|
|
86
|
+
|
|
87
|
+
def test_mcp_initialize_returns_server_info
|
|
88
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"}"
|
|
89
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
90
|
+
assert_equal "200", res.code
|
|
91
|
+
assert_includes res.body, "\"jsonrpc\":\"2.0\""
|
|
92
|
+
assert_includes res.body, "\"id\":1"
|
|
93
|
+
assert_includes res.body, "\"serverInfo\""
|
|
94
|
+
assert_includes res.body, "\"protocolVersion\""
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_mcp_tools_list_returns_both_tools
|
|
98
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\"}"
|
|
99
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
100
|
+
assert_equal "200", res.code
|
|
101
|
+
assert_includes res.body, "\"name\":\"greet\""
|
|
102
|
+
assert_includes res.body, "\"name\":\"add\""
|
|
103
|
+
assert_includes res.body, "\"description\":\"Say hi to someone\""
|
|
104
|
+
assert_includes res.body, "\"inputSchema\""
|
|
105
|
+
# Schema should encode integer params as JSON Schema integer type.
|
|
106
|
+
assert_includes res.body, "\"type\":\"integer\""
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_mcp_tools_call_round_trips_text_content
|
|
110
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\"," +
|
|
111
|
+
"\"params\":{\"name\":\"greet\",\"arguments\":{\"name\":\"bob\"}}}"
|
|
112
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
113
|
+
assert_equal "200", res.code
|
|
114
|
+
assert_includes res.body, "\"id\":3"
|
|
115
|
+
assert_includes res.body, "\"text\":\"hello bob\""
|
|
116
|
+
assert_includes res.body, "\"isError\":false"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_mcp_tools_call_propagates_is_error
|
|
120
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\"," +
|
|
121
|
+
"\"params\":{\"name\":\"greet\",\"arguments\":{\"name\":\"\"}}}"
|
|
122
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
123
|
+
assert_equal "200", res.code
|
|
124
|
+
assert_includes res.body, "\"text\":\"name required\""
|
|
125
|
+
assert_includes res.body, "\"isError\":true"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_mcp_tools_call_unknown_tool_returns_error_envelope
|
|
129
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\"," +
|
|
130
|
+
"\"params\":{\"name\":\"nope\",\"arguments\":{}}}"
|
|
131
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
132
|
+
assert_equal "200", res.code
|
|
133
|
+
assert_includes res.body, "\"error\""
|
|
134
|
+
assert_includes res.body, "\"code\":-32602"
|
|
135
|
+
assert_includes res.body, "unknown tool"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_mcp_unknown_method_returns_method_not_found
|
|
139
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"notreal\"}"
|
|
140
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
141
|
+
assert_equal "200", res.code
|
|
142
|
+
assert_includes res.body, "\"code\":-32601"
|
|
143
|
+
assert_includes res.body, "method not found"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# ---- caps gating (chunk 5.2) ----
|
|
147
|
+
|
|
148
|
+
def test_capped_tool_denies_anonymous_caller
|
|
149
|
+
res = post("/tools/wipe_db", "{}",
|
|
150
|
+
"Content-Type" => "application/json")
|
|
151
|
+
# Anonymous identity has empty caps, so :admin check fails.
|
|
152
|
+
# The tool returns an error Result; HTTP-direct surfaces it
|
|
153
|
+
# as 400 + the error text.
|
|
154
|
+
assert_equal "400", res.code
|
|
155
|
+
assert_includes res.body, "missing capability: admin"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_capped_tool_allows_caller_with_required_cap
|
|
159
|
+
res = post("/tools/wipe_db", "{}",
|
|
160
|
+
"Content-Type" => "application/json",
|
|
161
|
+
"X-Test-Cap-Admin" => "1")
|
|
162
|
+
assert_equal "200", res.code
|
|
163
|
+
assert_equal "wiped", res.body
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def test_capped_tool_over_mcp_returns_isError
|
|
167
|
+
# Same denial path through the JSON-RPC envelope: anonymous
|
|
168
|
+
# caller -> wipe_db -> error Result -> isError:true.
|
|
169
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/call\"," +
|
|
170
|
+
"\"params\":{\"name\":\"wipe_db\",\"arguments\":{}}}"
|
|
171
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
172
|
+
assert_equal "200", res.code
|
|
173
|
+
assert_includes res.body, "\"isError\":true"
|
|
174
|
+
assert_includes res.body, "missing capability: admin"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# ---- notifications/initialized (chunk 5.2) ----
|
|
178
|
+
|
|
179
|
+
def test_notifications_initialized_returns_204_no_body
|
|
180
|
+
body = "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}"
|
|
181
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
182
|
+
assert_equal "204", res.code
|
|
183
|
+
assert_equal "", res.body.to_s
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# ---- mcp_resource (chunk 5.3) ----
|
|
187
|
+
|
|
188
|
+
def test_http_direct_resource_read_returns_text
|
|
189
|
+
res = get("/resources/server/status")
|
|
190
|
+
assert_equal "200", res.code
|
|
191
|
+
assert_includes res["content-type"].to_s, "text/plain"
|
|
192
|
+
assert_equal "uptime: 42", res.body
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def test_mcp_initialize_advertises_resources_capability
|
|
196
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":11,\"method\":\"initialize\"}"
|
|
197
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
198
|
+
assert_equal "200", res.code
|
|
199
|
+
assert_includes res.body, "\"resources\":{}"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def test_mcp_resources_list_returns_both_resources
|
|
203
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":12,\"method\":\"resources/list\"}"
|
|
204
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
205
|
+
assert_equal "200", res.code
|
|
206
|
+
assert_includes res.body, "\"uri\":\"server/status\""
|
|
207
|
+
assert_includes res.body, "\"uri\":\"server/version\""
|
|
208
|
+
assert_includes res.body, "\"description\":\"Current server status\""
|
|
209
|
+
assert_includes res.body, "\"mimeType\":\"text/plain\""
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def test_mcp_resources_read_round_trips_content
|
|
213
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":13,\"method\":\"resources/read\"," +
|
|
214
|
+
"\"params\":{\"uri\":\"server/status\"}}"
|
|
215
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
216
|
+
assert_equal "200", res.code
|
|
217
|
+
assert_includes res.body, "\"uri\":\"server/status\""
|
|
218
|
+
assert_includes res.body, "\"mimeType\":\"text/plain\""
|
|
219
|
+
assert_includes res.body, "\"text\":\"uptime: 42\""
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def test_mcp_resources_read_unknown_uri_errors
|
|
223
|
+
body = "{\"jsonrpc\":\"2.0\",\"id\":14,\"method\":\"resources/read\"," +
|
|
224
|
+
"\"params\":{\"uri\":\"nope\"}}"
|
|
225
|
+
res = post("/mcp", body, "Content-Type" => "application/json")
|
|
226
|
+
assert_equal "200", res.code
|
|
227
|
+
assert_includes res.body, "\"code\":-32602"
|
|
228
|
+
assert_includes res.body, "unknown resource"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# ---- openapi.json (chunk 5.4) ----
|
|
232
|
+
|
|
233
|
+
def test_openapi_json_lists_tool_paths
|
|
234
|
+
res = get("/openapi.json")
|
|
235
|
+
assert_equal "200", res.code
|
|
236
|
+
assert_includes res["content-type"].to_s, "application/json"
|
|
237
|
+
assert_includes res.body, "\"openapi\":\"3.0.3\""
|
|
238
|
+
assert_includes res.body, "\"/tools/greet\""
|
|
239
|
+
assert_includes res.body, "\"/tools/add\""
|
|
240
|
+
# Integer params declared as integer in the OpenAPI schema.
|
|
241
|
+
assert_includes res.body, "\"type\":\"integer\""
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_openapi_json_lists_resource_paths
|
|
245
|
+
res = get("/openapi.json")
|
|
246
|
+
assert_includes res.body, "\"/resources/server/status\""
|
|
247
|
+
assert_includes res.body, "\"/resources/server/version\""
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# ---- llms.txt discovery ----
|
|
251
|
+
|
|
252
|
+
def test_llms_txt_lists_tools_with_descriptions
|
|
253
|
+
res = get("/llms.txt")
|
|
254
|
+
assert_equal "200", res.code
|
|
255
|
+
assert_includes res["content-type"].to_s, "text/markdown"
|
|
256
|
+
assert_includes res.body, "MCP-endpoint: /mcp"
|
|
257
|
+
assert_includes res.body, "OpenAPI: /openapi.json"
|
|
258
|
+
assert_includes res.body, "## Tools"
|
|
259
|
+
assert_includes res.body, "greet -- Say hi to someone"
|
|
260
|
+
assert_includes res.body, "add -- Add two integers"
|
|
261
|
+
assert_includes res.body, "## Resources"
|
|
262
|
+
assert_includes res.body, "server/status -- Current server status"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# send_file, configure, and __END__ inline templates -- the three small
|
|
4
|
+
# wins added on top of the v0.2 surface.
|
|
5
|
+
class TestMiscV02 < TepTest
|
|
6
|
+
app_source <<~RB
|
|
7
|
+
require 'sinatra'
|
|
8
|
+
|
|
9
|
+
configure do
|
|
10
|
+
$configured = "always"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
configure :production do
|
|
14
|
+
$configured = "prod"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
get '/configured' do
|
|
18
|
+
"configured=" + ($configured || "nil").to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
get '/file' do
|
|
22
|
+
send_file 'public/hello.txt'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
get '/inline' do
|
|
26
|
+
erb :inline_hi, locals: { who: "world" }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
__END__
|
|
30
|
+
|
|
31
|
+
@@ inline_hi
|
|
32
|
+
<h1>hi <%= locals["who"] %> from inline</h1>
|
|
33
|
+
RB
|
|
34
|
+
|
|
35
|
+
def test_configure_always_runs
|
|
36
|
+
res = get("/configured")
|
|
37
|
+
# Both the bare and :production blocks ran in dev env: "always" set first,
|
|
38
|
+
# then "prod" only if env=production. Default env is development, so the
|
|
39
|
+
# :production block is gated off and the value stays "always".
|
|
40
|
+
assert_equal "configured=always", res.body
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_send_file_streams_static
|
|
44
|
+
res = get("/file")
|
|
45
|
+
assert_equal "200", res.code
|
|
46
|
+
assert_match(/static file serving works/, res.body)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_inline_template_renders
|
|
50
|
+
res = get("/inline")
|
|
51
|
+
assert_equal "200", res.code
|
|
52
|
+
assert_match(%r{hi world from inline}, res.body)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestModular < TepTest
|
|
4
|
+
app_source <<~RB
|
|
5
|
+
require 'sinatra/base'
|
|
6
|
+
|
|
7
|
+
class Api < Sinatra::Base
|
|
8
|
+
before do
|
|
9
|
+
response.headers["X-App"] = "Api"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
get '/api/health' do
|
|
13
|
+
"ok"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Admin < Sinatra::Base
|
|
18
|
+
get '/admin/dashboard' do
|
|
19
|
+
"admin"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Api.run!
|
|
24
|
+
Admin.run!
|
|
25
|
+
RB
|
|
26
|
+
|
|
27
|
+
def test_first_app_route
|
|
28
|
+
res = get("/api/health")
|
|
29
|
+
assert_equal "200", res.code
|
|
30
|
+
assert_equal "ok", res.body
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_second_app_route
|
|
34
|
+
res = get("/admin/dashboard")
|
|
35
|
+
assert_equal "200", res.code
|
|
36
|
+
assert_equal "admin", res.body
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_modular_before_filter_ran
|
|
40
|
+
res = get("/api/health")
|
|
41
|
+
assert_equal "Api", res["x-app"]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Multiple `before do` / `after do` blocks should all run, in order.
|
|
4
|
+
class TestMultiFilters < TepTest
|
|
5
|
+
app_source <<~RB
|
|
6
|
+
require 'sinatra'
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
response.headers["X-First"] = "1"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
response.headers["X-Second"] = "2"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
after do
|
|
17
|
+
response.headers["X-After-A"] = "a"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
after do
|
|
21
|
+
response.headers["X-After-B"] = "b"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
get '/' do
|
|
25
|
+
"ok"
|
|
26
|
+
end
|
|
27
|
+
RB
|
|
28
|
+
|
|
29
|
+
def test_both_before_filters_run
|
|
30
|
+
res = get("/")
|
|
31
|
+
assert_equal "1", res["x-first"]
|
|
32
|
+
assert_equal "2", res["x-second"]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_both_after_filters_run
|
|
36
|
+
res = get("/")
|
|
37
|
+
assert_equal "a", res["x-after-a"]
|
|
38
|
+
assert_equal "b", res["x-after-b"]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep's Mustache subset (build-time AOT). Documented surface:
|
|
4
|
+
# `{{var}}` (escaped), `{{{var}}}` / `{{& var}}` (raw),
|
|
5
|
+
# `{{@ivar}}` (escaped or raw via triple-stache), `{{!comment}}`
|
|
6
|
+
# (dropped). Sections / partials / delimiter swaps are deliberately
|
|
7
|
+
# unsupported and the compiler raises at build time if reached.
|
|
8
|
+
class TestMustache < TepTest
|
|
9
|
+
app_source <<~RB
|
|
10
|
+
require 'sinatra'
|
|
11
|
+
|
|
12
|
+
set :views, '#{File.expand_path("views", __dir__)}'
|
|
13
|
+
|
|
14
|
+
get '/m/simple/:who' do
|
|
15
|
+
mustache :m_simple, locals: { name: params[:who], greeting: "hi", snippet: "<b>BOLD</b>" }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
@raw = "<i>I</i>"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
get '/m/ivars/:who/:n' do
|
|
23
|
+
@name = params[:who]
|
|
24
|
+
@count = params[:n]
|
|
25
|
+
mustache :m_ivars
|
|
26
|
+
end
|
|
27
|
+
RB
|
|
28
|
+
|
|
29
|
+
def test_simple_escaped_and_raw
|
|
30
|
+
res = get("/m/simple/alice")
|
|
31
|
+
assert_equal "200", res.code
|
|
32
|
+
assert_match(/hello, alice!/, res.body)
|
|
33
|
+
assert_match(/greeting: hi/, res.body)
|
|
34
|
+
# `{{{snippet}}}` (raw) keeps the live tag.
|
|
35
|
+
assert_match(/<p>raw html: <b>BOLD<\/b><\/p>/, res.body)
|
|
36
|
+
# comment line is dropped
|
|
37
|
+
refute_match(/this comment is dropped/, res.body)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_html_escape_dangerous_chars
|
|
41
|
+
# `<` and `>` need URL-encoding to even reach the server. The
|
|
42
|
+
# escaped `{{name}}` form must then render `<script>`,
|
|
43
|
+
# not the live tag.
|
|
44
|
+
res = get("/m/simple/%3Cscript%3E")
|
|
45
|
+
assert_match(/hello, <script>!/, res.body)
|
|
46
|
+
refute_match(/hello, <script>/, res.body)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_ivar_via_at_prefix
|
|
50
|
+
res = get("/m/ivars/bob/4")
|
|
51
|
+
assert_equal "200", res.code
|
|
52
|
+
assert_match(/hi, bob/, res.body)
|
|
53
|
+
assert_match(/visited: 4/, res.body)
|
|
54
|
+
# `{{{@raw}}}` (raw ivar) keeps the `<i>I</i>` literal
|
|
55
|
+
assert_match(/raw ivar: <i>I<\/i>/, res.body)
|
|
56
|
+
end
|
|
57
|
+
end
|