drydock-cli 3.0.107__tar.gz → 3.0.109__tar.gz

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 (121) hide show
  1. {drydock_cli-3.0.107/drydock_cli.egg-info → drydock_cli-3.0.109}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/tools/__init__.py +49 -7
  3. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/tuning.py +27 -1
  4. {drydock_cli-3.0.107 → drydock_cli-3.0.109/drydock_cli.egg-info}/PKG-INFO +1 -1
  5. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock_cli.egg-info/SOURCES.txt +1 -0
  6. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/pyproject.toml +1 -1
  7. drydock_cli-3.0.109/tests/test_windows_shell.py +79 -0
  8. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/LICENSE +0 -0
  9. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/NOTICE +0 -0
  10. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/README.md +0 -0
  11. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/__init__.py +0 -0
  12. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/__main__.py +0 -0
  13. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/advisor.py +0 -0
  14. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/agent.py +0 -0
  15. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/bash_safety.py +0 -0
  16. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/builtin_skills/__init__.py +0 -0
  17. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/builtin_skills/rmf-categorize.md +0 -0
  18. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/builtin_skills/rmf-control.md +0 -0
  19. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/builtin_skills/rmf-poam.md +0 -0
  20. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/builtin_skills/rmf-review.md +0 -0
  21. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/builtin_skills/stig-assess.md +0 -0
  22. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/builtin_skills/stig-remediate.md +0 -0
  23. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/cci.py +0 -0
  24. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/cli.py +0 -0
  25. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/compaction.py +0 -0
  26. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/config.py +0 -0
  27. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/detect.py +0 -0
  28. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/extract.py +0 -0
  29. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/gittools.py +0 -0
  30. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/graphrag.py +0 -0
  31. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/guards.py +0 -0
  32. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/loop_detect.py +0 -0
  33. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/mcp.py +0 -0
  34. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/poam.py +0 -0
  35. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/providers.py +0 -0
  36. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/rmf.py +0 -0
  37. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/rmf_graph.py +0 -0
  38. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/skills.py +0 -0
  39. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/stig.py +0 -0
  40. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/tool_registry.py +0 -0
  41. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/tui/__init__.py +0 -0
  42. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/tui/app.py +0 -0
  43. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/tui/approval.py +0 -0
  44. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/tui/messages.py +0 -0
  45. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/tui/widgets.py +0 -0
  46. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock/web.py +0 -0
  47. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock_cli.egg-info/dependency_links.txt +0 -0
  48. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock_cli.egg-info/entry_points.txt +0 -0
  49. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock_cli.egg-info/requires.txt +0 -0
  50. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/drydock_cli.egg-info/top_level.txt +0 -0
  51. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/setup.cfg +0 -0
  52. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_advisor.py +0 -0
  53. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_approval.py +0 -0
  54. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_back_command.py +0 -0
  55. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_background.py +0 -0
  56. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_binary_output.py +0 -0
  57. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_crossplatform.py +0 -0
  58. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_output_bounding.py +0 -0
  59. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_process_group.py +0 -0
  60. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_safety.py +0 -0
  61. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_sanitize.py +0 -0
  62. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_shell.py +0 -0
  63. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_stdin.py +0 -0
  64. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_stop_partial.py +0 -0
  65. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_timeout_network.py +0 -0
  66. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_bash_timeout_param.py +0 -0
  67. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_cci.py +0 -0
  68. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_cli_agents.py +0 -0
  69. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_compact_command.py +0 -0
  70. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_compaction.py +0 -0
  71. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_config.py +0 -0
  72. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_config_migration.py +0 -0
  73. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_context_limit_config.py +0 -0
  74. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_detect.py +0 -0
  75. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_dispatch.py +0 -0
  76. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_e2e_connected.py +0 -0
  77. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_edit_replace_all.py +0 -0
  78. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_empty_response.py +0 -0
  79. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_extract.py +0 -0
  80. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_failure_loop.py +0 -0
  81. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_first_run_setup.py +0 -0
  82. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_gittools.py +0 -0
  83. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_glob_edit_edges.py +0 -0
  84. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_graphify_example.py +0 -0
  85. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_graphrag.py +0 -0
  86. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_grep_and_read_robust.py +0 -0
  87. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_guards_and_tools.py +0 -0
  88. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_hallucinated_tools.py +0 -0
  89. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_leaked_tool_call.py +0 -0
  90. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_loop_detect.py +0 -0
  91. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_mcp.py +0 -0
  92. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_oneshot_unreachable.py +0 -0
  93. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_overthink_interrupt.py +0 -0
  94. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_plan_autocontinue.py +0 -0
  95. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_poam.py +0 -0
  96. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_providers_unreachable.py +0 -0
  97. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_read_index.py +0 -0
  98. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_rmf.py +0 -0
  99. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_rmf_graph.py +0 -0
  100. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_rmf_stig_graph.py +0 -0
  101. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_runaway_repetition.py +0 -0
  102. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_server_probe.py +0 -0
  103. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_skills.py +0 -0
  104. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_stall_retry.py +0 -0
  105. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_stig.py +0 -0
  106. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_stop.py +0 -0
  107. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_streaming_newlines.py +0 -0
  108. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_subagent.py +0 -0
  109. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_system_prompt_help.py +0 -0
  110. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_todo.py +0 -0
  111. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_tool_arg_coercion.py +0 -0
  112. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_tool_arg_coercion_more.py +0 -0
  113. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_tool_arg_parsing.py +0 -0
  114. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_tools_undo.py +0 -0
  115. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_tui.py +0 -0
  116. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_tuning.py +0 -0
  117. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_viewimage.py +0 -0
  118. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_vision_input.py +0 -0
  119. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_web_tools.py +0 -0
  120. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_write_content_coerce.py +0 -0
  121. {drydock_cli-3.0.107 → drydock_cli-3.0.109}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.107
