tokenade 1.0.0__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.
Files changed (363) hide show
  1. tokenade/__init__.py +84 -0
  2. tokenade/__main__.py +5 -0
  3. tokenade/cli/__init__.py +1937 -0
  4. tokenade/cli/__main__.py +5 -0
  5. tokenade/cli/advanced.py +490 -0
  6. tokenade/cli/completions.py +73 -0
  7. tokenade/cli/config.py +47 -0
  8. tokenade/cli/handlers/__init__.py +51 -0
  9. tokenade/cli/handlers/browser_ops.py +1373 -0
  10. tokenade/cli/handlers/ci.py +361 -0
  11. tokenade/cli/handlers/infrastructure.py +452 -0
  12. tokenade/cli/handlers/misc.py +222 -0
  13. tokenade/cli/handlers/session_ops.py +1263 -0
  14. tokenade/cli/management.py +82 -0
  15. tokenade/cli/output.py +176 -0
  16. tokenade/cli/proxy.py +279 -0
  17. tokenade/cli/security.py +167 -0
  18. tokenade/cli/session.py +885 -0
  19. tokenade/core/__init__.py +0 -0
  20. tokenade/core/antidetection/__init__.py +4 -0
  21. tokenade/core/antidetection/behavioral.py +140 -0
  22. tokenade/core/antidetection/cdp_cleaner.py +60 -0
  23. tokenade/core/api/__init__.py +3 -0
  24. tokenade/core/api/server.py +445 -0
  25. tokenade/core/batch/__init__.py +23 -0
  26. tokenade/core/batch/operations.py +398 -0
  27. tokenade/core/browser/__init__.py +84 -0
  28. tokenade/core/browser/battle.py +865 -0
  29. tokenade/core/browser/captcha.py +244 -0
  30. tokenade/core/browser/cdp_connection.py +1095 -0
  31. tokenade/core/browser/cloak.py +24 -0
  32. tokenade/core/browser/cloudflare.py +294 -0
  33. tokenade/core/browser/dashboard.py +195 -0
  34. tokenade/core/browser/dependencies.py +271 -0
  35. tokenade/core/browser/fingerprint.py +262 -0
  36. tokenade/core/browser/manager.py +317 -0
  37. tokenade/core/browser/patcher.py +322 -0
  38. tokenade/core/browser/profile_cloner.py +344 -0
  39. tokenade/core/browser/profiles.py +221 -0
  40. tokenade/core/browser/session_state.py +254 -0
  41. tokenade/core/browser/stealth/__init__.py +68 -0
  42. tokenade/core/browser/stealth/backend.py +101 -0
  43. tokenade/core/browser/stealth/cloak.py +258 -0
  44. tokenade/core/browser/stealth/launcher.py +593 -0
  45. tokenade/core/browser/stealth/manager.py +671 -0
  46. tokenade/core/browser/stealth.py +24 -0
  47. tokenade/core/browser/stealth_test.py +324 -0
  48. tokenade/core/browser/storage_extractor.py +231 -0
  49. tokenade/core/browser/synchronizer.py +200 -0
  50. tokenade/core/browser/tls_fingerprint.py +179 -0
  51. tokenade/core/browser/undetectable.py +16 -0
  52. tokenade/core/browser/xvfb.py +222 -0
  53. tokenade/core/cicd/__init__.py +29 -0
  54. tokenade/core/cicd/runner.py +548 -0
  55. tokenade/core/cicd/workflow_generator.py +432 -0
  56. tokenade/core/config.py +103 -0
  57. tokenade/core/crypto/__init__.py +19 -0
  58. tokenade/core/crypto/at_rest.py +178 -0
  59. tokenade/core/crypto/cookie_crypto.py +805 -0
  60. tokenade/core/crypto/encryptor.py +301 -0
  61. tokenade/core/daemon/__init__.py +4 -0
  62. tokenade/core/daemon/session_daemon.py +675 -0
  63. tokenade/core/errors.py +77 -0
  64. tokenade/core/fingerprint/__init__.py +0 -0
  65. tokenade/core/fingerprint/collectors/__init__.py +30 -0
  66. tokenade/core/fingerprint/collectors/audio.py +124 -0
  67. tokenade/core/fingerprint/collectors/base.py +66 -0
  68. tokenade/core/fingerprint/collectors/battery.py +59 -0
  69. tokenade/core/fingerprint/collectors/canvas.py +81 -0
  70. tokenade/core/fingerprint/collectors/fonts.py +52 -0
  71. tokenade/core/fingerprint/collectors/navigator.py +99 -0
  72. tokenade/core/fingerprint/collectors/plugins.py +92 -0
  73. tokenade/core/fingerprint/collectors/screen.py +81 -0
  74. tokenade/core/fingerprint/collectors/webgl.py +104 -0
  75. tokenade/core/fingerprint/collectors/webrtc.py +91 -0
  76. tokenade/core/fingerprint/injector.py +94 -0
  77. tokenade/core/fingerprint/manager.py +278 -0
  78. tokenade/core/fingerprint/stealth.py +357 -0
  79. tokenade/core/forensics/__init__.py +15 -0
  80. tokenade/core/forensics/autopsy.py +464 -0
  81. tokenade/core/importer/__init__.py +23 -0
  82. tokenade/core/importer/adb_extractor.py +259 -0
  83. tokenade/core/importer/advanced_validator.py +665 -0
  84. tokenade/core/importer/browser_discovery.py +313 -0
  85. tokenade/core/importer/chromium_forks.py +174 -0
  86. tokenade/core/importer/competitor_import.py +324 -0
  87. tokenade/core/importer/cookie_extractor.py +612 -0
  88. tokenade/core/importer/db_utils.py +66 -0
  89. tokenade/core/importer/format_exporter.py +210 -0
  90. tokenade/core/importer/format_importer.py +277 -0
  91. tokenade/core/importer/local_storage_extractor.py +271 -0
  92. tokenade/core/importer/mobile_extractor.py +263 -0
  93. tokenade/core/importer/mobile_import.py +556 -0
  94. tokenade/core/importer/plugin_export.py +321 -0
  95. tokenade/core/importer/safari_extractor.py +376 -0
  96. tokenade/core/importer/session_comparator.py +126 -0
  97. tokenade/core/importer/session_loader.py +494 -0
  98. tokenade/core/importer/session_manager.py +295 -0
  99. tokenade/core/importer/session_packager.py +492 -0
  100. tokenade/core/importer/session_refresher.py +467 -0
  101. tokenade/core/importer/session_rotation.py +147 -0
  102. tokenade/core/importer/session_rotator.py +279 -0
  103. tokenade/core/importer/session_sharer.py +713 -0
  104. tokenade/core/importer/session_sync.py +298 -0
  105. tokenade/core/importer/session_vault.py +282 -0
  106. tokenade/core/importer/site_configs.py +314 -0
  107. tokenade/core/importer/tor_extractor.py +138 -0
  108. tokenade/core/importer/validator.py +497 -0
  109. tokenade/core/injector/__init__.py +15 -0
  110. tokenade/core/injector/profile_manager.py +386 -0
  111. tokenade/core/integration/__init__.py +32 -0
  112. tokenade/core/integration/container_orchestrator.py +512 -0
  113. tokenade/core/integration/docker_manager.py +349 -0
  114. tokenade/core/integration/fleet.py +464 -0
  115. tokenade/core/integration/kubernetes.py +393 -0
  116. tokenade/core/integration/plugin_browser.py +251 -0
  117. tokenade/core/integration/plugin_loader.py +395 -0
  118. tokenade/core/integration/plugin_registry.py +620 -0
  119. tokenade/core/integration/plugin_search.py +186 -0
  120. tokenade/core/integration/plugin_testing.py +422 -0
  121. tokenade/core/integration/plugin_verifier.py +193 -0
  122. tokenade/core/integration/rating_sync.py +286 -0
  123. tokenade/core/integration/webhooks.py +219 -0
  124. tokenade/core/logging/__init__.py +6 -0
  125. tokenade/core/logging/structured.py +182 -0
  126. tokenade/core/monitoring/__init__.py +0 -0
  127. tokenade/core/monitoring/analytics.py +194 -0
  128. tokenade/core/monitoring/session_monitor.py +479 -0
  129. tokenade/core/proxy/__init__.py +42 -0
  130. tokenade/core/proxy/cdp_api.py +97 -0
  131. tokenade/core/proxy/cdp_gui.py +349 -0
  132. tokenade/core/proxy/cdp_injection.py +376 -0
  133. tokenade/core/proxy/cdp_proxy.py +808 -0
  134. tokenade/core/proxy/cdp_routing.py +419 -0
  135. tokenade/core/proxy/cdp_stealth.py +229 -0
  136. tokenade/core/proxy/extension_bridge.py +132 -0
  137. tokenade/core/proxy/forward_proxy.py +317 -0
  138. tokenade/core/proxy/manager.py +275 -0
  139. tokenade/core/proxy/multi_site_proxy.py +255 -0
  140. tokenade/core/proxy/residential.py +305 -0
  141. tokenade/core/proxy/rotation.py +365 -0
  142. tokenade/core/proxy/server.py +422 -0
  143. tokenade/core/proxy/server_gui.py +214 -0
  144. tokenade/core/proxy/server_routing.py +128 -0
  145. tokenade/core/proxy/server_utils.py +253 -0
  146. tokenade/core/refresh/__init__.py +67 -0
  147. tokenade/core/refresh/batch_refresh.py +319 -0
  148. tokenade/core/refresh/encrypted_refresh.py +342 -0
  149. tokenade/core/refresh/health_checker.py +355 -0
  150. tokenade/core/refresh/health_reporter.py +379 -0
  151. tokenade/core/refresh/health_scorer.py +292 -0
  152. tokenade/core/refresh/oauth_refresh.py +611 -0
  153. tokenade/core/refresh/session_validator.py +367 -0
  154. tokenade/core/runtime/__init__.py +30 -0
  155. tokenade/core/runtime/engine.py +783 -0
  156. tokenade/core/runtime/tls_matcher.py +282 -0
  157. tokenade/core/security/__init__.py +27 -0
  158. tokenade/core/security/audit.py +577 -0
  159. tokenade/core/security/credentials.py +417 -0
  160. tokenade/core/storage/__init__.py +1 -0
  161. tokenade/core/storage/session_versions.py +261 -0
  162. tokenade/core/utils/__init__.py +0 -0
  163. tokenade/core/utils/performance.py +446 -0
  164. tokenade/handlers/__init__.py +42 -0
  165. tokenade/handlers/base.py +288 -0
  166. tokenade/handlers/generic_oauth.py +593 -0
  167. tokenade/handlers/github.py +347 -0
  168. tokenade/handlers/google.py +301 -0
  169. tokenade/handlers/resolve.py +49 -0
  170. tokenade/plugin/__init__.py +62 -0
  171. tokenade/plugin/api.py +139 -0
  172. tokenade/plugin/base.py +681 -0
  173. tokenade/plugin/oauth2/__init__.py +4 -0
  174. tokenade/plugin/oauth2/plugin.py +282 -0
  175. tokenade/sdk/__init__.py +257 -0
  176. tokenade/tests/__init__.py +0 -0
  177. tokenade/tests/portability.py +308 -0
  178. tokenade/tests/test_accounts.py +286 -0
  179. tokenade/tests/test_advanced_validator.py +209 -0
  180. tokenade/tests/test_advanced_validator_edge_cases.py +1115 -0
  181. tokenade/tests/test_analytics.py +222 -0
  182. tokenade/tests/test_antidetection.py +145 -0
  183. tokenade/tests/test_api_monitor_endpoints.py +204 -0
  184. tokenade/tests/test_api_monitoring.py +26 -0
  185. tokenade/tests/test_api_sdk.py +262 -0
  186. tokenade/tests/test_api_server_comprehensive.py +400 -0
  187. tokenade/tests/test_api_server_coverage.py +232 -0
  188. tokenade/tests/test_at_rest_encryption.py +210 -0
  189. tokenade/tests/test_audit_coverage.py +560 -0
  190. tokenade/tests/test_autopsy.py +375 -0
  191. tokenade/tests/test_base_handler_coverage.py +365 -0
  192. tokenade/tests/test_batch_operations.py +311 -0
  193. tokenade/tests/test_batch_operations_coverage.py +415 -0
  194. tokenade/tests/test_batch_refresh.py +175 -0
  195. tokenade/tests/test_battle.py +274 -0
  196. tokenade/tests/test_benchmarks.py +209 -0
  197. tokenade/tests/test_binary_patcher.py +419 -0
  198. tokenade/tests/test_browser.py +367 -0
  199. tokenade/tests/test_browser_discovery.py +58 -0
  200. tokenade/tests/test_browser_discovery_coverage.py +516 -0
  201. tokenade/tests/test_browser_improvements.py +277 -0
  202. tokenade/tests/test_browser_manager_coverage.py +764 -0
  203. tokenade/tests/test_browser_support.py +728 -0
  204. tokenade/tests/test_cdp_gui_coverage.py +573 -0
  205. tokenade/tests/test_cdp_injection_coverage.py +1056 -0
  206. tokenade/tests/test_cdp_injection_edge_cases.py +396 -0
  207. tokenade/tests/test_cdp_proxy_coverage.py +534 -0
  208. tokenade/tests/test_cdp_routing_coverage.py +1162 -0
  209. tokenade/tests/test_cdp_stealth_urls.py +50 -0
  210. tokenade/tests/test_chromium_forks_coverage.py +411 -0
  211. tokenade/tests/test_ci_runner.py +500 -0
  212. tokenade/tests/test_cicd.py +122 -0
  213. tokenade/tests/test_cli.py +200 -0
  214. tokenade/tests/test_cli_advanced.py +943 -0
  215. tokenade/tests/test_cli_helpers.py +99 -0
  216. tokenade/tests/test_cli_init_coverage.py +651 -0
  217. tokenade/tests/test_cli_integration.py +205 -0
  218. tokenade/tests/test_cli_management.py +231 -0
  219. tokenade/tests/test_cli_management_coverage.py +419 -0
  220. tokenade/tests/test_cli_monitor.py +201 -0
  221. tokenade/tests/test_cli_output.py +219 -0
  222. tokenade/tests/test_cli_proxy.py +1004 -0
  223. tokenade/tests/test_cli_refactor.py +454 -0
  224. tokenade/tests/test_cli_security_coverage.py +306 -0
  225. tokenade/tests/test_cli_security_proxy.py +242 -0
  226. tokenade/tests/test_cli_session.py +1168 -0
  227. tokenade/tests/test_cloak.py +375 -0
  228. tokenade/tests/test_cloudflare_akamai.py +384 -0
  229. tokenade/tests/test_collectors.py +283 -0
  230. tokenade/tests/test_comprehensive.py +311 -0
  231. tokenade/tests/test_config_and_cdp_modules.py +610 -0
  232. tokenade/tests/test_container_orchestration.py +488 -0
  233. tokenade/tests/test_cookie_crypto.py +835 -0
  234. tokenade/tests/test_cookie_extractor.py +1088 -0
  235. tokenade/tests/test_credentials_keyring_errors.py +68 -0
  236. tokenade/tests/test_crypto.py +188 -0
  237. tokenade/tests/test_custom_errors.py +100 -0
  238. tokenade/tests/test_daemon.py +430 -0
  239. tokenade/tests/test_db_utils.py +93 -0
  240. tokenade/tests/test_db_utils_coverage.py +218 -0
  241. tokenade/tests/test_docker_manager.py +587 -0
  242. tokenade/tests/test_e2e_headless.py +207 -0
  243. tokenade/tests/test_encrypted_refresh.py +169 -0
  244. tokenade/tests/test_encryptor.py +323 -0
  245. tokenade/tests/test_encryptor_coverage.py +339 -0
  246. tokenade/tests/test_encryptor_edge.py +66 -0
  247. tokenade/tests/test_engine_coverage.py +748 -0
  248. tokenade/tests/test_enterprise.py +819 -0
  249. tokenade/tests/test_errors.py +49 -0
  250. tokenade/tests/test_extension_bridge_coverage.py +215 -0
  251. tokenade/tests/test_fingerprint.py +66 -0
  252. tokenade/tests/test_fingerprint_collectors.py +397 -0
  253. tokenade/tests/test_fingerprint_collectors_coverage.py +298 -0
  254. tokenade/tests/test_fingerprint_consistency.py +202 -0
  255. tokenade/tests/test_fingerprint_injector.py +308 -0
  256. tokenade/tests/test_fingerprint_manager.py +580 -0
  257. tokenade/tests/test_fleet.py +398 -0
  258. tokenade/tests/test_format_export.py +414 -0
  259. tokenade/tests/test_format_importer_and_helpers.py +656 -0
  260. tokenade/tests/test_forward_proxy.py +52 -0
  261. tokenade/tests/test_forward_proxy_coverage.py +725 -0
  262. tokenade/tests/test_generic_oauth_coverage.py +576 -0
  263. tokenade/tests/test_github_handler_coverage.py +342 -0
  264. tokenade/tests/test_google_handler_coverage.py +317 -0
  265. tokenade/tests/test_handler_resolve.py +32 -0
  266. tokenade/tests/test_handlers.py +521 -0
  267. tokenade/tests/test_health_checker_coverage.py +451 -0
  268. tokenade/tests/test_health_reporter.py +287 -0
  269. tokenade/tests/test_health_scorer.py +537 -0
  270. tokenade/tests/test_importer_integration.py +290 -0
  271. tokenade/tests/test_integration.py +662 -0
  272. tokenade/tests/test_kubernetes.py +484 -0
  273. tokenade/tests/test_local_storage.py +56 -0
  274. tokenade/tests/test_local_storage_extractor.py +174 -0
  275. tokenade/tests/test_local_storage_extractor_coverage.py +291 -0
  276. tokenade/tests/test_local_storage_extractor_plyvel.py +400 -0
  277. tokenade/tests/test_mac_crypto.py +74 -0
  278. tokenade/tests/test_mobile_extractor_coverage.py +397 -0
  279. tokenade/tests/test_mobile_import.py +240 -0
  280. tokenade/tests/test_monitoring.py +92 -0
  281. tokenade/tests/test_multi_site_proxy.py +51 -0
  282. tokenade/tests/test_multi_site_proxy_coverage.py +408 -0
  283. tokenade/tests/test_mutmut_killers.py +122 -0
  284. tokenade/tests/test_new_plugins.py +295 -0
  285. tokenade/tests/test_oauth_refresh.py +412 -0
  286. tokenade/tests/test_official_plugin_contracts.py +52 -0
  287. tokenade/tests/test_performance.py +294 -0
  288. tokenade/tests/test_performance_coverage.py +392 -0
  289. tokenade/tests/test_phase80.py +266 -0
  290. tokenade/tests/test_playwright_e2e.py +663 -0
  291. tokenade/tests/test_plugin_api.py +347 -0
  292. tokenade/tests/test_plugin_api_v11.py +357 -0
  293. tokenade/tests/test_plugin_loader.py +297 -0
  294. tokenade/tests/test_plugin_loader_coverage.py +254 -0
  295. tokenade/tests/test_plugin_marketplace.py +838 -0
  296. tokenade/tests/test_plugin_registry.py +447 -0
  297. tokenade/tests/test_plugin_system.py +339 -0
  298. tokenade/tests/test_plugin_types.py +358 -0
  299. tokenade/tests/test_profile_cloner.py +226 -0
  300. tokenade/tests/test_profile_management.py +280 -0
  301. tokenade/tests/test_profile_manager.py +338 -0
  302. tokenade/tests/test_profile_manager_coverage.py +400 -0
  303. tokenade/tests/test_profile_manager_errors.py +397 -0
  304. tokenade/tests/test_property_based.py +214 -0
  305. tokenade/tests/test_proxy.py +652 -0
  306. tokenade/tests/test_proxy_config.py +86 -0
  307. tokenade/tests/test_proxy_gui.py +80 -0
  308. tokenade/tests/test_proxy_manager.py +218 -0
  309. tokenade/tests/test_proxy_modules.py +197 -0
  310. tokenade/tests/test_proxy_rotation.py +584 -0
  311. tokenade/tests/test_rating_sync.py +156 -0
  312. tokenade/tests/test_refresh_browser.py +511 -0
  313. tokenade/tests/test_runtime.py +474 -0
  314. tokenade/tests/test_safari_extractor_coverage.py +538 -0
  315. tokenade/tests/test_sdk_coverage.py +429 -0
  316. tokenade/tests/test_security.py +349 -0
  317. tokenade/tests/test_server_coverage.py +696 -0
  318. tokenade/tests/test_server_routing_coverage.py +349 -0
  319. tokenade/tests/test_server_utils_coverage.py +485 -0
  320. tokenade/tests/test_session_comparator.py +110 -0
  321. tokenade/tests/test_session_loader.py +52 -0
  322. tokenade/tests/test_session_loader_coverage.py +621 -0
  323. tokenade/tests/test_session_manager.py +135 -0
  324. tokenade/tests/test_session_monitor_coverage.py +297 -0
  325. tokenade/tests/test_session_monitor_enhanced.py +618 -0
  326. tokenade/tests/test_session_packager.py +104 -0
  327. tokenade/tests/test_session_packager_coverage.py +466 -0
  328. tokenade/tests/test_session_refresher.py +543 -0
  329. tokenade/tests/test_session_rotation.py +408 -0
  330. tokenade/tests/test_session_rotator.py +377 -0
  331. tokenade/tests/test_session_sharer.py +591 -0
  332. tokenade/tests/test_session_sharer_qr_versions.py +272 -0
  333. tokenade/tests/test_session_sync.py +598 -0
  334. tokenade/tests/test_session_sync_coverage.py +302 -0
  335. tokenade/tests/test_session_validator.py +204 -0
  336. tokenade/tests/test_session_vault.py +418 -0
  337. tokenade/tests/test_session_versions.py +262 -0
  338. tokenade/tests/test_share_improvements.py +181 -0
  339. tokenade/tests/test_site_configs.py +92 -0
  340. tokenade/tests/test_site_filter.py +65 -0
  341. tokenade/tests/test_site_handlers.py +318 -0
  342. tokenade/tests/test_ssrf.py +54 -0
  343. tokenade/tests/test_stealth.py +147 -0
  344. tokenade/tests/test_stealth_enhanced.py +444 -0
  345. tokenade/tests/test_stealth_validation.py +217 -0
  346. tokenade/tests/test_structured_logging.py +243 -0
  347. tokenade/tests/test_sync_import.py +302 -0
  348. tokenade/tests/test_tls.py +39 -0
  349. tokenade/tests/test_tls_matcher.py +133 -0
  350. tokenade/tests/test_tls_matcher_coverage.py +310 -0
  351. tokenade/tests/test_tor_extractor_coverage.py +141 -0
  352. tokenade/tests/test_tui.py +259 -0
  353. tokenade/tests/test_undetectable.py +176 -0
  354. tokenade/tests/test_validator_coverage.py +582 -0
  355. tokenade/tests/test_webhooks.py +290 -0
  356. tokenade/tests/test_xvfb.py +172 -0
  357. tokenade/tui/__init__.py +7 -0
  358. tokenade/tui/app.py +1145 -0
  359. tokenade-1.0.0.dist-info/METADATA +831 -0
  360. tokenade-1.0.0.dist-info/RECORD +363 -0
  361. tokenade-1.0.0.dist-info/WHEEL +5 -0
  362. tokenade-1.0.0.dist-info/entry_points.txt +2 -0
  363. tokenade-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,651 @@
