8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
304 lines
11 KiB
Python
304 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests for tools/dsh_worker.py -- the one-shot CLI bridge to a dsh headless
|
|
agent.
|
|
|
|
Loaded by path, like `test_aiextract_run.py`: the tool is host CPython and
|
|
lives outside `WillTools.extension` entirely. The subprocess boundary
|
|
(`which` / `runner`) is faked here exactly the way `aiextract_run.py` fakes
|
|
`transport` -- no real dsh install and no real API key are exercised by this
|
|
suite. What matters is the part that makes this safe for a caller to trust:
|
|
every failure shape (missing binary, non-zero exit, timeout, a zero exit
|
|
with an empty reply) comes back as a named DshRefusal, never as a quiet
|
|
"ok" with nothing in it -- see the module docstring's "could not judge" vs
|
|
"judged, found nothing" rule.
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
|
|
os.path.abspath(__file__))))
|
|
TOOLS = os.path.join(ROOT, 'tools')
|
|
if TOOLS not in sys.path:
|
|
sys.path.insert(0, TOOLS)
|
|
|
|
dsh_worker = pytest.importorskip('dsh_worker')
|
|
|
|
|
|
# --------------------------------------------------------------- fixtures
|
|
|
|
def _which_found(path='C:\\fake\\dsh.cmd'):
|
|
return lambda name: path
|
|
|
|
|
|
def _which_missing():
|
|
return lambda name: None
|
|
|
|
|
|
class _FakeCompletedProcess(object):
|
|
def __init__(self, returncode=0, stdout='', stderr=''):
|
|
self.returncode = returncode
|
|
self.stdout = stdout
|
|
self.stderr = stderr
|
|
|
|
|
|
def _runner_returning(returncode=0, stdout='', stderr=''):
|
|
calls = []
|
|
|
|
def runner(cmd, capture_output, text, timeout, encoding=None, errors=None):
|
|
calls.append({'cmd': cmd, 'timeout': timeout})
|
|
return _FakeCompletedProcess(returncode, stdout, stderr)
|
|
|
|
runner.calls = calls
|
|
return runner
|
|
|
|
|
|
def _runner_raising(exc):
|
|
def runner(cmd, capture_output, text, timeout, encoding=None, errors=None):
|
|
raise exc
|
|
return runner
|
|
|
|
|
|
# ------------------------------------------------------------- run_task ok
|
|
|
|
def test_run_task_returns_the_trimmed_reply_on_success():
|
|
runner = _runner_returning(0, stdout=' pong \n', stderr='')
|
|
reply = dsh_worker.run_task('ping', which=_which_found(),
|
|
runner=runner)
|
|
assert reply == 'pong'
|
|
|
|
|
|
def test_run_task_passes_profile_and_task_through_to_the_command_line():
|
|
runner = _runner_returning(0, stdout='ok')
|
|
dsh_worker.run_task('do the thing', profile='headless',
|
|
which=_which_found('C:\\fake\\dsh.cmd'),
|
|
runner=runner)
|
|
cmd = runner.calls[0]['cmd']
|
|
assert cmd == ['C:\\fake\\dsh.cmd', '--profile', 'headless',
|
|
'do the thing']
|
|
|
|
|
|
def test_run_task_passes_the_timeout_through():
|
|
runner = _runner_returning(0, stdout='ok')
|
|
dsh_worker.run_task('x', which=_which_found(), runner=runner,
|
|
timeout_s=42)
|
|
assert runner.calls[0]['timeout'] == 42
|
|
|
|
|
|
# --------------------------------------------------------- refusal: no dsh
|
|
|
|
def test_run_task_refuses_when_dsh_is_not_on_path():
|
|
with pytest.raises(dsh_worker.DshRefusal) as exc:
|
|
dsh_worker.run_task('ping', which=_which_missing(),
|
|
runner=_runner_returning(0, stdout='ok'))
|
|
assert 'not on PATH' in str(exc.value)
|
|
assert 'npm install' in str(exc.value)
|
|
|
|
|
|
def test_run_task_refuses_on_empty_task_text():
|
|
with pytest.raises(dsh_worker.DshRefusal) as exc:
|
|
dsh_worker.run_task(' ', which=_which_found(),
|
|
runner=_runner_returning(0, stdout='ok'))
|
|
assert 'empty task' in str(exc.value)
|
|
|
|
|
|
# ------------------------------------------------------ refusal: bad exit
|
|
|
|
def test_run_task_refuses_and_quotes_stderr_on_nonzero_exit():
|
|
runner = _runner_returning(1, stdout='', stderr='MISSING_CREDENTIAL: '
|
|
'llm-deepseek: no API '
|
|
'key')
|
|
with pytest.raises(dsh_worker.DshRefusal) as exc:
|
|
dsh_worker.run_task('ping', which=_which_found(), runner=runner)
|
|
msg = str(exc.value)
|
|
assert 'exited 1' in msg
|
|
assert 'MISSING_CREDENTIAL' in msg
|
|
|
|
|
|
def test_run_task_refuses_with_a_placeholder_when_nonzero_exit_has_no_stderr():
|
|
runner = _runner_returning(1, stdout='', stderr='')
|
|
with pytest.raises(dsh_worker.DshRefusal) as exc:
|
|
dsh_worker.run_task('ping', which=_which_found(), runner=runner)
|
|
assert '(no stderr)' in str(exc.value)
|
|
|
|
|
|
# ------------------------------------------------------- refusal: timeout
|
|
|
|
def test_run_task_refuses_on_timeout_never_hangs_the_caller():
|
|
runner = _runner_raising(
|
|
subprocess.TimeoutExpired(cmd='dsh', timeout=5))
|
|
with pytest.raises(dsh_worker.DshRefusal) as exc:
|
|
dsh_worker.run_task('ping', which=_which_found(), runner=runner,
|
|
timeout_s=5)
|
|
assert '5s' in str(exc.value)
|
|
|
|
|
|
def test_run_task_refuses_when_the_process_cannot_even_start():
|
|
runner = _runner_raising(OSError('Access is denied'))
|
|
with pytest.raises(dsh_worker.DshRefusal) as exc:
|
|
dsh_worker.run_task('ping', which=_which_found(), runner=runner)
|
|
assert 'could not start dsh' in str(exc.value)
|
|
|
|
|
|
# ---------------------------------------------- refusal: THE EMPTY-OK TRAP
|
|
#
|
|
# This is the one the module docstring calls out by name: dsh exiting 0
|
|
# with nothing on stdout must never read as success. This is the same
|
|
# discipline CLAUDE.md SS3 / SS5 requires of `ticketchecks.py` skip probes --
|
|
# a check (or here, a worker call) that returns nothing owes an explanation,
|
|
# and "ran fine, empty" is a claim, not a default.
|
|
|
|
def test_run_task_refuses_when_exit_is_zero_but_stdout_is_empty():
|
|
runner = _runner_returning(0, stdout='', stderr='')
|
|
with pytest.raises(dsh_worker.DshRefusal) as exc:
|
|
dsh_worker.run_task('ping', which=_which_found(), runner=runner)
|
|
msg = str(exc.value)
|
|
assert 'exited 0 but printed no reply' in msg
|
|
assert 'could not judge' in msg
|
|
|
|
|
|
def test_run_task_refuses_when_exit_is_zero_but_stdout_is_only_whitespace():
|
|
runner = _runner_returning(0, stdout=' \n\n ', stderr='')
|
|
with pytest.raises(dsh_worker.DshRefusal):
|
|
dsh_worker.run_task('ping', which=_which_found(), runner=runner)
|
|
|
|
|
|
# --------------------------------------------------------------- CLI: main
|
|
|
|
def _run_main(argv, capsys, monkeypatch=None, stdin_text=None):
|
|
if stdin_text is not None:
|
|
import io
|
|
sys.stdin = io.StringIO(stdin_text)
|
|
code = dsh_worker.main(argv)
|
|
out = capsys.readouterr().out
|
|
return code, json.loads(out)
|
|
|
|
|
|
def test_main_prints_ok_json_and_exits_0_on_success(capsys, monkeypatch):
|
|
monkeypatch.setattr(dsh_worker, 'run_task',
|
|
lambda task, profile, timeout_s: 'pong')
|
|
code, payload = _run_main(['hello'], capsys)
|
|
assert code == 0
|
|
assert payload == {'ok': True, 'reply': 'pong'}
|
|
|
|
|
|
def test_main_prints_refusal_json_and_exits_1_on_failure(capsys, monkeypatch):
|
|
def fake(task, profile, timeout_s):
|
|
raise dsh_worker.DshRefusal('dsh is not on PATH')
|
|
monkeypatch.setattr(dsh_worker, 'run_task', fake)
|
|
code, payload = _run_main(['hello'], capsys)
|
|
assert code == 1
|
|
assert payload == {'ok': False, 'refused': True,
|
|
'reason': 'dsh is not on PATH'}
|
|
|
|
|
|
def test_main_refuses_cleanly_with_no_task_at_all(capsys, monkeypatch):
|
|
monkeypatch.setattr(sys.stdin, 'isatty', lambda: True)
|
|
code, payload = _run_main([], capsys)
|
|
assert code == 1
|
|
assert payload['ok'] is False
|
|
assert 'no task given' in payload['reason']
|
|
|
|
|
|
def test_main_reads_the_task_from_a_file(capsys, monkeypatch, tmp_path):
|
|
task_file = tmp_path / 'task.md'
|
|
task_file.write_text('summarize this', encoding='utf-8')
|
|
seen = {}
|
|
|
|
def fake(task, profile, timeout_s):
|
|
seen['task'] = task
|
|
return 'done'
|
|
|
|
monkeypatch.setattr(dsh_worker, 'run_task', fake)
|
|
code, payload = _run_main(['--file', str(task_file)], capsys)
|
|
assert code == 0
|
|
assert seen['task'] == 'summarize this'
|
|
|
|
|
|
# ------------------------------------------------------------- CLI: --plain
|
|
|
|
def test_main_plain_mode_prints_just_the_reply_on_success(capsys,
|
|
monkeypatch):
|
|
monkeypatch.setattr(dsh_worker, 'run_task',
|
|
lambda task, profile, timeout_s: 'pong')
|
|
code = dsh_worker.main(['--plain', 'hello'])
|
|
out = capsys.readouterr().out
|
|
assert code == 0
|
|
assert out == 'pong\n'
|
|
|
|
|
|
def test_main_plain_mode_prints_just_the_reason_on_refusal(capsys,
|
|
monkeypatch):
|
|
def fake(task, profile, timeout_s):
|
|
raise dsh_worker.DshRefusal('dsh is not on PATH')
|
|
monkeypatch.setattr(dsh_worker, 'run_task', fake)
|
|
code = dsh_worker.main(['--plain', 'hello'])
|
|
out = capsys.readouterr().out
|
|
assert code == 1
|
|
assert out == 'dsh is not on PATH\n'
|
|
|
|
|
|
# ---------------------------------------------- _resolve_launch_cmd: the
|
|
# Windows argv trap (module docstring: a multi-line task truncates at the
|
|
# first newline when the .CMD shim routes it through cmd.exe -- exit 0, no
|
|
# error, and the truncated task still gets a plausible-looking reply). On
|
|
# Windows, this must skip the shim and call node on its bin.js directly.
|
|
|
|
def test_resolve_launch_cmd_leaves_non_windows_paths_alone(monkeypatch):
|
|
monkeypatch.setattr(dsh_worker.os, 'name', 'posix')
|
|
assert dsh_worker._resolve_launch_cmd('/usr/local/bin/dsh') == \
|
|
['/usr/local/bin/dsh']
|
|
|
|
|
|
def test_resolve_launch_cmd_leaves_non_shim_windows_paths_alone(monkeypatch):
|
|
monkeypatch.setattr(dsh_worker.os, 'name', 'nt')
|
|
# not a .cmd/.bat -- e.g. a real dsh.exe or a git-bash shell shim
|
|
assert dsh_worker._resolve_launch_cmd('C:\\fake\\dsh') == \
|
|
['C:\\fake\\dsh']
|
|
|
|
|
|
def test_resolve_launch_cmd_falls_back_when_bin_js_is_not_where_expected(
|
|
monkeypatch):
|
|
monkeypatch.setattr(dsh_worker.os, 'name', 'nt')
|
|
# C:\fake\dsh.cmd does not exist on disk, so its sibling bin.js can't
|
|
# either -- must fall back to invoking the shim rather than crash.
|
|
assert dsh_worker._resolve_launch_cmd('C:\\fake\\dsh.cmd') == \
|
|
['C:\\fake\\dsh.cmd']
|
|
|
|
|
|
def test_resolve_launch_cmd_calls_node_on_bin_js_directly_when_present(
|
|
monkeypatch, tmp_path):
|
|
monkeypatch.setattr(dsh_worker.os, 'name', 'nt')
|
|
shim_dir = tmp_path
|
|
bin_js_dir = shim_dir / 'node_modules' / '@deepseek-ai' / 'dsh' / 'lib'
|
|
bin_js_dir.mkdir(parents=True)
|
|
bin_js = bin_js_dir / 'bin.js'
|
|
bin_js.write_text('// fake', encoding='utf-8')
|
|
node_exe = shim_dir / 'node.exe'
|
|
node_exe.write_text('', encoding='utf-8')
|
|
|
|
dsh_cmd = str(shim_dir / 'dsh.CMD')
|
|
result = dsh_worker._resolve_launch_cmd(dsh_cmd)
|
|
assert result == [str(node_exe), str(bin_js)]
|
|
|
|
|
|
def test_resolve_launch_cmd_falls_back_to_which_node_when_no_sibling_node(
|
|
monkeypatch, tmp_path):
|
|
monkeypatch.setattr(dsh_worker.os, 'name', 'nt')
|
|
shim_dir = tmp_path
|
|
bin_js_dir = shim_dir / 'node_modules' / '@deepseek-ai' / 'dsh' / 'lib'
|
|
bin_js_dir.mkdir(parents=True)
|
|
bin_js = bin_js_dir / 'bin.js'
|
|
bin_js.write_text('// fake', encoding='utf-8')
|
|
# no node.exe placed beside the shim this time
|
|
|
|
monkeypatch.setattr(dsh_worker.shutil, 'which',
|
|
lambda name: '/usr/bin/node' if name == 'node'
|
|
else None)
|
|
dsh_cmd = str(shim_dir / 'dsh.cmd')
|
|
result = dsh_worker._resolve_launch_cmd(dsh_cmd)
|
|
assert result == ['/usr/bin/node', str(bin_js)]
|