3
+ Version: 3.0.109
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -6,6 +6,7 @@ Each tool is a function (params, config) -> str.
6
6
  from __future__ import annotations
7
7
 
8
8
  import os
9
+ import sys
9
10
  import re
10
11
  import difflib
11
12
  import glob as _glob
@@ -92,10 +93,45 @@ def _detect_bash() -> str | None:
92
93
  return None
93
94
 
94
95
 
95
- _IS_WINDOWS = os.name == "nt"
96
- # Resolved once at import (in whatever environment drydock runs — host, the task
97
- # container, Linux, or Windows). None bash unavailable, fall back to the
98
- # platform's default shell (cmd.exe on Windows, /bin/sh on POSIX).
96
+ # Windows even if a non-native Python (rare) reports a posix os.name sys.platform
97
+ # is the reliable signal on CPython.
98
+ _IS_WINDOWS = os.name == "nt" or sys.platform.startswith("win")
99
+
100
+
101
+ def _detect_shell() -> tuple[str, str]:
102
+ """Pick the shell tool_bash runs commands in — returns (kind, path).
103
+
104
+ Override with env DRYDOCK_SHELL=powershell|cmd|bash to force one. Otherwise:
105
+ POSIX → bash (so the model's bash-isms work), else /bin/sh.
106
+ Windows → PowerShell (what users actually launch drydock in — pwsh, else
107
+ Windows PowerShell), else cmd.exe. WSL / Git-Bash is NEVER required on Windows.
108
+ """
109
+ import shutil
110
+
111
+ def _powershell():
112
+ p = shutil.which("pwsh") or shutil.which("powershell")
113
+ return ("powershell", p) if p else None
114
+
115
+ def _bash():
116
+ b = _detect_bash()
117
+ return ("bash", b) if b else ("sh", shutil.which("sh") or "/bin/sh")
118
+
119
+ forced = os.environ.get("DRYDOCK_SHELL", "").strip().lower()
120
+ if forced in ("powershell", "pwsh"):
121
+ return _powershell() or ("cmd", shutil.which("cmd") or "cmd.exe")
122
+ if forced == "cmd":
123
+ return ("cmd", shutil.which("cmd") or "cmd.exe")
124
+ if forced in ("bash", "sh"):
125
+ return _bash()
126
+
127
+ if _IS_WINDOWS:
128
+ return _powershell() or ("cmd", shutil.which("cmd") or "cmd.exe")
129
+ return _bash()
130
+
131
+
132
+ # Resolved once at import (in whatever environment drydock runs — Linux/macOS,
133
+ # the task container, or native Windows). _BASH_SHELL kept for back-compat.
134
+ _SHELL_KIND, _SHELL_PATH = _detect_shell()
99
135
  _BASH_SHELL = _detect_bash()
