codebuff-agent 2.1.74__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 (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. jupyter_ext/tools/user_tools.py +132 -0
@@ -0,0 +1,58 @@
1
+ import os
2
+
3
+ from jupyter_ext.handlers import (
4
+ DEFAULT_EXECUTE_COMMAND_TIMEOUT_MS,
5
+ _append_stream_output,
6
+ _resolve_path_in_workspace,
7
+ _resolve_stream_timeout_ms,
8
+ _resolve_timeout_ms,
9
+ )
10
+
11
+
12
+ def test_resolve_path_strips_duplicate_cwd_prefix() -> None:
13
+ workspace_root = "/workspace"
14
+ requested_cwd = os.path.join(workspace_root, "extensions", "jupyter")
15
+ path = os.path.join("extensions", "jupyter", "cal.py")
16
+ resolved = _resolve_path_in_workspace(path, workspace_root, requested_cwd)
17
+ assert resolved == os.path.join(requested_cwd, "cal.py")
18
+
19
+
20
+ def test_resolve_path_relative_to_cwd() -> None:
21
+ workspace_root = "/workspace"
22
+ requested_cwd = os.path.join(workspace_root, "extensions", "jupyter")
23
+ resolved = _resolve_path_in_workspace("cal.py", workspace_root, requested_cwd)
24
+ assert resolved == os.path.join(requested_cwd, "cal.py")
25
+
26
+
27
+ def test_resolve_timeout_ms_uses_default_on_invalid() -> None:
28
+ assert _resolve_timeout_ms(None) == DEFAULT_EXECUTE_COMMAND_TIMEOUT_MS
29
+ assert _resolve_timeout_ms("not-a-number") == DEFAULT_EXECUTE_COMMAND_TIMEOUT_MS
30
+ assert _resolve_timeout_ms(-1) == DEFAULT_EXECUTE_COMMAND_TIMEOUT_MS
31
+
32
+
33
+ def test_resolve_timeout_ms_accepts_value() -> None:
34
+ assert _resolve_timeout_ms(120000) == 120000
35
+
36
+
37
+ def test_resolve_stream_timeout_ms_uses_default_on_invalid() -> None:
38
+ assert _resolve_stream_timeout_ms(None) == DEFAULT_EXECUTE_COMMAND_TIMEOUT_MS
39
+ assert (
40
+ _resolve_stream_timeout_ms("not-a-number") == DEFAULT_EXECUTE_COMMAND_TIMEOUT_MS
41
+ )
42
+
43
+
44
+ def test_resolve_stream_timeout_ms_disables_on_non_positive() -> None:
45
+ assert _resolve_stream_timeout_ms(0) is None
46
+ assert _resolve_stream_timeout_ms(-1) is None
47
+
48
+
49
+ def test_append_stream_output_appends() -> None:
50
+ output, truncated = _append_stream_output("abc", "def", max_bytes=10)
51
+ assert output == "abcdef"
52
+ assert truncated is False
53
+
54
+
55
+ def test_append_stream_output_truncates() -> None:
56
+ output, truncated = _append_stream_output("abc", "def", max_bytes=5)
57
+ assert output == "abcde"
58
+ assert truncated is True
@@ -0,0 +1,204 @@
1
+ """`/mcp_test` 백엔드 헬퍼 테스트.
2
+
3
+ bdp_mcp 는 이 패키지의 의존성이 아니므로(별도 venv 설치, 3.9/3.10 요구 충돌),
4
+ 여기서는 **설치 여부와 무관하게 도는** 순수 함수만 검증한다.
5
+ 실제 프로토콜 왕복은 bdp_mcp 쪽 selftest 가 본다.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+
14
+ from jupyter_ext.http_handlers import (
15
+ _kill_quietly,
16
+ _terminate_then_kill,
17
+ _BDP_MCP_PYTHON_ENV,
18
+ _bdp_mcp_python_candidates,
19
+ _build_selftest_cmd,
20
+ _parse_selftest_output,
21
+ )
22
+
23
+
24
+ def test_cmd_omits_transport_when_both() -> None:
25
+ """both 는 상류 기본값이라 넘기지 않는다 — 넘기면 인자만 늘고 의미는 같다."""
26
+ cmd = _build_selftest_cmd("/x/python", "both")
27
+ assert cmd == ["/x/python", "-m", "awslabs.bdp_mcp_server.selftest", "--json"]
28
+
29
+
30
+ def test_cmd_includes_transport_and_port() -> None:
31
+ cmd = _build_selftest_cmd("/x/python", "streamable-http", 18099)
32
+ assert cmd[-4:] == ["--transport", "streamable-http", "--port", "18099"]
33
+
34
+
35
+ def test_cmd_uses_module_not_console_script() -> None:
36
+ """콘솔 스크립트는 launchctl/nohup 기동 시 PATH 에 없을 수 있다."""
37
+ cmd = _build_selftest_cmd(sys.executable)
38
+ assert cmd[0] == sys.executable
39
+ assert cmd[1] == "-m"
40
+ assert "bdp-mcp-selftest" not in cmd
41
+
42
+
43
+ def test_parse_reads_stdout_only() -> None:
44
+ """stderr 에 경고가 섞여도 판정은 stdout 으로만 한다.
45
+
46
+ stderr 를 섞어 파싱하면 멀쩡한 결과를 "깨진 출력" 으로 오판한다.
47
+ """
48
+ report = {
49
+ "ok": True,
50
+ "transports": [{"transport": "stdio", "ok": True, "checks": []}],
51
+ }
52
+ parsed = _parse_selftest_output(
53
+ json.dumps(report).encode(),
54
+ b"IncompleteFieldDefinitionWarning: Field 'lifespan' ...",
55
+ 0,
56
+ )
57
+ assert parsed["report"]["ok"] is True
58
+ assert "error" not in parsed
59
+
60
+
61
+ def test_parse_reports_empty_output() -> None:
62
+ parsed = _parse_selftest_output(b"", b"boom", 1)
63
+ assert "error" in parsed
64
+ assert parsed["stderr"] == "boom"
65
+
66
+
67
+ def test_parse_reports_non_json_stdout() -> None:
68
+ """JSON 이 아니면 원문 꼬리를 남긴다 — 원인을 못 보면 진단이 아니다."""
69
+ parsed = _parse_selftest_output(b"Traceback (most recent call last):", b"", 1)
70
+ assert "error" in parsed
71
+ assert "Traceback" in parsed["stdout"]
72
+
73
+
74
+ def test_parse_rejects_unexpected_schema() -> None:
75
+ """상류 스키마가 바뀌면 조용히 통과시키지 않는다."""
76
+ parsed = _parse_selftest_output(b'{"ok": true}', b"", 0)
77
+ assert "error" in parsed
78
+ assert "transports" in parsed["error"]
79
+
80
+
81
+ def test_candidates_prefer_env_var(monkeypatch) -> None:
82
+ monkeypatch.setenv(_BDP_MCP_PYTHON_ENV, "/custom/venv/bin/python")
83
+ assert _bdp_mcp_python_candidates()[0] == "/custom/venv/bin/python"
84
+
85
+
86
+ def test_candidates_fall_back_without_env(monkeypatch) -> None:
87
+ """환경변수가 없으면 ~/repo/bdp_mcp/.venv 관습 → 서버 venv 순."""
88
+ monkeypatch.delenv(_BDP_MCP_PYTHON_ENV, raising=False)
89
+ candidates = _bdp_mcp_python_candidates()
90
+ assert candidates[0].endswith("repo/bdp_mcp/.venv/bin/python")
91
+ assert candidates[-1] == sys.executable
92
+
93
+
94
+ def test_candidates_deduplicate(monkeypatch) -> None:
95
+ """환경변수가 서버 venv 와 같으면 같은 인터프리터를 두 번 재지 않는다."""
96
+ monkeypatch.setenv(_BDP_MCP_PYTHON_ENV, sys.executable)
97
+ candidates = _bdp_mcp_python_candidates()
98
+ assert candidates.count(sys.executable) == 1
99
+
100
+
101
+ def test_candidates_expand_tilde_in_env(monkeypatch) -> None:
102
+ """`~/...` 를 확장하지 않으면 조용히 다음 후보로 폴백해 환경변수가 무시된다.
103
+
104
+ 진단 도구에서 "왜 내 설정이 안 먹나" 가 묻히는 것은 나쁜 실패다.
105
+ """
106
+ monkeypatch.setenv(_BDP_MCP_PYTHON_ENV, "~/myvenv/bin/python")
107
+ first = _bdp_mcp_python_candidates()[0]
108
+ assert "~" not in first
109
+ assert first.endswith("/myvenv/bin/python")
110
+ assert os.path.isabs(first)
111
+
112
+
113
+ class _AlreadyGoneProc:
114
+ """returncode 확인과 시그널 사이에 스스로 끝난 프로세스."""
115
+
116
+ returncode = None
117
+
118
+ def kill(self):
119
+ raise ProcessLookupError
120
+
121
+ def terminate(self):
122
+ raise ProcessLookupError
123
+
124
+ async def wait(self): # pragma: no cover - 도달하지 않아야 한다
125
+ raise AssertionError("이미 죽은 프로세스를 기다리면 안 된다")
126
+
127
+
128
+ def test_kill_quietly_survives_process_lookup_error() -> None:
129
+ """ProcessLookupError 가 except 블록 안에서 터지면 응답 없는 500 이 된다.
130
+
131
+ 진단 도구가 진단 불가로 죽는 형태라 반드시 삼켜야 한다.
132
+ """
133
+ asyncio.run(_kill_quietly(_AlreadyGoneProc()))
134
+
135
+
136
+ def test_terminate_then_kill_survives_process_lookup_error() -> None:
137
+ asyncio.run(_terminate_then_kill(_AlreadyGoneProc()))
138
+
139
+
140
+ def test_kill_helpers_ignore_finished_process() -> None:
141
+ class _Done:
142
+ returncode = 0
143
+
144
+ asyncio.run(_kill_quietly(_Done()))
145
+ asyncio.run(_terminate_then_kill(_Done()))
146
+
147
+
148
+ def test_terminate_then_kill_prefers_sigterm() -> None:
149
+ """SIGKILL 로 바로 죽이면 selftest 의 finally 가 못 돌아 손자 MCP 서버가
150
+ 고아로 남아 포트를 계속 점유한다. SIGTERM 을 먼저 준다."""
151
+ calls = []
152
+
153
+ class _Proc:
154
+ returncode = None
155
+
156
+ def terminate(self):
157
+ calls.append("terminate")
158
+ self.returncode = -15
159
+
160
+ def kill(self): # pragma: no cover - 여기 오면 안 된다
161
+ calls.append("kill")
162
+
163
+ async def wait(self):
164
+ return self.returncode
165
+
166
+ asyncio.run(_terminate_then_kill(_Proc()))
167
+ assert calls == ["terminate"]
168
+
169
+
170
+ def test_terminate_then_kill_escalates_when_sigterm_ignored() -> None:
171
+ calls = []
172
+
173
+ class _Stubborn:
174
+ returncode = None
175
+
176
+ def terminate(self):
177
+ calls.append("terminate")
178
+
179
+ def kill(self):
180
+ calls.append("kill")
181
+ self.returncode = -9
182
+
183
+ async def wait(self):
184
+ if self.returncode is None:
185
+ await asyncio.sleep(10) # SIGTERM 을 무시하는 프로세스
186
+ return self.returncode
187
+
188
+ asyncio.run(_terminate_then_kill(_Stubborn(), grace=0.05))
189
+ assert calls == ["terminate", "kill"]
190
+
191
+
192
+ def test_cmd_adds_call_flag() -> None:
193
+ """`--call` 은 필수 인자 없는 도구를 실제로 호출한다 (실 AWS)."""
194
+ assert _build_selftest_cmd("/x/python", call_tools=True)[-1] == "--call"
195
+
196
+
197
+ def test_cmd_omits_call_flag_by_default() -> None:
198
+ """기본은 호출하지 않는다 — 자격 없는 환경에서 늘 빨간불이 되면 안 된다."""
199
+ assert "--call" not in _build_selftest_cmd("/x/python")
200
+
201
+
202
+ def test_cmd_combines_transport_port_and_call() -> None:
203
+ cmd = _build_selftest_cmd("/x/python", "streamable-http", 18099, True)
204
+ assert cmd[-5:] == ["--transport", "streamable-http", "--port", "18099", "--call"]
@@ -0,0 +1,272 @@
1
+ """Tests for mermaid_fixer — deterministic Mermaid syntax repair."""
2
+
3
+ from jupyter_ext.agent.mermaid_fixer import fix_mermaid_blocks, fix_mermaid_code
4
+
5
+
6
+ # ---------------------------------------------------------------------------
7
+ # Category 1: Comments
8
+ # ---------------------------------------------------------------------------
9
+
10
+
11
+ def test_fix_comments():
12
+ code = "graph TD\n A --> B %% this is a comment\n B --> C"
13
+ result = fix_mermaid_code(code)
14
+ assert "%%" not in result
15
+ assert "A --> B" in result
16
+
17
+
18
+ def test_fix_comments_standalone_line():
19
+ code = "graph TD\n %% full line comment\n A --> B"
20
+ result = fix_mermaid_code(code)
21
+ assert "%%" not in result
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Category 2: Reserved keyword `end`
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ def test_fix_end_keyword_as_node():
30
+ code = "graph TD\n start --> end"
31
+ result = fix_mermaid_code(code)
32
+ assert "end_node" in result
33
+
34
+
35
+ def test_fix_end_preserves_subgraph_closer():
36
+ code = "graph TD\n subgraph sub1\n A --> B\n end"
37
+ result = fix_mermaid_code(code)
38
+ # The standalone `end` (subgraph closer) must remain
39
+ assert result.strip().endswith("end")
40
+
41
+
42
+ def test_fix_end_preserves_directive():
43
+ code = "graph TD\n class end myClass"
44
+ result = fix_mermaid_code(code)
45
+ # `end` inside a class directive must remain
46
+ assert "class end" in result
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Category 3: Escaped quotes
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ def test_fix_escaped_quotes():
55
+ code = 'A["hello \\"world\\""]'
56
+ result = fix_mermaid_code(code)
57
+ assert '\\"' not in result
58
+ assert """ in result
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Category 4: Arrow syntax
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ def test_fix_single_arrow():
67
+ code = "graph TD\n A -> B"
68
+ result = fix_mermaid_code(code)
69
+ assert " --> " in result
70
+
71
+
72
+ def test_fix_dotted_arrow():
73
+ code = "graph TD\n A -. B"
74
+ result = fix_mermaid_code(code)
75
+ assert "-.-> " in result
76
+
77
+
78
+ def test_double_arrow_unchanged():
79
+ code = "graph TD\n A --> B"
80
+ result = fix_mermaid_code(code)
81
+ assert " --> " in result
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Category 5: Nested brackets
86
+ # ---------------------------------------------------------------------------
87
+
88
+
89
+ def test_fix_nested_brackets():
90
+ code = 'graph TD\n H["dask[dataframe] 설치"]'
91
+ result = fix_mermaid_code(code)
92
+ assert "[" in result
93
+ assert "]" in result
94
+
95
+
96
+ def test_fix_misquoted_labels():
97
+ """Misquoted: opening " but no matching closing "."""
98
+ code = "graph TD\n E[\"rcParams['family'\"] 설정]"
99
+ result = fix_mermaid_code(code)
100
+ # Should handle gracefully without crashing
101
+ assert "E[" in result
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Category 6: Node labels
106
+ # ---------------------------------------------------------------------------
107
+
108
+
109
+ def test_fix_unquoted_node_label():
110
+ code = "graph TD\n A[Hello World]"
111
+ result = fix_mermaid_code(code)
112
+ assert '["Hello World"]' in result
113
+
114
+
115
+ def test_fix_already_quoted_label():
116
+ code = 'graph TD\n A["Hello World"]'
117
+ result = fix_mermaid_code(code)
118
+ assert '["Hello World"]' in result
119
+
120
+
121
+ def test_fix_subroutine_label():
122
+ code = "graph TD\n A[[My Process]]"
123
+ result = fix_mermaid_code(code)
124
+ assert '[["My Process"]]' in result
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Category 7: Edge labels
129
+ # ---------------------------------------------------------------------------
130
+
131
+
132
+ def test_fix_edge_labels_with_brackets():
133
+ code = "graph TD\n A -->|[text]| B"
134
+ result = fix_mermaid_code(code)
135
+ assert "[" in result
136
+ assert "]" in result
137
+
138
+
139
+ def test_edge_labels_no_brackets_unchanged():
140
+ code = "graph TD\n A -->|yes| B"
141
+ result = fix_mermaid_code(code)
142
+ assert "|yes|" in result
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Category 8: Subgraph titles
147
+ # ---------------------------------------------------------------------------
148
+
149
+
150
+ def test_fix_subgraph_unquoted_title():
151
+ code = "subgraph My Section\n A --> B\nend"
152
+ result = fix_mermaid_code(code)
153
+ assert '"My Section"' in result
154
+
155
+
156
+ def test_subgraph_quoted_title_unchanged():
157
+ code = 'subgraph "My Section"\n A --> B\nend'
158
+ result = fix_mermaid_code(code)
159
+ assert '"My Section"' in result
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Category 9: Empty labels
164
+ # ---------------------------------------------------------------------------
165
+
166
+
167
+ def test_fix_empty_labels():
168
+ code = 'graph TD\n A[""] --> B'
169
+ result = fix_mermaid_code(code)
170
+ assert '[""]' not in result
171
+ assert "A --> B" in result
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Category 10: Direction
176
+ # ---------------------------------------------------------------------------
177
+
178
+
179
+ def test_fix_invalid_direction():
180
+ code = "graph TD\n direction XX\n A --> B"
181
+ result = fix_mermaid_code(code)
182
+ assert "direction TD" in result
183
+
184
+
185
+ def test_valid_direction_unchanged():
186
+ for d in ("TB", "BT", "LR", "RL", "TD"):
187
+ code = f"graph TD\n direction {d}\n A --> B"
188
+ result = fix_mermaid_code(code)
189
+ assert f"direction {d}" in result
190
+
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # Category 11: Trailing semicolons
194
+ # ---------------------------------------------------------------------------
195
+
196
+
197
+ def test_fix_trailing_semicolons():
198
+ code = "graph TD\n A --> B;\n B --> C;"
199
+ result = fix_mermaid_code(code)
200
+ assert ";" not in result
201
+
202
+
203
+ def test_no_semicolons_unchanged():
204
+ code = "graph TD\n A --> B\n B --> C"
205
+ result = fix_mermaid_code(code)
206
+ assert result.strip() == code.strip()
207
+
208
+
209
+ # ---------------------------------------------------------------------------
210
+ # Category 12: Duplicate edges
211
+ # ---------------------------------------------------------------------------
212
+
213
+
214
+ def test_fix_duplicate_edges():
215
+ code = "graph TD\n A --> B\n A --> B\n B --> C"
216
+ result = fix_mermaid_code(code)
217
+ assert result.count("A --> B") == 1
218
+ assert "B --> C" in result
219
+
220
+
221
+ def test_different_edges_preserved():
222
+ code = "graph TD\n A --> B\n B --> C"
223
+ result = fix_mermaid_code(code)
224
+ assert "A --> B" in result
225
+ assert "B --> C" in result
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Integration: fix_mermaid_blocks
230
+ # ---------------------------------------------------------------------------
231
+
232
+
233
+ def test_no_mermaid_passthrough():
234
+ text = "Hello world, no mermaid here."
235
+ assert fix_mermaid_blocks(text) == text
236
+
237
+
238
+ def test_multiple_blocks():
239
+ text = (
240
+ "Some text\n"
241
+ "```mermaid\n"
242
+ "graph TD\n A -> B\n"
243
+ "```\n"
244
+ "Middle text\n"
245
+ "```mermaid\n"
246
+ "graph LR\n C -> D\n"
247
+ "```\n"
248
+ "End text"
249
+ )
250
+ result = fix_mermaid_blocks(text)
251
+ assert result.count("-->") == 2
252
+ assert "Some text" in result
253
+ assert "Middle text" in result
254
+ assert "End text" in result
255
+
256
+
257
+ def test_composition_multiple_issues():
258
+ """Multiple issues coexist in a single block."""
259
+ code = (
260
+ "graph TD\n"
261
+ " %% comment\n"
262
+ " start --> end;\n"
263
+ " A[Hello World] -> B\n"
264
+ " direction XX\n"
265
+ )
266
+ result = fix_mermaid_code(code)
267
+ assert "%%" not in result
268
+ assert "end_node" in result
269
+ assert ";" not in result
270
+ assert " --> " in result # single arrow fixed
271
+ assert "direction TD" in result
272
+ assert '["Hello World"]' in result