1
+ """Tests for uncovered lines in tokenade/cli/__init__.py (lines 18-55, 60-126, 131-186, 528-540)."""
2
+ import unittest
3
+ from io import StringIO
4
+ from unittest.mock import MagicMock, patch
5
+
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # cmd_config (lines 18-55)
9
+ # ---------------------------------------------------------------------------
10
+ class TestCmdConfig(unittest.TestCase):
11
+ """Test the cmd_config function."""
12
+
13
+ def _make_args(self, config_command, key=None, value=None):
14
+ args = MagicMock()
15
+ args.config_command = config_command
16
+ args.key = key
17
+ args.value = value
18
+ return args
19
+
20
+ @patch("tokenade.core.config.load_config")
21
+ def test_path(self, mock_load):
22
+ mock_config = MagicMock()
23
+ mock_config.config_path = "/tmp/test.json"
24
+ mock_load.return_value = mock_config
25
+
26
+ from tokenade.cli import cmd_config
27
+ args = self._make_args("path")
28
+
29
+ with patch("builtins.print") as mock_print:
30
+ cmd_config(args)
31
+ mock_print.assert_called_once_with("/tmp/test.json")
32
+
33
+ @patch("tokenade.core.config.load_config")
34
+ def test_show(self, mock_load):
35
+ mock_config = MagicMock()
36
+ mock_config.config_path = "/tmp/test.json"
37
+ mock_config.get.return_value = "val"
38
+ mock_load.return_value = mock_config
39
+
40
+ from tokenade.cli import cmd_config
41
+ args = self._make_args("show")
42
+
43
+ with patch("builtins.print") as mock_print, \
44
+ patch("tokenade.core.config.DEFAULTS", {"key_a": "default", "key_b": "other"}):
45
+ cmd_config(args)
46
+ self.assertTrue(mock_print.call_count >= 3)
47
+
48
+ @patch("tokenade.core.config.load_config")
49
+ def test_get_with_key(self, mock_load):
50
+ mock_config = MagicMock()
51
+ mock_config.get.return_value = "some_value"
52
+ mock_load.return_value = mock_config
53
+
54
+ from tokenade.cli import cmd_config
55
+ args = self._make_args("get", key="my_key")
56
+
57
+ with patch("builtins.print") as mock_print:
58
+ cmd_config(args)
59
+ mock_print.assert_called_once_with("some_value")
60
+
61
+ @patch("tokenade.core.config.load_config")
62
+ def test_get_without_key(self, mock_load):
63
+ mock_config = MagicMock()
64
+ mock_config.config_path = "/tmp/test.json"
65
+ mock_config.get.return_value = None
66
+ mock_load.return_value = mock_config
67
+
68
+ from tokenade.cli import cmd_config
69
+ args = self._make_args("get", key=None)
70
+
71
+ with patch("builtins.print") as mock_print:
72
+ cmd_config(args)
73
+ printed = mock_print.call_args[0][0]
74
+ self.assertIn("Usage", printed)
75
+
76
+ @patch("tokenade.core.config.load_config")
77
+ def test_get_unknown_key(self, mock_load):
78
+ mock_config = MagicMock()
79
+ mock_config.config_path = "/tmp/test.json"
80
+ mock_config.get.return_value = None
81
+ mock_load.return_value = mock_config
82
+
83
+ from tokenade.cli import cmd_config
84
+ args = self._make_args("get", key="unknown")
85
+
86
+ with patch("builtins.print") as mock_print:
87
+ cmd_config(args)
88
+ printed = mock_print.call_args[0][0]
89
+ self.assertIn("Unknown", printed)
90
+
91
+ @patch("tokenade.core.config.load_config")
92
+ def test_set_valid_key_and_value(self, mock_load):
93
+ mock_config = MagicMock()
94
+ mock_config.config_path = "/tmp/test.json"
95
+ mock_config.get.return_value = None
96
+ mock_load.return_value = mock_config
97
+
98
+ from tokenade.cli import cmd_config
99
+ args = self._make_args("set", key="my_key", value="my_value")
100
+
101
+ with patch("builtins.print") as mock_print:
102
+ cmd_config(args)
103
+ mock_config.set.assert_called_once_with("my_key", "my_value")
104
+ mock_config.save.assert_called_once()
105
+ printed = mock_print.call_args[0][0]
106
+ self.assertIn("my_key", printed)
107
+
108
+ @patch("tokenade.core.config.load_config")
109
+ def test_set_without_key(self, mock_load):
110
+ mock_config = MagicMock()
111
+ mock_config.config_path = "/tmp/test.json"
112
+ mock_config.get.return_value = None
113
+ mock_load.return_value = mock_config
114
+
115
+ from tokenade.cli import cmd_config
116
+ args = self._make_args("set", key=None, value=None)
117
+
118
+ with patch("builtins.print") as mock_print:
119
+ cmd_config(args)
120
+ printed = mock_print.call_args[0][0]
121
+ self.assertIn("Usage", printed)
122
+
123
+ @patch("tokenade.core.config.load_config")
124
+ def test_set_bool_true(self, mock_load):
125
+ mock_config = MagicMock()
126
+ mock_config.config_path = "/tmp/test.json"
127
+ mock_config.get.return_value = None
128
+ mock_load.return_value = mock_config
129
+
130
+ from tokenade.cli import cmd_config
131
+ args = self._make_args("set", key="flag", value="true")
132
+
133
+ with patch("builtins.print"):
134
+ cmd_config(args)
135
+ mock_config.set.assert_called_once_with("flag", True)
136
+ mock_config.save.assert_called_once()
137
+
138
+ @patch("tokenade.core.config.load_config")
139
+ def test_set_bool_false(self, mock_load):
140
+ mock_config = MagicMock()
141
+ mock_config.config_path = "/tmp/test.json"
142
+ mock_config.get.return_value = None
143
+ mock_load.return_value = mock_config
144
+
145
+ from tokenade.cli import cmd_config
146
+ args = self._make_args("set", key="flag", value="false")
147
+
148
+ with patch("builtins.print"):
149
+ cmd_config(args)
150
+ mock_config.set.assert_called_once_with("flag", False)
151
+
152
+ @patch("tokenade.core.config.load_config")
153
+ def test_set_int_value(self, mock_load):
154
+ mock_config = MagicMock()
155
+ mock_config.config_path = "/tmp/test.json"
156
+ mock_config.get.return_value = None
157
+ mock_load.return_value = mock_config
158
+
159
+ from tokenade.cli import cmd_config
160
+ args = self._make_args("set", key="port", value="8080")
161
+
162
+ with patch("builtins.print"):
163
+ cmd_config(args)
164
+ mock_config.set.assert_called_once_with("port", 8080)
165
+
166
+ @patch("tokenade.core.config.load_config")
167
+ def test_set_string_value(self, mock_load):
168
+ mock_config = MagicMock()
169
+ mock_config.config_path = "/tmp/test.json"
170
+ mock_config.get.return_value = None
171
+ mock_load.return_value = mock_config
172
+
173
+ from tokenade.cli import cmd_config
174
+ args = self._make_args("set", key="name", value="hello_world")
175
+
176
+ with patch("builtins.print"):
177
+ cmd_config(args)
178
+ mock_config.set.assert_called_once_with("name", "hello_world")
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # cmd_completion (lines 60-126)
183
+ # ---------------------------------------------------------------------------
184
+ class TestCmdCompletion(unittest.TestCase):
185
+ """Test the cmd_completion function."""
186
+
187
+ def _make_args(self, shell):
188
+ args = MagicMock()
189
+ args.shell = shell
190
+ return args
191
+
192
+ def test_bash(self):
193
+ from tokenade.cli import cmd_completion
194
+ args = self._make_args("bash")
195
+ out = StringIO()
196
+ with patch("sys.stdout", out):
197
+ cmd_completion(args)
198
+ self.assertIn("complete -F _tokenade tokenade", out.getvalue())
199
+
200
+ def test_zsh(self):
201
+ from tokenade.cli import cmd_completion
202
+ args = self._make_args("zsh")
203
+ out = StringIO()
204
+ with patch("sys.stdout", out):
205
+ cmd_completion(args)
206
+ self.assertIn("compdef _tokenade tokenade", out.getvalue())
207
+
208
+ def test_fish(self):
209
+ from tokenade.cli import cmd_completion
210
+ args = self._make_args("fish")
211
+ out = StringIO()
212
+ with patch("sys.stdout", out):
213
+ cmd_completion(args)
214
+ self.assertIn("complete -c tokenade", out.getvalue())
215
+
216
+ def test_unsupported_shell_exits(self):
217
+ from tokenade.cli import cmd_completion
218
+ args = self._make_args("powershell")
219
+ with self.assertRaises(SystemExit) as cm:
220
+ cmd_completion(args)
221
+ self.assertEqual(cm.exception.code, 1)
222
+
223
+ def test_unsupported_shell_message(self):
224
+ from tokenade.cli import cmd_completion
225
+ args = self._make_args("powershell")
226
+ out = StringIO()
227
+ with patch("sys.stdout", out), self.assertRaises(SystemExit):
228
+ cmd_completion(args)
229
+ self.assertIn("Unsupported shell", out.getvalue())
230
+
231
+
232
+ # ---------------------------------------------------------------------------
233
+ # cmd_plugin (lines 131-186)
234
+ # ---------------------------------------------------------------------------
235
+ class TestCmdPlugin(unittest.TestCase):
236
+ """Test the cmd_plugin function."""
237
+
238
+ def _make_args(self, plugin_command, name=None, available=False):
239
+ args = MagicMock()
240
+ args.plugin_command = plugin_command
241
+ args.name = name
242
+ args.available = available
243
+ return args
244
+
245
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
246
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
247
+ def test_list_installed(self, MockReg, MockLoader):
248
+ mock_loader = MagicMock()
249
+ mock_loader.discover.return_value = [
250
+ {"name": "p1", "version": "1.0", "type": "ext", "description": "A plugin"},
251
+ ]
252
+ MockLoader.return_value = mock_loader
253
+
254
+ from tokenade.cli import cmd_plugin
255
+ args = self._make_args("list", available=False)
256
+
257
+ out = StringIO()
258
+ with patch("sys.stdout", out):
259
+ cmd_plugin(args)
260
+ self.assertIn("Installed plugins", out.getvalue())
261
+ self.assertIn("p1", out.getvalue())
262
+
263
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
264
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
265
+ def test_list_installed_empty(self, MockReg, MockLoader):
266
+ mock_loader = MagicMock()
267
+ mock_loader.discover.return_value = []
268
+ MockLoader.return_value = mock_loader
269
+
270
+ from tokenade.cli import cmd_plugin
271
+ args = self._make_args("list", available=False)
272
+
273
+ out = StringIO()
274
+ with patch("sys.stdout", out):
275
+ cmd_plugin(args)
276
+ self.assertIn("No plugins installed", out.getvalue())
277
+
278
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
279
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
280
+ def test_list_available(self, MockReg, MockLoader):
281
+ mock_registry = MagicMock()
282
+ mock_registry.search.return_value = [
283
+ {"name": "r1", "version": "2.0", "description": "Registry plugin"},
284
+ ]
285
+ MockReg.return_value = mock_registry
286
+
287
+ from tokenade.cli import cmd_plugin
288
+ args = self._make_args("list", available=True)
289
+
290
+ out = StringIO()
291
+ with patch("sys.stdout", out):
292
+ cmd_plugin(args)
293
+ self.assertIn("Available plugins", out.getvalue())
294
+ self.assertIn("r1", out.getvalue())
295
+
296
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
297
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
298
+ def test_list_available_empty(self, MockReg, MockLoader):
299
+ mock_registry = MagicMock()
300
+ mock_registry.search.return_value = []
301
+ MockReg.return_value = mock_registry
302
+
303
+ from tokenade.cli import cmd_plugin
304
+ args = self._make_args("list", available=True)
305
+
306
+ out = StringIO()
307
+ with patch("sys.stdout", out):
308
+ cmd_plugin(args)
309
+ self.assertIn("No plugins found", out.getvalue())
310
+
311
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
312
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
313
+ def test_install_success(self, MockReg, MockLoader):
314
+ mock_registry = MagicMock()
315
+ mock_registry.install.return_value = True
316
+ MockReg.return_value = mock_registry
317
+
318
+ from tokenade.cli import cmd_plugin
319
+ args = self._make_args("install", name="my_plugin")
320
+
321
+ out = StringIO()
322
+ with patch("sys.stdout", out):
323
+ cmd_plugin(args)
324
+ mock_registry.install.assert_called_once_with("my_plugin")
325
+ self.assertIn("installed successfully", out.getvalue())
326
+
327
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
328
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
329
+ def test_install_failure(self, MockReg, MockLoader):
330
+ mock_registry = MagicMock()
331
+ mock_registry.install.return_value = False
332
+ MockReg.return_value = mock_registry
333
+
334
+ from tokenade.cli import cmd_plugin
335
+ args = self._make_args("install", name="my_plugin")
336
+
337
+ out = StringIO()
338
+ with patch("sys.stdout", out):
339
+ cmd_plugin(args)
340
+ self.assertIn("Failed to install", out.getvalue())
341
+
342
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
343
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
344
+ def test_uninstall_success(self, MockReg, MockLoader):
345
+ mock_registry = MagicMock()
346
+ mock_registry.uninstall.return_value = True
347
+ MockReg.return_value = mock_registry
348
+
349
+ from tokenade.cli import cmd_plugin
350
+ args = self._make_args("uninstall", name="my_plugin")
351
+
352
+ out = StringIO()
353
+ with patch("sys.stdout", out):
354
+ cmd_plugin(args)
355
+ mock_registry.uninstall.assert_called_once_with("my_plugin")
356
+ self.assertIn("uninstalled successfully", out.getvalue())
357
+
358
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
359
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
360
+ def test_uninstall_failure(self, MockReg, MockLoader):
361
+ mock_registry = MagicMock()
362
+ mock_registry.uninstall.return_value = False
363
+ MockReg.return_value = mock_registry
364
+
365
+ from tokenade.cli import cmd_plugin
366
+ args = self._make_args("uninstall", name="my_plugin")
367
+
368
+ out = StringIO()
369
+ with patch("sys.stdout", out):
370
+ cmd_plugin(args)
371
+ self.assertIn("Failed to uninstall", out.getvalue())
372
+
373
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
374
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
375
+ def test_info_found(self, MockReg, MockLoader):
376
+ mock_loader = MagicMock()
377
+ mock_loader.discover.return_value = [
378
+ {
379
+ "name": "my_plugin",
380
+ "version": "1.0",
381
+ "type": "ext",
382
+ "author": "Test Author",
383
+ "description": "A test plugin",
384
+ "dependencies": ["dep1", "dep2"],
385
+ },
386
+ ]
387
+ MockLoader.return_value = mock_loader
388
+
389
+ from tokenade.cli import cmd_plugin
390
+ args = self._make_args("info", name="my_plugin")
391
+
392
+ out = StringIO()
393
+ with patch("sys.stdout", out):
394
+ cmd_plugin(args)
395
+ self.assertIn("Plugin: my_plugin", out.getvalue())
396
+ self.assertIn("1.0", out.getvalue())
397
+ self.assertIn("dep1", out.getvalue())
398
+
399
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
400
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
401
+ def test_info_not_found(self, MockReg, MockLoader):
402
+ mock_loader = MagicMock()
403
+ mock_loader.discover.return_value = []
404
+ MockLoader.return_value = mock_loader
405
+ mock_reg = MagicMock()
406
+ mock_reg.get_plugin_details.return_value = None
407
+ MockReg.return_value = mock_reg
408
+
409
+ from tokenade.cli import cmd_plugin
410
+ args = self._make_args("info", name="nonexistent")
411
+
412
+ out = StringIO()
413
+ with patch("sys.stdout", out):
414
+ cmd_plugin(args)
415
+ self.assertIn("not found", out.getvalue())
416
+
417
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
418
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
419
+ def test_info_no_dependencies(self, MockReg, MockLoader):
420
+ mock_loader = MagicMock()
421
+ mock_loader.discover.return_value = [
422
+ {
423
+ "name": "simple_plugin",
424
+ "version": "1.0",
425
+ "type": "ext",
426
+ "author": "Author",
427
+ "description": "Simple",
428
+ },
429
+ ]
430
+ MockLoader.return_value = mock_loader
431
+
432
+ from tokenade.cli import cmd_plugin
433
+ args = self._make_args("info", name="simple_plugin")
434
+
435
+ out = StringIO()
436
+ with patch("sys.stdout", out):
437
+ cmd_plugin(args)
438
+ self.assertIn("simple_plugin", out.getvalue())
439
+ self.assertNotIn("Dependencies", out.getvalue())
440
+
441
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
442
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
443
+ def test_else_branch(self, MockReg, MockLoader):
444
+ from tokenade.cli import cmd_plugin
445
+ args = self._make_args("invalid_command")
446
+
447
+ out = StringIO()
448
+ with patch("sys.stdout", out):
449
+ cmd_plugin(args)
450
+ self.assertIn("Usage", out.getvalue())
451
+
452
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
453
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
454
+ def test_enable_success(self, MockReg, MockLoader):
455
+ mock_loader = MagicMock()
456
+ mock_loader.enable.return_value = True
457
+ MockLoader.return_value = mock_loader
458
+
459
+ from tokenade.cli import cmd_plugin
460
+ args = self._make_args("enable", name="my_plugin")
461
+
462
+ out = StringIO()
463
+ with patch("sys.stdout", out):
464
+ cmd_plugin(args)
465
+ mock_loader.enable.assert_called_once_with("my_plugin")
466
+ self.assertIn("enabled", out.getvalue())
467
+
468
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
469
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
470
+ def test_enable_not_found(self, MockReg, MockLoader):
471
+ mock_loader = MagicMock()
472
+ mock_loader.enable.return_value = False
473
+ MockLoader.return_value = mock_loader
474
+
475
+ from tokenade.cli import cmd_plugin
476
+ args = self._make_args("enable", name="nonexistent")
477
+
478
+ out = StringIO()
479
+ with patch("sys.stdout", out):
480
+ cmd_plugin(args)
481
+ self.assertIn("not found", out.getvalue())
482
+
483
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
484
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
485
+ def test_disable_success(self, MockReg, MockLoader):
486
+ mock_loader = MagicMock()
487
+ mock_loader.disable.return_value = True
488
+ MockLoader.return_value = mock_loader
489
+
490
+ from tokenade.cli import cmd_plugin
491
+ args = self._make_args("disable", name="my_plugin")
492
+
493
+ out = StringIO()
494
+ with patch("sys.stdout", out):
495
+ cmd_plugin(args)
496
+ mock_loader.disable.assert_called_once_with("my_plugin")
497
+ self.assertIn("disabled", out.getvalue())
498
+
499
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
500
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
501
+ def test_disable_not_found(self, MockReg, MockLoader):
502
+ mock_loader = MagicMock()
503
+ mock_loader.disable.return_value = False
504
+ MockLoader.return_value = mock_loader
505
+
506
+ from tokenade.cli import cmd_plugin
507
+ args = self._make_args("disable", name="nonexistent")
508
+
509
+ out = StringIO()
510
+ with patch("sys.stdout", out):
511
+ cmd_plugin(args)
512
+ self.assertIn("not found", out.getvalue())
513
+
514
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
515
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
516
+ def test_reload_success(self, MockReg, MockLoader):
517
+ mock_loader = MagicMock()
518
+ mock_plugin = MagicMock()
519
+ mock_plugin.version = "1.0"
520
+ mock_loader.reload.return_value = mock_plugin
521
+ MockLoader.return_value = mock_loader
522
+
523
+ from tokenade.cli import cmd_plugin
524
+ args = self._make_args("reload", name="my_plugin")
525
+
526
+ out = StringIO()
527
+ with patch("sys.stdout", out):
528
+ cmd_plugin(args)
529
+ self.assertIn("reloaded", out.getvalue())
530
+
531
+ @patch("tokenade.core.integration.plugin_loader.PluginLoader")
532
+ @patch("tokenade.core.integration.plugin_registry.PluginRegistry")
533
+ def test_reload_failure(self, MockReg, MockLoader):
534
+ mock_loader = MagicMock()
535
+ mock_loader.reload.return_value = None
536
+ MockLoader.return_value = mock_loader
537
+
538
+ from tokenade.cli import cmd_plugin
539
+ args = self._make_args("reload", name="nonexistent")
540
+
541
+ out = StringIO()
542
+ with patch("sys.stdout", out):
543
+ cmd_plugin(args)
544
+ self.assertIn("Failed", out.getvalue())
545
+
546
+
547
+ # ---------------------------------------------------------------------------
548
+ # main() error handling (lines 528-540)
549
+ # ---------------------------------------------------------------------------
550
+ class TestMainErrorHandling(unittest.TestCase):
551
+ """Test the main() function error handling branches (lines 528-540)."""
552
+
553
+ def test_keyboard_interrupt(self):
554
+ with patch("sys.argv", ["tokenade", "config", "path"]), \
555
+ patch("tokenade.cli.cmd_config", side_effect=KeyboardInterrupt), \
556
+ patch("sys.exit") as mock_exit:
557
+ from tokenade.cli import main
558
+ main()
559
+ mock_exit.assert_called_once_with(130)
560
+
561
+ def test_tokenade_error(self):
562
+ from tokenade.core.errors import TokenadeError
563
+
564
+ err = TokenadeError("test error message")
565
+ out = StringIO()
566
+ with patch("sys.argv", ["tokenade", "config", "path"]), \
567
+ patch("tokenade.cli.cmd_config", side_effect=err), \
568
+ patch("sys.exit") as mock_exit, \
569
+ patch("sys.stdout", out):
570
+ from tokenade.cli import main
571
+ main()
572
+ mock_exit.assert_called_once_with(1)
573
+ self.assertIn("test error message", out.getvalue())
574
+
575
+ def test_generic_exception(self):
576
+ err = RuntimeError("something broke")
577
+ out = StringIO()
578
+ with patch("sys.argv", ["tokenade", "config", "path"]), \
579
+ patch("tokenade.cli.cmd_config", side_effect=err), \
580
+ patch("sys.exit") as mock_exit, \
581
+ patch("sys.stdout", out):
582
+ from tokenade.cli import main
583
+ main()
584
+ mock_exit.assert_called_once_with(1)
585
+ output = out.getvalue()
586
+ self.assertIn("something broke", output)
587
+ self.assertIn("Unexpected error", output)
588
+
589
+
590
+ # ---------------------------------------------------------------------------
591
+ # cmd_plugin → plugin test (Phase 48)
592
+ # ---------------------------------------------------------------------------
593
+ class TestCmdPluginTest(unittest.TestCase):
594
+ """Test the plugin test CLI command."""
595
+
596
+ def test_plugin_test_no_plugins(self):
597
+ """plugin test with no plugins prints message."""
598
+ from tokenade.cli import _plugin_test
599
+ from tokenade.core.integration.plugin_testing import PluginTestRunner
600
+ args = MagicMock()
601
+ args.name = None
602
+ with patch.object(PluginTestRunner, 'test_all', return_value=[]):
603
+ out = StringIO()
604
+ with patch("sys.stdout", out):
605
+ _plugin_test(args)
606
+ self.assertIn("No plugins found", out.getvalue())
607
+
608
+ def test_plugin_test_single_pass(self):
609
+ """plugin test with a single passing plugin."""
610
+ from tokenade.cli import _plugin_test
611
+ from tokenade.core.integration.plugin_testing import PluginTestSuite, PluginTestResult
612
+ args = MagicMock()
613
+ args.name = "my-plugin"
614
+ suite = PluginTestSuite(
615
+ plugin_name="my-plugin",
616
+ results=[
617
+ PluginTestResult("manifest_exists", True),
618
+ PluginTestResult("entry_point_exists", True),
619
+ ],
620
+ )
621
+ with patch("tokenade.core.integration.plugin_testing.PluginTestRunner.test_plugin", return_value=suite):
622
+ out = StringIO()
623
+ with patch("sys.stdout", out):
624
+ _plugin_test(args)
625
+ self.assertIn("2 passed", out.getvalue())
626
+ self.assertIn("✓", out.getvalue())
627
+
628
+ def test_plugin_test_single_fail(self):
629
+ """plugin test with a failing plugin."""
630
+ from tokenade.cli import _plugin_test
631
+ from tokenade.core.integration.plugin_testing import PluginTestSuite, PluginTestResult
632
+ args = MagicMock()
633
+ args.name = "bad-plugin"
634
+ suite = PluginTestSuite(
635
+ plugin_name="bad-plugin",
636
+ results=[
637
+ PluginTestResult("manifest_exists", True),
638
+ PluginTestResult("entry_point_exists", False, "file missing"),
639
+ ],
640
+ )
641
+ with patch("tokenade.core.integration.plugin_testing.PluginTestRunner.test_plugin", return_value=suite):
642
+ out = StringIO()
643
+ with patch("sys.stdout", out):
644
+ _plugin_test(args)
645
+ self.assertIn("1 failed", out.getvalue())
646
+ self.assertIn("✗", out.getvalue())
647
+ self.assertIn("file missing", out.getvalue())
648
+
649
+
650
+ if __name__ == "__main__":
651
+ unittest.main()