100
136
 
101
137
 
@@ -1040,9 +1076,15 @@ def tool_bash(params: dict, config: dict) -> str:
1040
1076
  # would be called as `bash /c ...` and fail. `[bash, "-c", cmd]` runs the same
1041
1077
  # on Linux and Windows. Without bash, fall back to the platform default shell
1042
1078
  # (cmd.exe on Windows, /bin/sh on POSIX) via shell=True.
1043
- if _BASH_SHELL:
1044
- popen_cmd, use_shell = [_BASH_SHELL, "-c", cmd], False
1045
- else:
1079
+ # Build the invocation for the detected shell. Explicit argv (shell=False)
1080
+ # for bash/PowerShell so it's correct on every OS; shell=True lets the
1081
+ # platform default (/bin/sh on POSIX, cmd.exe on Windows) handle the rest.
1082
+ if _SHELL_KIND == "bash":
1083
+ popen_cmd, use_shell = [_SHELL_PATH, "-c", cmd], False
1084
+ elif _SHELL_KIND == "powershell":
1085
+ popen_cmd, use_shell = [_SHELL_PATH, "-NoProfile", "-NonInteractive",
1086
+ "-Command", cmd], False
1087
+ else: # "sh" (POSIX) or "cmd" (Windows) → platform default shell
1046
1088
  popen_cmd, use_shell = cmd, True
1047
1089
  try:
