返回 CodeWhale
stock_host_tests.rs
根目录 / crates / tui / src / tools / stock_host_tests.rs
1 //! Actual Core ToolSpecs, committed pinned host and local HTTP/file fixtures.
2 //! No provider traffic. Host selection is independent of third-party Native policy.
3 use super::*;
4 use crate::extension_host::tests::node_for_tests;
5 use crate::extension_host::{ExtensionHostManager, ExtensionHostOptions, TestManagerGuard};
6 use crate::features::{Feature, Features};
7 use crate::plugins::activation::TestPolicyGuard;
8 use crate::tools::validate_data::ValidateDataTool;
9 use std::sync::Arc;
10 use wiremock::matchers::{method, path};
11 use wiremock::{Mock, MockServer, ResponseTemplate};
12
13 fn manager(node: std::path::PathBuf, home: &std::path::Path) -> Arc<ExtensionHostManager> {
14 Arc::new(ExtensionHostManager::new(ExtensionHostOptions {
15 runtime: crate::config::ExtensionHostRuntime::Node,
16 node_override: Some(node),
17 root: Some(home.join("host")),
18 ..ExtensionHostOptions::default()
19 }))
20 }
21 fn context(root: &std::path::Path, feature: Option<Feature>) -> ToolContext {
22 let mut features = Features::with_defaults();
23 if let Some(feature) = feature {
24 features.enable(feature);
25 }
26 ToolContext::new(root).with_features(features)
27 }
28 fn equal_tool_results(rust: &ToolResult, host: &ToolResult) {
29 assert_eq!(host.success, rust.success);
30 assert_eq!(host.content, rust.content, "exact user/model result bytes");
31 assert_eq!(host.metadata, rust.metadata);
32 }
33
34 #[test]
35 fn finance_and_data_host_flags_are_separate_and_default_to_rust() {
36 let defaults = Features::with_defaults();
37 assert!(!defaults.enabled(Feature::FinanceHost));
38 assert!(!defaults.enabled(Feature::DataHost));
39 assert_eq!(
40 crate::features::feature_from_key("finance_host"),
41 Some(Feature::FinanceHost)
42 );
43 assert_eq!(
44 crate::features::feature_from_key("data_host"),
45 Some(Feature::DataHost)
46 );
47 }
48
49 #[tokio::test(flavor = "current_thread")]
50 async fn real_host_data_recorded_results_match_rust_without_native_activation() {
51 let _home = crate::test_support::SealedHome::new();
52 let _policy = TestPolicyGuard::extension_host(false);
53 let Some(node) = node_for_tests("real_host_data_recorded_results") else {
54 return;
55 };
56 let home = tempfile::tempdir().unwrap();
57 let manager = manager(node, home.path());
58 let _manager = TestManagerGuard::install(Arc::clone(&manager));
59 let rust = context(home.path(), None);
60 let host = context(home.path(), Some(Feature::DataHost));
61 let cases = [
62 json!({"content":"{\"😀\":1,\"\":2,\"__proto__\":3,\"a\":4}"}),
63 json!({"content":"[1,true,null]", "format":"json"}),
64 json!({"content":"42", "format":"json"}),
65 json!({"content":"-0.0", "format":"json"}),
66 json!({"content":"title = \"hello\"\ndate = 2026-10-02\n[a]\nvalue = 2\n"}),
67 json!({"content":"{bad 😀", "format":"json"}),
68 json!({"content":"key = [", "format":"toml"}),
69 json!({"content":"neither valid JSON nor TOML"}),
70 ];
71 for input in cases {
72 let expected = ValidateDataTool
73 .execute(input.clone(), &rust)
74 .await
75 .unwrap();
76 let actual = ValidateDataTool.execute(input, &host).await.unwrap();
77 equal_tool_results(&expected, &actual);
78 }
79 // File extension must pin a failing JSON parser even if TOML could succeed.
80 std::fs::write(home.path().join("pinned.json"), "value = 1").unwrap();
81 let input = json!({"path":"pinned.json"});
82 equal_tool_results(
83 &ValidateDataTool
84 .execute(input.clone(), &rust)
85 .await
86 .unwrap(),
87 &ValidateDataTool.execute(input, &host).await.unwrap(),
88 );
89 assert!(!crate::plugins::activation::extension_host_policy_enabled());
90 manager.shutdown().await;
91 }
92
93 #[tokio::test(flavor = "current_thread")]
94 async fn real_host_finance_quote_and_chart_results_match_rust_exactly() {
95 let _home = crate::test_support::SealedHome::new();
96 let _policy = TestPolicyGuard::extension_host(false);
97 let Some(node) = node_for_tests("real_host_finance_quote_and_chart") else {
98 return;
99 };
100 let home = tempfile::tempdir().unwrap();
101 let manager = manager(node, home.path());
102 let _manager = TestManagerGuard::install(Arc::clone(&manager));
103 let server = MockServer::start().await;
104 let tool = FinanceTool::with_endpoints(
105 format!("{}/quote", server.uri()),
106 format!("{}/chart", server.uri()),
107 );
108 let rust = context(home.path(), None);
109 let host = context(home.path(), Some(Feature::FinanceHost));
110 let fixtures = [
111 (
112 json!({"quoteResponse":{"result":[{"symbol":"btc-usd","regularMarketPrice":125.0,"regularMarketPreviousClose":100.0,"regularMarketTime":9223372036854775807i64,"longName":"Long","shortName":"Short","fullExchangeName":"Full","exchange":"Low"}]}}),
113 json!({"chart":{"result":[]}}),
114 ),
115 (
116 json!({"quoteResponse":{"result":[{"symbol":"BTC-USD","regularMarketPrice":-0.0,"regularMarketPreviousClose":0.0}]}}),
117 json!({"chart":{"result":[]}}),
118 ),
119 (
120 json!({"quoteResponse":{"result":[{"symbol":"BTC-USD","regularMarketPrice":1e308,"regularMarketPreviousClose":-1e308}]}}),
121 json!({"chart":{"result":[]}}),
122 ),
123 (
124 json!({"quoteResponse":{"result":[]}}),
125 json!({"chart":{"result":[{"meta":{"symbol":"BTC-USD","regularMarketPrice":125.0,"chartPreviousClose":100.0,"previousClose":50.0,"regularMarketTime":-9223372036854775808i64,"instrumentType":"CRYPTOCURRENCY"}}],"error":null}}),
126 ),
127 ];
128 for (quote, chart) in fixtures {
129 server.reset().await;
130 Mock::given(method("GET"))
131 .and(path("/quote"))
132 .respond_with(ResponseTemplate::new(200).set_body_json(quote))
133 .mount(&server)
134 .await;
135 Mock::given(method("GET"))
136 .and(path("/chart/BTC-USD"))
137 .respond_with(ResponseTemplate::new(200).set_body_json(chart))
138 .mount(&server)
139 .await;
140 let input = json!({"ticker":"BTC","timeout_ms":60000});
141 let expected = tool.execute(input.clone(), &rust).await.unwrap();
142 let actual = tool.execute(input, &host).await.unwrap();
143 equal_tool_results(&expected, &actual);
144 }
145 manager.shutdown().await;
146 }
147
148 #[tokio::test(flavor = "current_thread")]
149 async fn real_host_finance_domain_errors_match_and_do_not_change_fallback_policy() {
150 let _home = crate::test_support::SealedHome::new();
151 let _policy = TestPolicyGuard::extension_host(false);
152 let Some(node) = node_for_tests("real_host_finance_domain_errors") else {
153 return;
154 };
155 let home = tempfile::tempdir().unwrap();
156 let manager = manager(node, home.path());
157 let _manager = TestManagerGuard::install(Arc::clone(&manager));
158 let server = MockServer::start().await;
159 let tool = FinanceTool::with_endpoints(
160 format!("{}/quote", server.uri()),
161 format!("{}/chart", server.uri()),
162 );
163 for (quote, chart) in [
164 (
165 json!({"quoteResponse":{"result":[]}}),
166 json!({"chart":{"error":{"code":"Not Found","description":"Symbol may be delisted"}}}),
167 ),
168 (
169 json!({"quoteResponse":{"result":[{"symbol":"BTC-USD"}]}}),
170 json!({"chart":{"error":{"code":"rate limit","description":"Try later"}}}),
171 ),
172 ] {
173 server.reset().await;
174 Mock::given(method("GET"))
175 .and(path("/quote"))
176 .respond_with(ResponseTemplate::new(200).set_body_json(quote))
177 .mount(&server)
178 .await;
179 Mock::given(method("GET"))
180 .and(path("/chart/BTC-USD"))
181 .respond_with(ResponseTemplate::new(200).set_body_json(chart))
182 .mount(&server)
183 .await;
184 let input = json!({"ticker":"BTC","timeout_ms":60000});
185 let expected = tool
186 .execute(input.clone(), &context(home.path(), None))
187 .await
188 .unwrap_err();
189 let actual = tool
190 .execute(input, &context(home.path(), Some(Feature::FinanceHost)))
191 .await
192 .unwrap_err();
193 assert_eq!(actual.to_string(), expected.to_string());
194 }
195 manager.shutdown().await;
196 }
197
198 #[tokio::test(flavor = "current_thread")]
199 async fn selected_host_missing_runtime_refuses_both_tools_without_rust_fallback() {
200 let _home = crate::test_support::SealedHome::new();
201 let _policy = TestPolicyGuard::extension_host(false);
202 let home = tempfile::tempdir().unwrap();
203 let manager = manager(home.path().join("absent-node"), home.path());
204 let _manager = TestManagerGuard::install(Arc::clone(&manager));
205 let data = ValidateDataTool
206 .execute(
207 json!({"content":"{}"}),
208 &context(home.path(), Some(Feature::DataHost)),
209 )
210 .await;
211 assert!(data.is_err(), "Rust validation would have succeeded");
212 let server = MockServer::start().await;
213 Mock::given(method("GET"))
214 .and(path("/quote"))
215 .respond_with(ResponseTemplate::new(200).set_body_json(
216 json!({"quoteResponse":{"result":[{"symbol":"AAPL","regularMarketPrice":1.0}]}}),
217 ))
218 .mount(&server)
219 .await;
220 Mock::given(method("GET"))
221 .and(path("/chart/AAPL"))
222 .respond_with(ResponseTemplate::new(200).set_body_json(
223 json!({"chart":{"result":[{"meta":{"symbol":"AAPL","regularMarketPrice":2.0}}]}}),
224 ))
225 .expect(0)
226 .mount(&server)
227 .await;
228 let tool = FinanceTool::with_endpoints(
229 format!("{}/quote", server.uri()),
230 format!("{}/chart", server.uri()),
231 );
232 assert!(
233 tool.execute(
234 json!({"ticker":"AAPL"}),
235 &context(home.path(), Some(Feature::FinanceHost))
236 )
237 .await
238 .is_err()
239 );
240 server.verify().await;
241 manager.shutdown().await;
242 }
243
244 #[tokio::test(flavor = "current_thread")]
245 async fn host_input_bound_and_cancel_refuse_before_admitting_transform() {
246 let _home = crate::test_support::SealedHome::new();
247 let _policy = TestPolicyGuard::extension_host(false);
248 let home = tempfile::tempdir().unwrap();
249 let host = context(home.path(), Some(Feature::DataHost));
250 assert!(matches!(
251 ValidateDataTool
252 .execute(json!({"content":"x".repeat(1024*1024+1)}), &host)
253 .await,
254 Err(ToolError::InvalidInput { .. })
255 ));
256 let manager = manager(home.path().join("absent-node"), home.path());
257 let _manager = TestManagerGuard::install(Arc::clone(&manager));
258 let cancel = tokio_util::sync::CancellationToken::new();
259 cancel.cancel();
260 let host = host.with_cancel_token(cancel);
261 let error = ValidateDataTool
262 .execute(json!({"content":"{}"}), &host)
263 .await
264 .unwrap_err();
265 assert!(error.to_string().contains("cancelled"));
266 manager.shutdown().await;
267 }
268
268 lines RUST