drydock-cli 3.0.113__tar.gz → 3.0.114__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.
- {drydock_cli-3.0.113/drydock_cli.egg-info → drydock_cli-3.0.114}/PKG-INFO +1 -1
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tools/__init__.py +87 -1
- {drydock_cli-3.0.113 → drydock_cli-3.0.114/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/pyproject.toml +1 -1
- drydock_cli-3.0.114/tests/test_screenshot.py +59 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/LICENSE +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/NOTICE +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/README.md +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/agent.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/cci.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/cli.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/config.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/detect.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/extract.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/guards.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/poam.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/providers.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/skills.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/stig.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/web.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/setup.cfg +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_config.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_xccdf.py +0 -0
|
@@ -244,6 +244,22 @@ SCHEMAS = [
|
|
|
244
244
|
"required": ["path"],
|
|
245
245
|
},
|
|
246
246
|
},
|
|
247
|
+
{
|
|
248
|
+
"name": "Screenshot",
|
|
249
|
+
"description": "CAPTURE the current screen and SEE it with your vision — use "
|
|
250
|
+
"when the user asks you to look at their screen, review what's "
|
|
251
|
+
"displayed, debug a GUI, or read something shown on screen. Takes "
|
|
252
|
+
"the screenshot and shows it to you directly (no need to call "
|
|
253
|
+
"ViewImage after). Works on Windows (PowerShell), macOS, and "
|
|
254
|
+
"Linux with a display. Optionally pass `path` to also save the PNG.",
|
|
255
|
+
"input_schema": {
|
|
256
|
+
"type": "object",
|
|
257
|
+
"properties": {
|
|
258
|
+
"path": {"type": "string",
|
|
259
|
+
"description": "Optional file to save the PNG to. Omit to use a temp file."},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
247
263
|
{
|
|
248
264
|
"name": "Write",
|
|
249
265
|
"description": "Write content to a file, creating parent directories as needed.",
|
|
@@ -711,6 +727,74 @@ def tool_viewimage(params: dict, config: dict) -> str:
|
|
|
711
727
|
"to you. Describe it, read any text in it, or use it to answer the task.")
|
|
712
728
|
|
|
713
729
|
|
|
730
|
+
def _capture_screen(out: str) -> str | None:
|
|
731
|
+
"""Grab the whole (virtual) screen to PNG `out`. Returns an error string, or
|
|
732
|
+
None on success. Per-OS: Windows uses PowerShell + System.Drawing; macOS uses
|
|
733
|
+
screencapture; Linux tries the common grabbers in turn."""
|
|
734
|
+
import shutil
|
|
735
|
+
import subprocess
|
|
736
|
+
try:
|
|
737
|
+
if _IS_WINDOWS:
|
|
738
|
+
ps = (
|
|
739
|
+
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing; "
|
|
740
|
+
"$s=[System.Windows.Forms.SystemInformation]::VirtualScreen; "
|
|
741
|
+
"$b=New-Object System.Drawing.Bitmap $s.Width,$s.Height; "
|
|
742
|
+
"$g=[System.Drawing.Graphics]::FromImage($b); "
|
|
743
|
+
"$g.CopyFromScreen($s.Left,$s.Top,0,0,$b.Size); "
|
|
744
|
+
f"$b.Save('{out}',[System.Drawing.Imaging.ImageFormat]::Png); "
|
|
745
|
+
"$g.Dispose(); $b.Dispose()"
|
|
746
|
+
)
|
|
747
|
+
exe = shutil.which("pwsh") or shutil.which("powershell") or "powershell"
|
|
748
|
+
subprocess.run([exe, "-NoProfile", "-NonInteractive", "-Command", ps],
|
|
749
|
+
capture_output=True, timeout=30)
|
|
750
|
+
elif sys.platform == "darwin":
|
|
751
|
+
subprocess.run(["screencapture", "-x", out], capture_output=True, timeout=30)
|
|
752
|
+
else: # Linux/BSD — try grabbers in order of ubiquity (X11 + wayland)
|
|
753
|
+
grabbers = (["scrot", "-o", out], ["import", "-window", "root", out],
|
|
754
|
+
["gnome-screenshot", "-f", out], ["grim", out],
|
|
755
|
+
["spectacle", "-b", "-n", "-o", out], ["maim", out])
|
|
756
|
+
tried = [g[0] for g in grabbers if shutil.which(g[0])]
|
|
757
|
+
if not tried:
|
|
758
|
+
return ("no screenshot tool found. Install one of: scrot, imagemagick "
|
|
759
|
+
"(import), gnome-screenshot, grim (wayland), spectacle, maim.")
|
|
760
|
+
for g in grabbers:
|
|
761
|
+
if shutil.which(g[0]):
|
|
762
|
+
subprocess.run(g, capture_output=True, timeout=30)
|
|
763
|
+
if os.path.isfile(out) and os.path.getsize(out):
|
|
764
|
+
break
|
|
765
|
+
except subprocess.TimeoutExpired:
|
|
766
|
+
return "screen capture timed out (30s)."
|
|
767
|
+
except Exception as e: # noqa: BLE001 — report, never crash the loop
|
|
768
|
+
return f"screen capture failed: {e}"
|
|
769
|
+
return None
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def tool_screenshot(params: dict, config: dict) -> str:
|
|
773
|
+
"""Capture the screen to a PNG and make it visible to the vision model — the
|
|
774
|
+
returned absolute path is auto-attached by the API boundary, same as ViewImage."""
|
|
775
|
+
import tempfile
|
|
776
|
+
|
|
777
|
+
raw = _as_str_arg(params.get("path")).strip()
|
|
778
|
+
out = os.path.abspath(_resolve_path(raw, config) if raw
|
|
779
|
+
else os.path.join(tempfile.gettempdir(), "drydock_screenshot.png"))
|
|
780
|
+
try:
|
|
781
|
+
if os.path.dirname(out):
|
|
782
|
+
os.makedirs(os.path.dirname(out), exist_ok=True)
|
|
783
|
+
except OSError:
|
|
784
|
+
pass
|
|
785
|
+
err = _capture_screen(out)
|
|
786
|
+
if err:
|
|
787
|
+
return (f"Error: {err} (On a headless server there is no display to capture.)")
|
|
788
|
+
if not os.path.isfile(out) or not os.path.getsize(out):
|
|
789
|
+
return "Error: screen capture produced no image (no display, or the grabber failed)."
|
|
790
|
+
size = os.path.getsize(out)
|
|
791
|
+
if size > _MAX_VIEW_IMAGE_BYTES:
|
|
792
|
+
return (f"Captured the screen to {out} but it is {size // 1_000_000}MB — too "
|
|
793
|
+
"large to view (limit 20MB). The file is saved; open it manually.")
|
|
794
|
+
return (f"Captured the screen to {out} (PNG, {size // 1024 or 1}KB) — it is now "
|
|
795
|
+
"visible to you. Describe what's on screen or use it to answer the task.")
|
|
796
|
+
|
|
797
|
+
|
|
714
798
|
def tool_read(params: dict, config: dict) -> str:
|
|
715
799
|
fp = _resolve_path(_as_str_arg(params.get("file_path")), config)
|
|
716
800
|
# Reading an image as text yields binary garbage — point at the vision tool.
|
|
@@ -1801,6 +1885,7 @@ def tool_build_knowledge(params: dict, config: dict) -> str:
|
|
|
1801
1885
|
|
|
1802
1886
|
_TOOLS = [
|
|
1803
1887
|
("Read", tool_read, True),
|
|
1888
|
+
("Screenshot", tool_screenshot, True),
|
|
1804
1889
|
("Write", tool_write, False),
|
|
1805
1890
|
("Edit", tool_edit, False),
|
|
1806
1891
|
("Bash", tool_bash, False),
|
|
@@ -1829,6 +1914,7 @@ def register_all():
|
|
|
1829
1914
|
name = schema["name"]
|
|
1830
1915
|
func = {
|
|
1831
1916
|
"Read": tool_read, "ViewImage": tool_viewimage,
|
|
1917
|
+
"Screenshot": tool_screenshot,
|
|
1832
1918
|
"Write": tool_write, "Edit": tool_edit,
|
|
1833
1919
|
"Bash": tool_bash, "Glob": tool_glob, "Grep": tool_grep,
|
|
1834
1920
|
"todo": tool_todo, "task": tool_task, "Dispatch": tool_dispatch,
|
|
@@ -1843,7 +1929,7 @@ def register_all():
|
|
|
1843
1929
|
# Read-only w.r.t. the parent's files (GitStatus/Diff/Log inspect only;
|
|
1844
1930
|
# GitCommit + GraphAdd write).
|
|
1845
1931
|
read_only = name in (
|
|
1846
|
-
"Read", "ViewImage", "Glob", "Grep", "task", "Dispatch", "Consult",
|
|
1932
|
+
"Read", "ViewImage", "Screenshot", "Glob", "Grep", "task", "Dispatch", "Consult",
|
|
1847
1933
|
"Knowledge", "GraphQuery", "StigRules", "StigRule",
|
|
1848
1934
|
"WebSearch", "WebFetch", "GitStatus", "GitDiff", "GitLog",
|
|
1849
1935
|
)
|
|
@@ -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.
|
|
10
|
+
version = "3.0.114"
|
|
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,59 @@
|
|
|
1
|
+
"""Screenshot tool: capture the screen to PNG and make it visible to the vision
|
|
2
|
+
model (the returned path auto-attaches, same path as ViewImage). Cross-platform:
|
|
3
|
+
PowerShell on Windows, screencapture on macOS, common grabbers on Linux."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from unittest import mock
|
|
9
|
+
|
|
10
|
+
from drydock import tools, tool_registry
|
|
11
|
+
from drydock.providers import detect_image_paths
|
|
12
|
+
from drydock.tuning import filter_tool_schemas
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_registered_and_available_to_gemma():
|
|
16
|
+
names = [s["name"] for s in tool_registry.schemas()]
|
|
17
|
+
gnames = [s["name"] for s in filter_tool_schemas(tool_registry.schemas(), "gemma4")]
|
|
18
|
+
assert "Screenshot" in names and "Screenshot" in gnames
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_graceful_when_no_grabber(monkeypatch):
|
|
22
|
+
monkeypatch.setattr(tools, "_IS_WINDOWS", False)
|
|
23
|
+
monkeypatch.setattr("sys.platform", "linux")
|
|
24
|
+
monkeypatch.setattr("shutil.which", lambda _n: None) # no grabbers installed
|
|
25
|
+
out = tools.tool_screenshot({}, {"cwd": os.getcwd()})
|
|
26
|
+
assert out.startswith("Error") and "screenshot tool" in out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_success_returns_attachable_path():
|
|
30
|
+
png = tempfile.mktemp(suffix=".png")
|
|
31
|
+
|
|
32
|
+
def fake_cap(o):
|
|
33
|
+
open(o, "wb").write(b"\x89PNG\r\n\x1a\n" + b"\x00" * 200)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
with mock.patch.object(tools, "_capture_screen", fake_cap):
|
|
37
|
+
out = tools.tool_screenshot({"path": png}, {"cwd": os.getcwd()})
|
|
38
|
+
assert "visible to you" in out
|
|
39
|
+
assert detect_image_paths(out) == [os.path.abspath(png)] # auto-attaches
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_windows_builds_powershell_capture():
|
|
43
|
+
calls = {}
|
|
44
|
+
with mock.patch.object(tools, "_IS_WINDOWS", True), \
|
|
45
|
+
mock.patch("shutil.which", side_effect=lambda n: r"C:\pwsh.exe" if n == "pwsh" else None), \
|
|
46
|
+
mock.patch("subprocess.run", side_effect=lambda a, **k: calls.setdefault("argv", a)):
|
|
47
|
+
assert tools._capture_screen(r"C:\shot.png") is None
|
|
48
|
+
argv = calls["argv"]
|
|
49
|
+
assert argv[0].endswith("pwsh.exe") and argv[1] == "-NoProfile"
|
|
50
|
+
assert "CopyFromScreen" in argv[-1] and "ImageFormat]::Png" in argv[-1]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_macos_uses_screencapture(monkeypatch):
|
|
54
|
+
calls = {}
|
|
55
|
+
monkeypatch.setattr(tools, "_IS_WINDOWS", False)
|
|
56
|
+
monkeypatch.setattr("sys.platform", "darwin")
|
|
57
|
+
monkeypatch.setattr("subprocess.run", lambda a, **k: calls.setdefault("argv", a))
|
|
58
|
+
tools._capture_screen("/tmp/x.png")
|
|
59
|
+
assert calls["argv"][0] == "screencapture"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|