1048
1090
  proc = subprocess.Popen(
@@ -272,10 +272,36 @@ _DRYDOCK_COMMANDS_HELP = (
272
272
  )
273
273
 
274
274
 
275
+ def _shell_env_note() -> str:
276
+ """On Windows, tell the model its shell is PowerShell/cmd so it stops
277
+ emitting bash it can't run. Empty on POSIX (bash is the default assumption)."""
278
+ import os
279
+ import shutil
280
+ if os.name != "nt":
281
+ return ""
282
+ if shutil.which("pwsh") or shutil.which("powershell"):
283
+ return (
284
+ "\nENVIRONMENT: You are on WINDOWS and the Bash tool runs your commands "
285
+ "through POWERSHELL, not bash. Use PowerShell / Windows commands: "
286
+ "Get-ChildItem (ls), Get-Content (cat), Select-String (grep), Copy-Item, "
287
+ "Move-Item, Remove-Item, New-Item, Test-Path, Where-Object. Many Unix "
288
+ "aliases (ls, cat, cp, rm, pwd, echo) also work. Do NOT use bash-only "
289
+ "syntax ([[ ]], <<<, $(...) subshells, grep/sed/awk pipelines) — it "
290
+ "fails. Use Windows paths (backslashes) and $env:VAR for environment "
291
+ "variables. Chain commands with ';' (not '&&')."
292
+ )
293
+ return (
294
+ "\nENVIRONMENT: You are on WINDOWS and the Bash tool runs your commands "
295
+ "through cmd.exe, not bash. Use Windows/cmd commands (dir, type, findstr, "
296
+ "copy, del, move, mkdir); do NOT use bash syntax. Use backslash paths, "
297
+ "%VAR% for environment variables, and '&' or '&&' to chain commands."
298
+ )
299
+
300
+
275
301
  def system_prompt_for_model(model: str | None) -> str:
276
302
  """Return the system prompt best suited to the model."""
277
303
  base = _GEMMA_SYSTEM_PROMPT if is_gemma(model) else _DEFAULT_SYSTEM_PROMPT
278
- return base + _DRYDOCK_COMMANDS_HELP
304
+ return base + _DRYDOCK_COMMANDS_HELP + _shell_env_note()
279
305
 
280
306
 
281
307
  def thinking_level_for_turn(turn_count: int, is_user_turn: bool) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.107
3
+ Version: 3.0.109
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -114,5 +114,6 @@ tests/test_tuning.py
114
114
  tests/test_viewimage.py
115
115
  tests/test_vision_input.py
116
116
  tests/test_web_tools.py
117
+ tests/test_windows_shell.py
117
118
  tests/test_write_content_coerce.py
118
119
  tests/test_xccdf.py
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
  # PyPI distribution name is drydock-cli (the established install name, continued
8
8
  # from the retired v2 fork); the import package + CLI command stay `drydock`.
9
9
  name = "drydock-cli"
10
- version = "3.0.107"
10
+ version = "3.0.109"
11
11
  description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -0,0 +1,79 @@
1
+ """drydock runs natively on Windows via PowerShell/cmd — no WSL/bash required.
2
+ tool_bash picks the shell (POSIX→bash, Windows→PowerShell else cmd) and builds
3
+ the right invocation; the system prompt tells the model which shell it's on."""
4
+ from __future__ import annotations
5
+
6
+ import subprocess
7
+ from unittest import mock
8
+
9
+ import drydock.tools as T
10
+ from drydock import tuning
11
+
12
+
13
+ def test_posix_uses_bash():
14
+ assert T._SHELL_KIND in ("bash", "sh")
15
+
16
+
17
+ def test_windows_prefers_powershell():
18
+ with mock.patch.object(T, "_IS_WINDOWS", True), \
19
+ mock.patch("shutil.which", side_effect=lambda n: r"C:\ps\pwsh.exe" if n == "pwsh" else None):
20
+ kind, path = T._detect_shell()
21
+ assert kind == "powershell" and path.endswith("pwsh.exe")
22
+
23
+
24
+ def test_windows_falls_back_to_cmd():
25
+ with mock.patch.object(T, "_IS_WINDOWS", True), \
26
+ mock.patch("shutil.which", side_effect=lambda n: r"C:\Windows\System32\cmd.exe" if n == "cmd" else None):
27
+ kind, _ = T._detect_shell()
28
+ assert kind == "cmd"
29
+
30
+
31
+ def test_powershell_invocation_argv(monkeypatch):
32
+ """When the shell is PowerShell, Popen is called [pwsh -NoProfile
33
+ -NonInteractive -Command <cmd>] with shell=False — not shell=True."""
34
+ seen = {}
35
+
36
+ def spy(popen_cmd, *a, **kw):
37
+ seen["cmd"] = popen_cmd
38
+ seen["shell"] = kw.get("shell")
39
+ raise RuntimeError("stop-after-capture") # don't actually run pwsh on Linux
40
+
41
+ monkeypatch.setattr(T, "_SHELL_KIND", "powershell")
42
+ monkeypatch.setattr(T, "_SHELL_PATH", r"C:\ps\pwsh.exe")
43
+ monkeypatch.setattr(subprocess, "Popen", spy)
44
+ T.tool_bash({"command": "Get-ChildItem", "timeout": 5}, {"cwd": "."})
45
+ assert seen["shell"] is False
46
+ assert seen["cmd"][:4] == [r"C:\ps\pwsh.exe", "-NoProfile", "-NonInteractive", "-Command"]
47
+ assert seen["cmd"][4] == "Get-ChildItem"
48
+
49
+
50
+ def test_prompt_note_windows_powershell():
51
+ with mock.patch("os.name", "nt"), \
52
+ mock.patch("shutil.which", side_effect=lambda n: "x" if n in ("pwsh", "powershell") else None):
53
+ assert "POWERSHELL" in tuning._shell_env_note()
54
+
55
+
56
+ def test_prompt_note_windows_cmd():
57
+ with mock.patch("os.name", "nt"), mock.patch("shutil.which", return_value=None):
58
+ assert "cmd.exe" in tuning._shell_env_note()
59
+
60
+
61
+ def test_prompt_note_empty_on_posix():
62
+ assert tuning._shell_env_note() == ""
63
+
64
+
65
+ def test_env_override_forces_powershell():
66
+ with mock.patch.dict("os.environ", {"DRYDOCK_SHELL": "powershell"}), \
67
+ mock.patch("shutil.which", side_effect=lambda n: r"C:\pwsh.exe" if n in ("pwsh", "powershell") else None):
68
+ assert T._detect_shell()[0] == "powershell"
69
+
70
+
71
+ def test_env_override_forces_cmd():
72
+ with mock.patch.dict("os.environ", {"DRYDOCK_SHELL": "cmd"}), \
73
+ mock.patch("shutil.which", return_value=r"C:\cmd.exe"):
74
+ assert T._detect_shell()[0] == "cmd"
75
+
76
+
77
+ def test_env_override_forces_bash():
78
+ with mock.patch.dict("os.environ", {"DRYDOCK_SHELL": "bash"}):
79
+ assert T._detect_shell()[0] in ("bash", "sh")
File without changes
File without changes
File without changes
File without changes