fix: spawn_test never raises — coerce env/command to str, wrap unexpected errors (#34)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 8s
CI / Tests (py3.12) (pull_request) Successful in 8s

Closes #34

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cowork Supervisor
2026-07-12 12:48:03 -04:00
parent 06326e5e9d
commit c7b2c90518
2 changed files with 58 additions and 2 deletions
+22 -2
View File
@@ -1591,12 +1591,29 @@ def spawn_test(data: dict, timeout: float = 3.0) -> dict:
"crashed" — exited with a non-zero code before timeout
"not_found" — command could not be resolved to an executable
"not_applicable" — remote server or no command; nothing to spawn
"error" — unexpected internal failure while spawning/observing
returncode: int | None
stderr: str (first ~4 KB)
detail: str
Never raises: the GUI threads (Test launch / Test all) re-enable their
buttons only when a result arrives, so an escaping exception would leave
the UI stuck. Anything unexpected comes back as outcome "error".
Run this off the UI thread — it blocks for up to `timeout` seconds.
"""
try:
return _spawn_test_impl(data, timeout)
except Exception as e:
return {
"outcome": "error",
"returncode": None,
"stderr": "",
"detail": f"unexpected error: {e!r}",
}
def _spawn_test_impl(data: dict, timeout: float) -> dict:
if "url" in data and "command" not in data:
return {
"outcome": "not_applicable",
@@ -1605,7 +1622,9 @@ def spawn_test(data: dict, timeout: float = 3.0) -> dict:
"detail": "remote server",
}
cmd = (data.get("command") or "").strip()
# str() first: pasted JSON can legally carry a non-string here and the
# value never round-trips through the editor before a Test all run.
cmd = str(data.get("command") or "").strip()
if not cmd:
return {
"outcome": "not_applicable",
@@ -1627,7 +1646,8 @@ def spawn_test(data: dict, timeout: float = 3.0) -> dict:
args_list = [resolved_cmd] + [str(a) for a in (data.get("args") or [])]
merged_env = {**os.environ, "PATH": augmented_path()}
merged_env.update(data.get("env") or {})
# Popen rejects non-string env values; pasted JSON may carry numbers.
merged_env.update({str(k): str(v) for k, v in (data.get("env") or {}).items()})
stderr_chunks: list[bytes] = []