Files
willtools-demo/WillTools.extension/tests/test_dsh_watch.py
T
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:32:15 -05:00

261 lines
9.8 KiB
Python

# -*- coding: utf-8 -*-
"""Tests for tools/dsh_watch.py -- the dsh background-session feedback watcher.
`dsh_watch.py` is host CPython (urllib, f-free but stdlib-modern) and lives in
`tools/`, so it is loaded the same way `tools/scrub.py` is in `test_scrub.py`:
put `tools/` on `sys.path` and `import dsh_watch` directly.
What is worth testing is the CLASSIFIER LOOP, not the HTTP. The one network
call is quarantined in `_post_json`, and the loop only ever touches `_poll`,
which returns a compact `{"asOfSeq","tail","perm","finish","ok"}` dict. So we
monkeypatch `_poll` (occasionally `_post_json`) to script the session's state
across polls, inject a fake clock and a no-op sleep, and assert the loop
reaches the right verdict + exit code without a socket ever opening or a real
second passing:
* DONE -- tail turn/end with asOfSeq frozen, finish reason "stop"
* CUT_OFF -- tail turn/end with asOfSeq frozen, finish reason "max-tokens"
(reply truncated by the output cap, not finished)
* STALLED -- asOfSeq frozen past --stall, tail not turn/end
* ERROR -- poll raises, and poll returns ok:False
* running-then-DONE -- asOfSeq advances a few polls, then turn/end freezes
* TIMEOUT -- still active past --max-minutes
No real dsh, no network, no sleeping. `_poll`/`_post_json` are restored by
pytest's monkeypatch between tests.
"""
import os
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_watch = pytest.importorskip('dsh_watch')
# --------------------------------------------------------------- helpers
def scripted(states):
"""A fake `_poll` that yields one dict per call, repeating the last one
forever once the script is exhausted (so a slow-to-terminate verdict never
trips a StopIteration)."""
box = {'i': 0}
def _poll(sid, base_url=None, timeout=10.0):
i = box['i']
if i < len(states):
box['i'] += 1
return states[min(i, len(states) - 1)]
return _poll
def stepping_clock(step=100.0):
"""A monotonic-ish clock that advances `step` seconds every call, so a
small script of polls crosses --stall / --max-minutes deterministically."""
box = {'t': 0.0}
def _now():
t = box['t']
box['t'] += step
return t
return _now
def ok(seq, tail, perm='workspace-write', finish=None):
return {'ok': True, 'asOfSeq': seq, 'tail': tail, 'perm': perm,
'finish': finish}
def run(sid='sess-1', **kw):
kw.setdefault('interval', 0.0)
kw.setdefault('sleep', lambda *_a, **_k: None)
return dsh_watch.watch(sid, **kw)
# --------------------------------------------------------------- DONE
def test_done_when_turn_end_and_seq_frozen(monkeypatch):
# Same state every poll: turn/end, seq never moves.
monkeypatch.setattr(dsh_watch, '_poll', scripted([ok(42, 'turn/end')]))
v = run(now=stepping_clock(1.0))
assert v.kind == 'DONE'
assert v.code == 0
assert 'steps=42' in v.message
assert 'turn ended' in v.message
assert v.message.startswith('DONE sess-1')
def test_running_then_done(monkeypatch):
# seq climbs for a few polls, then turn/end appears and the seq freezes.
states = [
ok(10, 'assistant/chunk'),
ok(11, 'step/end'),
ok(12, 'turn/end'), # the poll that first shows turn/end -- advanced
ok(12, 'turn/end'), # frozen now -> DONE fires here
]
monkeypatch.setattr(dsh_watch, '_poll', scripted(states))
v = run(now=stepping_clock(1.0), stall=1000.0)
assert v.kind == 'DONE'
assert v.code == 0
assert 'steps=12' in v.message
# --------------------------------------------------------------- CUT_OFF
def test_cut_off_when_turn_end_and_finish_is_max_tokens(monkeypatch):
# turn/end reached with the counter frozen, but the last finish chunk says
# the reply was truncated by the model's output cap: CUT_OFF, not DONE.
monkeypatch.setattr(dsh_watch, '_poll',
scripted([ok(42, 'turn/end', finish='max-tokens')]))
v = run(now=stepping_clock(1.0))
assert v.kind == 'CUT_OFF'
assert v.code == 5
assert 'max-tokens' in v.message
assert '"continue"' in v.message
assert v.message.startswith('CUT_OFF sess-1')
def test_turn_end_with_finish_stop_is_still_done(monkeypatch):
# The new finish field must not disturb the ordinary case: turn/end frozen
# with a normal "stop" finish is DONE with code 0, exactly as before.
monkeypatch.setattr(dsh_watch, '_poll',
scripted([ok(42, 'turn/end', finish='stop')]))
v = run(now=stepping_clock(1.0))
assert v.kind == 'DONE'
assert v.code == 0
assert 'steps=42' in v.message
assert 'turn ended' in v.message
# --------------------------------------------------------------- STALLED
def test_stalled_when_seq_frozen_and_not_turn_end(monkeypatch):
# seq never moves and the tail is a mid-turn event: blocked on a prompt.
monkeypatch.setattr(dsh_watch, '_poll',
scripted([ok(7, 'assistant/chunk', perm='read-only')]))
v = run(stall=60.0, now=stepping_clock(100.0))
assert v.kind == 'STALLED'
assert v.code == 3
assert 'seq=7' in v.message
assert 'tail=assistant/chunk' in v.message
assert 'perm=read-only' in v.message
assert 'pending approval/prompt' in v.message
def test_turn_end_never_counts_as_stalled(monkeypatch):
# A frozen seq whose tail is turn/end is DONE, never STALLED, even with a
# clock that blows past --stall on the second poll.
monkeypatch.setattr(dsh_watch, '_poll', scripted([ok(5, 'turn/end')]))
v = run(stall=1.0, now=stepping_clock(100.0))
assert v.kind == 'DONE'
# --------------------------------------------------------------- ERROR
def test_error_when_poll_raises(monkeypatch):
def boom(sid, base_url=None, timeout=10.0):
raise RuntimeError('connection refused')
monkeypatch.setattr(dsh_watch, '_poll', boom)
v = run(now=stepping_clock(1.0))
assert v.kind == 'ERROR'
assert v.code == 2
assert 'connection refused' in v.message
def test_error_when_result_not_ok(monkeypatch):
monkeypatch.setattr(dsh_watch, '_poll',
scripted([{'ok': False, 'reason': 'session not found'}]))
v = run(now=stepping_clock(1.0))
assert v.kind == 'ERROR'
assert v.code == 2
assert 'session not found' in v.message
def test_transient_hiccup_then_ok_is_not_an_error(monkeypatch):
# First two polls fail, third succeeds within the same cycle's retries ->
# the session is NOT declared dead; it goes on to reach DONE.
states = [
{'ok': False, 'reason': 'hiccup 1'},
{'ok': False, 'reason': 'hiccup 2'},
ok(3, 'turn/end'),
ok(3, 'turn/end'),
]
monkeypatch.setattr(dsh_watch, '_poll', scripted(states))
v = run(now=stepping_clock(1.0))
assert v.kind == 'DONE'
# --------------------------------------------------------------- TIMEOUT
def test_timeout_when_still_active(monkeypatch):
# seq keeps climbing every poll (genuinely busy) so DONE/STALLED never fire;
# the clock crosses --max-minutes and we bail out as TIMEOUT.
box = {'s': 100}
def busy(sid, base_url=None, timeout=10.0):
box['s'] += 1
return ok(box['s'], 'assistant/chunk')
monkeypatch.setattr(dsh_watch, '_poll', busy)
# step 40s/poll, cap 1 minute -> elapsed passes 60s on the second poll.
v = run(max_minutes=1.0, now=stepping_clock(40.0))
assert v.kind == 'TIMEOUT'
assert v.code == 4
assert 'still active' in v.message
# --------------------------------------------------------------- _poll parsing
def test_poll_parses_history_via_post_json(monkeypatch):
# Drive _poll through a faked _post_json to prove it reads the three fields
# out of the RPC envelope and never touches the (huge) events middle.
captured = {}
def fake_post(url, body, timeout):
captured['url'] = url
captured['body'] = body
return {'type': 'server-response', 'rpcId': body['rpcId'],
'result': {'ok': True, 'value': {
'events': [{'event': {'type': 'assistant/chunk'}},
{'event': {'type': 'assistant/chunk',
'data': {'chunk': {'type': 'finish',
'reason': {'kind': 'stop'}}}}},
{'event': {'type': 'turn/end'}}],
'hasMore': False,
'projections': {'asOfSeq': 99,
'values': {'permissions':
{'currentValue': 'workspace-write'}}}}}}
monkeypatch.setattr(dsh_watch, '_post_json', fake_post)
res = dsh_watch._poll('sid-xyz', base_url='http://127.0.0.1:3080')
assert res == {'ok': True, 'asOfSeq': 99, 'tail': 'turn/end',
'perm': 'workspace-write', 'finish': 'stop'}
assert captured['url'].endswith('/api/session.history')
assert captured['body']['payload']['sessionId'] == 'sid-xyz'
assert captured['body']['method'] == 'session.history'
def test_poll_reports_result_ok_false(monkeypatch):
def fake_post(url, body, timeout):
return {'result': {'ok': False, 'error': 'no such session'}}
monkeypatch.setattr(dsh_watch, '_post_json', fake_post)
res = dsh_watch._poll('nope')
assert res['ok'] is False
assert 'no such session' in res['reason']
def test_poll_swallows_transport_exception(monkeypatch):
def fake_post(url, body, timeout):
raise OSError('socket blew up')
monkeypatch.setattr(dsh_watch, '_post_json', fake_post)
res = dsh_watch._poll('sid')
assert res['ok'] is False
assert 'socket blew up' in res['reason']